Export the Accounting Dashboard as a vector PDF

Add a dashboard PDF generator that renders the metrics, an Income & Expenses
bar chart, a Top Expenses donut, the invoices overview, and recent transactions
as native PDF vector graphics + selectable text (not a screenshot), using the
shared branded header/footer. Wire an "Export PDF" button into the dashboard
header (association logo, ACM fallback).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 18:52:43 -04:00
parent cf926c04fa
commit 7b54ddd40d
2 changed files with 241 additions and 4 deletions
@@ -1,11 +1,15 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { accounting } from "@/lib/accountingClient";
import { supabase } from "@/integrations/supabase/client";
import { useCompanyId } from "./lib/useCompanyId";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { money, fmtDate } from "./lib/format";
import { generateDashboardPdf } from "./lib/dashboardPdf";
import {
Receipt, FileText, Landmark, ArrowUpRight, ArrowDownRight, Loader2,
Receipt, FileText, Landmark, ArrowUpRight, ArrowDownRight, Loader2, FileDown,
} from "lucide-react";
import {
BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid,
@@ -19,6 +23,14 @@ export default function AccountingDashboardPage({ association }: { association?:
const cid = companyId ?? "";
const c = "USD";
const { data: assocMeta } = useQuery({
queryKey: ["assoc-logo", associationId],
enabled: !!associationId,
queryFn: async () => (await supabase.from("associations").select("logo_url").eq("id", associationId!).maybeSingle()).data,
});
const logoUrl = (assocMeta as any)?.logo_url || null;
const [exporting, setExporting] = useState(false);
const { data } = useQuery({
queryKey: ["dashboard", cid],
enabled: !!cid,
@@ -160,9 +172,28 @@ export default function AccountingDashboardPage({ association }: { association?:
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Dashboard</h1>
<p className="text-sm text-muted-foreground">{associationName}</p>
<div className="flex items-start justify-between gap-4">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Dashboard</h1>
<p className="text-sm text-muted-foreground">{associationName}</p>
</div>
<Button
variant="outline"
size="sm"
className="gap-1"
disabled={!data || exporting}
onClick={async () => {
if (!data) return;
setExporting(true);
try {
await generateDashboardPdf({ companyName: associationName ?? "Company", logoUrl, currency: c, data });
} finally {
setExporting(false);
}
}}
>
{exporting ? <Loader2 className="h-4 w-4 animate-spin" /> : <FileDown className="h-4 w-4" />} Export PDF
</Button>
</div>
{/* Cash flow summary */}