diff --git a/src/lib/zohoFinancialReportPdf.ts b/src/lib/zohoFinancialReportPdf.ts index 55c8da2..cafc2ab 100644 --- a/src/lib/zohoFinancialReportPdf.ts +++ b/src/lib/zohoFinancialReportPdf.ts @@ -1,21 +1,8 @@ import jsPDF from "jspdf"; import autoTable, { RowInput } from "jspdf-autotable"; -import { drawReportCoverPage } from "@/lib/reportCover"; type Association = { id?: string; name: string; logo_url?: string | null }; -/** Prepend the shared branded cover page, then move to a fresh page for content. */ -async function coverPage(doc: jsPDF, association: Association | null, title: string, subTitle: string) { - await drawReportCoverPage(doc, doc.internal.pageSize.getWidth(), doc.internal.pageSize.getHeight(), { - title, - date: subTitle, - companyName: association?.name || "All Associations", - preparedBy: "Avria Community Management, LLC", - logoUrl: association?.logo_url || undefined, - }); - doc.addPage(); -} - const fmt = (n: number | string | undefined | null) => { const v = typeof n === "string" ? parseFloat(n) : n; if (v == null || isNaN(v as number)) return "$0.00"; @@ -126,7 +113,6 @@ async function generateSectionedReportPdf(opts: { }) { const { association, reportLabel, subTitle, rows } = opts; const doc = new jsPDF({ unit: "pt", format: "letter" }); - await coverPage(doc, association, reportLabel, subTitle); await drawHeader(doc, association, reportLabel, subTitle); const body: RowInput[] = rows.map((r) => { @@ -265,7 +251,6 @@ export async function generateARAgingPdf(opts: { } const doc = new jsPDF({ unit: "pt", format: "letter", orientation: "landscape" }); - await coverPage(doc, association, "Accounts Receivable Aging", `As of ${asOfDate}`); await drawHeader(doc, association, "Accounts Receivable Aging", `As of ${asOfDate}`); const totals = { current: 0, d1_30: 0, d31_60: 0, d61_90: 0, over_90: 0, total: 0 }; @@ -392,7 +377,6 @@ export async function generateBudgetVsActualPdf(opts: { : `${fromDate} to ${toDate}`; const doc = new jsPDF({ unit: "pt", format: "letter", orientation: "landscape" }); - await coverPage(doc, association, "Budget vs Actual", subTitle); await drawHeader(doc, association, "Budget vs Actual", subTitle); if (budgets.length === 0) { diff --git a/src/pages/accounting/AccountingReportsPage.tsx b/src/pages/accounting/AccountingReportsPage.tsx index 969ab65..15045b9 100644 --- a/src/pages/accounting/AccountingReportsPage.tsx +++ b/src/pages/accounting/AccountingReportsPage.tsx @@ -18,10 +18,9 @@ import { money, fmtDate } from "./lib/format"; import jsPDF from "jspdf"; import autoTable from "jspdf-autotable"; import { - renderReportPdfWithCover, fmtAmount, + renderReportPdf, fmtAmount, type StructuredReport, type StructuredRow, } from "./lib/reportPdf"; -import { drawReportCoverPage, type ReportCoverData } from "@/lib/reportCover"; import { calcNetIncome, isRetainedEarnings, isCurrentYearEarnings, isSystemEquityAccount } from "./lib/earnings"; import { computePnL, computeMargins, toMinor, fromMinor, PnlValidationError, @@ -335,27 +334,17 @@ export default function AccountingReportsPage() { const hasOwnExport = active === "trial-balance" || active === "general-ledger" || active === "budget-vs-actuals"; const anyExportable = !!(structured || flat || exportFlat); - const doExportPDF = async () => { + const doExportPDF = () => { const fileBase = `${activeMeta.name.replace(/\s+/g, "-").toLowerCase()}-${from}-to-${to}`; const src = flat ?? exportFlat; - const cover: ReportCoverData = { - title: activeMeta.name, - date: rangeLabel, - companyName: associationName ?? "Company", - preparedBy: "Avria Community Management, LLC", - }; if (structured) { - const doc = await renderReportPdfWithCover( + const doc = renderReportPdf( structured, { companyName: associationName ?? "Company", appName: APP_NAME, rangeLabel, currency: cur, showCodes, showCompare, showZero }, - cover, ); doc.save(`${fileBase}.pdf`); } else if (src) { const doc = new jsPDF({ orientation: src.columns.length > 6 ? "landscape" : "portrait" }); - // Shared branded cover page, then the tabular report on the next page. - await drawReportCoverPage(doc, doc.internal.pageSize.getWidth(), doc.internal.pageSize.getHeight(), cover); - doc.addPage(); doc.setFont("helvetica", "bold"); doc.setFontSize(14); doc.setTextColor(33, 37, 41); doc.text(src.title, 14, 16); doc.setFont("helvetica", "bold"); doc.setFontSize(9); @@ -1184,6 +1173,30 @@ function flatToStructured(flat: Flat, title: string): StructuredReport { // ---------- Budget vs Actuals ---------- +/** Order accounts as a tree (parents first, children indented) instead of by number. */ +function orderAccountsHierarchically(accs: any[]): any[] { + const byId = new Map(accs.map((a) => [a.id, a])); + const childrenByParent = new Map(); + const roots: any[] = []; + for (const a of accs) { + if (a.parent_account_id && byId.has(a.parent_account_id)) { + const arr = childrenByParent.get(a.parent_account_id) ?? []; + arr.push(a); childrenByParent.set(a.parent_account_id, arr); + } else { + roots.push(a); + } + } + const byCode = (a: any, b: any) => String(a.code ?? "").localeCompare(String(b.code ?? "")); + roots.sort(byCode); + const out: any[] = []; + const visit = (node: any, depth: number) => { + out.push({ ...node, _depth: depth }); + for (const k of (childrenByParent.get(node.id) ?? []).sort(byCode)) visit(k, depth + 1); + }; + for (const r of roots) visit(r, 0); + return out; +} + function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabel }: { companyId: string; from: string; to: string; currency: string; companyName: string; rangeLabel: string }) { const { data: budgets = [] } = useQuery({ queryKey: ["budgets-active", companyId], @@ -1240,6 +1253,12 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe return out; }, [accounts]); + // Same accounts, ordered as a parent→child tree (each carries `_depth`). + const groupedOrdered = useMemo(() => ({ + income: orderAccountsHierarchically(grouped.income ?? []), + expense: orderAccountsHierarchically(grouped.expense ?? []), + } as Record), [grouped]); + const budgetByAcct = useMemo(() => { const m: Record = {}; for (const e of (entries as any[])) m[e.account_id] = (m[e.account_id] ?? 0) + Number(e.amount); @@ -1321,13 +1340,14 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe const ta = accs.reduce((s, a) => s + (actualByAcct[a.id] ?? 0), 0); const tv = ta - tb; rows.push({ label: t.label, budget: tb, actual: ta, variance: tv, pct: tb ? `${((tv / tb) * 100).toFixed(1)}%` : "—", group: true }); - for (const a of accs) { + for (const a of groupedOrdered[t.value] ?? []) { const b = budgetByAcct[a.id] ?? 0; const ac = actualByAcct[a.id] ?? 0; const v = ac - b; - rows.push({ label: a.code ? `${a.name} (${a.code})` : a.name, budget: b, actual: ac, variance: v, pct: b ? `${((v / b) * 100).toFixed(1)}%` : "—", group: false }); + const indent = " ".repeat(a._depth ?? 0); + rows.push({ label: `${indent}${a.code ? `${a.name} (${a.code})` : a.name}`, budget: b, actual: ac, variance: v, pct: b ? `${((v / b) * 100).toFixed(1)}%` : "—", group: false }); } } return rows; - }, [grouped, budgetByAcct, actualByAcct]); + }, [grouped, groupedOrdered, budgetByAcct, actualByAcct]); const fileBase = `budget-vs-actuals-${from}-to-${to}`; @@ -1342,12 +1362,8 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe URL.revokeObjectURL(url); }; - const exportPDF = async () => { + const exportPDF = () => { const doc = new jsPDF({ unit: "pt", format: "letter" }); - await drawReportCoverPage(doc, doc.internal.pageSize.getWidth(), doc.internal.pageSize.getHeight(), { - title: "Budget vs Actuals", date: rangeLabel, companyName, preparedBy: "Avria Community Management, LLC", - }); - doc.addPage(); doc.setFont("helvetica", "bold"); doc.setFontSize(14); doc.setTextColor(33, 37, 41); doc.text("Budget vs Actuals", 40, 50); doc.setFont("helvetica", "normal"); doc.setFontSize(9); doc.setTextColor(110, 116, 122); @@ -1437,16 +1453,17 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe {money(totalVar, currency)} {totalB ? `${totalPct.toFixed(1)}%` : "—"} - {accs.map((a: any) => { + {(groupedOrdered[t.value] ?? []).map((a: any) => { const b = budgetByAcct[a.id] ?? 0; const ac = actualByAcct[a.id] ?? 0; const v = ac - b; const pct = b ? (v / b) * 100 : 0; const fav = t.favorableWhen === "over" ? v >= 0 : v <= 0; + const depth = a._depth ?? 0; return ( - - {a.name} + + {a.name} {a.code && {a.code}} {money(b, currency)} diff --git a/src/pages/accounting/components/GeneralLedgerReport.tsx b/src/pages/accounting/components/GeneralLedgerReport.tsx index bc8c86d..2850b3c 100644 --- a/src/pages/accounting/components/GeneralLedgerReport.tsx +++ b/src/pages/accounting/components/GeneralLedgerReport.tsx @@ -15,7 +15,6 @@ import { FileDown, Download, ChevronDown, ChevronsDownUp, ChevronsUpDown, AlertT import { fmtDate } from "../lib/format"; import { cn } from "@/lib/utils"; import jsPDF from "jspdf"; -import { drawReportCoverPage } from "@/lib/reportCover"; import autoTable from "jspdf-autotable"; const TEAL: [number, number, number] = [0, 137, 123]; @@ -143,21 +142,12 @@ export function GeneralLedgerReport({ companyId, companyName }: { companyId: str const toggleAccount = (id: string) => setSelectedAccounts((s) => s.includes(id) ? s.filter((x) => x !== id) : [...s, id]); - const exportPDF = async () => { + const exportPDF = () => { const doc = new jsPDF({ unit: "pt", format: "letter" }); const W = doc.internal.pageSize.getWidth(); const H = doc.internal.pageSize.getHeight(); const ML = 54; - // Shared branded cover page, then the report on the next page. - await drawReportCoverPage(doc, W, H, { - title: "General Ledger", - date: `${fmtDate(from)} – ${fmtDate(to)}`, - companyName: companyName || "Association", - preparedBy: "Avria Community Management, LLC", - }); - doc.addPage(); - // Header doc.setFillColor(...TEAL); doc.rect(ML, 40, 44, 44, "F"); doc.setFont("times", "bold"); doc.setFontSize(20); doc.setTextColor(255); diff --git a/src/pages/accounting/components/TrialBalanceReport.tsx b/src/pages/accounting/components/TrialBalanceReport.tsx index 042ccb3..44a62e2 100644 --- a/src/pages/accounting/components/TrialBalanceReport.tsx +++ b/src/pages/accounting/components/TrialBalanceReport.tsx @@ -11,7 +11,6 @@ import { Badge } from "@/components/ui/badge"; import { CheckCircle2, AlertTriangle, FileDown, Download } from "lucide-react"; import { fmtDate } from "../lib/format"; import jsPDF from "jspdf"; -import { drawReportCoverPage } from "@/lib/reportCover"; import autoTable from "jspdf-autotable"; type Account = { @@ -92,20 +91,11 @@ export function TrialBalanceReport({ companyId, companyName }: { companyId: stri const diff = totals.debit - totals.credit; const inBalance = Math.abs(diff) < 0.005; - const exportPDF = async () => { + const exportPDF = () => { const doc = new jsPDF({ unit: "pt", format: "letter" }); const W = doc.internal.pageSize.getWidth(); const ML = 54; - // Shared branded cover page, then the report on the next page. - await drawReportCoverPage(doc, W, doc.internal.pageSize.getHeight(), { - title: "Trial Balance", - date: `As of ${fmtDate(asOf)}`, - companyName: companyName || "Association", - preparedBy: "Avria Community Management, LLC", - }); - doc.addPage(); - // Header doc.setFillColor(...TEAL); doc.rect(ML, 40, 44, 44, "F"); doc.setFont("times", "bold"); doc.setFontSize(20); doc.setTextColor(255);