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, FileDown, } from "lucide-react"; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, PieChart, Pie, Cell, Legend, } from "recharts"; const DONUT_COLORS = ["#00897B", "#26A69A", "#F59E0B", "#EF4444", "#6366F1", "#8B5CF6"]; export default function AccountingDashboardPage({ association }: { association?: { id: string; name?: string } | null } = {}) { const { companyId, loading: companyLoading, error: companyError, associationId, associationName } = useCompanyId(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, queryFn: async () => { const sixMonthsAgo = new Date(); sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 5); sixMonthsAgo.setDate(1); const sinceIso = sixMonthsAgo.toISOString().slice(0, 10); const [inv, bills, tx, acc] = await Promise.all([ accounting.from("invoices").select("total,status,issue_date").eq("company_id", cid), accounting.from("bills").select("total,status,issue_date").eq("company_id", cid), accounting .from("transactions") .select("amount,type,date,description,category,reference") .eq("company_id", cid) .order("date", { ascending: false }) .limit(10), accounting.from("accounts").select("balance,is_bank").eq("company_id", cid), ]); const invoices = inv.data ?? []; const billsArr = bills.data ?? []; const txAll = tx.data ?? []; const totalReceivable = invoices .filter((i) => i.status !== "paid" && i.status !== "void") .reduce((s, i) => s + Number(i.total), 0); const totalPayable = billsArr .filter((b) => b.status !== "paid" && b.status !== "void") .reduce((s, b) => s + Number(b.total), 0); const cash = (acc.data ?? []) .filter((a) => a.is_bank) .reduce((s, a) => s + Number(a.balance), 0); // Invoice status counts const counts = { draft: 0, sent: 0, overdue: 0, paid: 0 }; for (const i of invoices) { if (i.status === "paid") counts.paid++; else if (i.status === "draft") counts.draft++; else if (i.status === "void") continue; else { if ((i as any).status === "overdue") counts.overdue++; else counts.sent++; } } // 6 month income/expense const months: { key: string; label: string; income: number; expense: number }[] = []; const now = new Date(); for (let i = 5; i >= 0; i--) { const d = new Date(now.getFullYear(), now.getMonth() - i, 1); months.push({ key: `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`, label: d.toLocaleString("en-US", { month: "short" }), income: 0, expense: 0, }); } const bucket = (date: string) => date.slice(0, 7); for (const i of invoices) { if (i.issue_date && i.issue_date >= sinceIso) { const m = months.find((x) => x.key === bucket(i.issue_date)); if (m) m.income += Number(i.total); } } for (const b of billsArr) { if (b.issue_date && b.issue_date >= sinceIso) { const m = months.find((x) => x.key === bucket(b.issue_date)); if (m) m.expense += Number(b.total); } } // Top expenses by category (from debit transactions) const catMap = new Map(); for (const t of txAll) { if (t.type === "debit") { const key = t.category || "Uncategorized"; catMap.set(key, (catMap.get(key) ?? 0) + Number(t.amount)); } } const topExpenses = Array.from(catMap.entries()) .map(([name, value]) => ({ name, value })) .sort((a, b) => b.value - a.value) .slice(0, 6); return { totalReceivable, totalPayable, cash, counts, months, topExpenses, recent: txAll, }; }, }); const summary = [ { label: "Total Receivables", value: money(data?.totalReceivable, c), icon: FileText, accent: "bg-blue-50 text-blue-600 ring-blue-100", bar: "from-blue-500/10 to-transparent", trend: "Outstanding", TrendIcon: ArrowUpRight, }, { label: "Total Payables", value: money(data?.totalPayable, c), icon: Receipt, accent: "bg-orange-50 text-orange-600 ring-orange-100", bar: "from-orange-500/10 to-transparent", trend: "Owed", TrendIcon: ArrowDownRight, }, { label: "Bank Balance", value: money(data?.cash, c), icon: Landmark, accent: "bg-emerald-50 text-emerald-600 ring-emerald-100", bar: "from-emerald-500/10 to-transparent", trend: "All accounts", TrendIcon: ArrowUpRight, }, ]; const invoiceTiles = [ { label: "Draft", value: data?.counts.draft ?? 0, dot: "bg-slate-400", badge: "bg-slate-100 text-slate-700" }, { label: "Sent", value: data?.counts.sent ?? 0, dot: "bg-blue-500", badge: "bg-blue-100 text-blue-700" }, { label: "Overdue", value: data?.counts.overdue ?? 0, dot: "bg-red-500", badge: "bg-red-100 text-red-700" }, { label: "Paid", value: data?.counts.paid ?? 0, dot: "bg-emerald-500", badge: "bg-emerald-100 text-emerald-700" }, ]; if (!associationId) return

Select an association.

; if (companyLoading) return
; if (companyError || !companyId) return

{companyError || "Accounting setup is not ready."}

; return (

Dashboard

{associationName}

{/* Cash flow summary */}
{summary.map((s) => (

{s.label}

{s.value}

{s.trend}
))}
{/* Charts row */}
Income & Expenses

Last 6 months

money(v, c)} />
Top Expenses

By category

{(data?.topExpenses?.length ?? 0) === 0 ? (
No expenses yet
) : ( {data!.topExpenses.map((_, i) => ( ))} money(v, c)} /> )}
{/* Invoices overview */} Invoices Overview
{invoiceTiles.map((t) => (
{t.label}
{t.value}
))}
{/* Recent Transactions */} Recent Transactions {(data?.recent?.length ?? 0) === 0 ? (

No transactions yet.

) : (
{data!.recent.map((t: any, i: number) => ( ))}
Date Description Category Reference Amount
{fmtDate(t.date)} {t.description} {t.category ?? "—"} {t.reference ?? "—"} {t.type === "credit" ? "+" : "−"} {money(t.amount, c)}
)}
); }