import { useState, useEffect, useMemo } from "react"; import { supabase } from "@/integrations/supabase/client"; import { useToast } from "@/hooks/use-toast"; import { PieChart, Download, Printer } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; type ReportType = "trial_balance" | "income_statement" | "balance_sheet" | "aging" | "delinquency"; export default function AccountingReportsPage({ associationIds }: { associationIds?: string[] } = {}) { const { toast } = useToast(); const [associations, setAssociations] = useState([]); const [selectedAssocId, setSelectedAssocId] = useState(""); const [startDate, setStartDate] = useState(new Date(new Date().getFullYear(), 0, 1).toISOString().split("T")[0]); const [endDate, setEndDate] = useState(new Date().toISOString().split("T")[0]); const [reportType, setReportType] = useState("trial_balance"); // Data const [accounts, setAccounts] = useState([]); const [journalEntries, setJournalEntries] = useState([]); const [ownerLedger, setOwnerLedger] = useState([]); const [owners, setOwners] = useState([]); const [loading, setLoading] = useState(false); useEffect(() => { let q = supabase.from("associations").select("id, name").eq("status", "active").order("name"); // When scoped (e.g. board members), limit the picker to the given associations. if (associationIds?.length) q = q.in("id", associationIds); q.then(({ data }) => { setAssociations(data || []); if (data?.length) setSelectedAssocId(data[0].id); }); }, [associationIds?.join(",")]); const generateReport = async () => { if (!selectedAssocId) return; setLoading(true); const [acctRes, jeRes, olRes, oRes] = await Promise.all([ supabase.from("chart_of_accounts").select("*").eq("association_id", selectedAssocId).order("account_number"), supabase.from("journal_entries").select("*, chart_of_accounts(account_number, account_name, account_type)").eq("association_id", selectedAssocId).gte("date", startDate).lte("date", endDate), supabase.from("owner_ledger_entries").select("*, owners(first_name, last_name), units(unit_number)").eq("association_id", selectedAssocId), supabase.from("owners").select("id, first_name, last_name, balance, unit_id, units(unit_number)").eq("status", "active").eq("association_id", selectedAssocId).order("last_name"), ]); setAccounts(acctRes.data || []); setJournalEntries(jeRes.data || []); setOwnerLedger(olRes.data || []); setOwners(oRes.data || []); setLoading(false); }; // Trial Balance const trialBalance = useMemo(() => { const map = new Map(); journalEntries.forEach(je => { const key = je.chart_of_account_id || "uncategorized"; if (!map.has(key)) { map.set(key, { account: je.chart_of_accounts || { account_number: "N/A", account_name: "Uncategorized", account_type: "expense" }, debit: 0, credit: 0 }); } const entry = map.get(key)!; if (je.type === "debit") entry.debit += Number(je.amount); else entry.credit += Number(je.amount); }); return Array.from(map.values()).sort((a, b) => (a.account.account_number || "").localeCompare(b.account.account_number || "")); }, [journalEntries]); // Income Statement const incomeStatement = useMemo(() => { const income: typeof trialBalance = []; const expenses: typeof trialBalance = []; trialBalance.forEach(tb => { if (tb.account.account_type === "income") income.push(tb); else if (tb.account.account_type === "expense") expenses.push(tb); }); const totalIncome = income.reduce((s, i) => s + i.credit - i.debit, 0); const totalExpenses = expenses.reduce((s, e) => s + e.debit - e.credit, 0); return { income, expenses, totalIncome, totalExpenses, netIncome: totalIncome - totalExpenses }; }, [trialBalance]); // Balance Sheet const balanceSheet = useMemo(() => { const assets: typeof trialBalance = []; const liabilities: typeof trialBalance = []; const equity: typeof trialBalance = []; trialBalance.forEach(tb => { if (tb.account.account_type === "asset") assets.push(tb); else if (tb.account.account_type === "liability") liabilities.push(tb); else if (tb.account.account_type === "equity") equity.push(tb); }); return { assets, liabilities, equity, totalAssets: assets.reduce((s, a) => s + a.debit - a.credit, 0), totalLiabilities: liabilities.reduce((s, l) => s + l.credit - l.debit, 0), totalEquity: equity.reduce((s, e) => s + e.credit - e.debit, 0), }; }, [trialBalance]); // Aging Report const agingReport = useMemo(() => { const now = new Date(); return owners.filter(o => Number(o.balance) > 0).map(o => { const entries = ownerLedger.filter(e => e.owner_id === o.id && Number(e.debit) > 0); let current = 0, over30 = 0, over60 = 0, over90 = 0; entries.forEach(e => { const days = Math.floor((now.getTime() - new Date(e.date).getTime()) / 86400000); const amt = Number(e.debit) - Number(e.credit); if (amt <= 0) return; if (days <= 30) current += amt; else if (days <= 60) over30 += amt; else if (days <= 90) over60 += amt; else over90 += amt; }); return { owner: o, current, over30, over60, over90, total: Number(o.balance) }; }).sort((a, b) => b.total - a.total); }, [owners, ownerLedger]); const fmtMoney = (v: number) => `$${v.toLocaleString("en-US", { minimumFractionDigits: 2 })}`; return (

Financial Reports

Generate accounting and owner reports.

{/* Filters */}
setStartDate(e.target.value)} />
setEndDate(e.target.value)} />
setReportType(v as ReportType)}> Trial Balance Income Statement Balance Sheet Aging Report Delinquency Trial Balance Account #Account NameType DebitCredit {trialBalance.map((tb, i) => ( {tb.account.account_number} {tb.account.account_name} {tb.account.account_type} {tb.debit > 0 ? fmtMoney(tb.debit) : "—"} {tb.credit > 0 ? fmtMoney(tb.credit) : "—"} ))} {trialBalance.length > 0 && ( Totals {fmtMoney(trialBalance.reduce((s, t) => s + t.debit, 0))} {fmtMoney(trialBalance.reduce((s, t) => s + t.credit, 0))} )} {trialBalance.length === 0 && Generate a report to see data.}
Income Statement (P&L) AccountAmount Revenue {incomeStatement.income.map((i, idx) => ( {i.account.account_name}{fmtMoney(i.credit - i.debit)} ))} Total Revenue{fmtMoney(incomeStatement.totalIncome)} Expenses {incomeStatement.expenses.map((e, idx) => ( {e.account.account_name}{fmtMoney(e.debit - e.credit)} ))} Total Expenses{fmtMoney(incomeStatement.totalExpenses)} Net Income = 0 ? "text-emerald-600" : "text-destructive"}`}>{fmtMoney(incomeStatement.netIncome)} {journalEntries.length === 0 && Generate a report to see data.}
Balance Sheet AccountAmount Assets {balanceSheet.assets.map((a, i) => ( {a.account.account_name}{fmtMoney(a.debit - a.credit)} ))} Total Assets{fmtMoney(balanceSheet.totalAssets)} Liabilities {balanceSheet.liabilities.map((l, i) => ( {l.account.account_name}{fmtMoney(l.credit - l.debit)} ))} Total Liabilities{fmtMoney(balanceSheet.totalLiabilities)} Equity {balanceSheet.equity.map((e, i) => ( {e.account.account_name}{fmtMoney(e.credit - e.debit)} ))} Total Equity{fmtMoney(balanceSheet.totalEquity)} {journalEntries.length === 0 && Generate a report to see data.}
Aging Report OwnerUnit Current31-60 61-9090+ Total {agingReport.length === 0 ? ( No delinquent accounts. ) : agingReport.map((r, i) => ( {r.owner.last_name}, {r.owner.first_name} {r.owner.units?.unit_number || "—"} {r.current > 0 ? fmtMoney(r.current) : "—"} {r.over30 > 0 ? fmtMoney(r.over30) : "—"} {r.over60 > 0 ? fmtMoney(r.over60) : "—"} {r.over90 > 0 ? fmtMoney(r.over90) : "—"} {fmtMoney(r.total)} ))}
Delinquency Report OwnerUnit Balance Due {owners.filter(o => Number(o.balance) > 0).length === 0 ? ( No delinquent accounts. ) : owners.filter(o => Number(o.balance) > 0).sort((a, b) => Number(b.balance) - Number(a.balance)).map((o, i) => ( {o.last_name}, {o.first_name} {o.units?.unit_number || "—"} {fmtMoney(Number(o.balance))} ))}
); }