mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
Fix report exports: Budget vs Actuals + branded cover everywhere
- Budget vs Actuals now exports (CSV + branded PDF); previously the page's export buttons were disabled for it. Wired into hasOwnExport with its own buttons in the report. - Apply the shared branded cover page to the remaining PDF exports so they match the main scheme: homeowner account Statement (AccountingCustomer DetailPage), Trial Balance, and General Ledger. Note: payments already render on the accounting customer ledger/statement via payments_received (phase 3+4). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -332,7 +332,7 @@ export default function AccountingReportsPage() {
|
||||
}, [active, arOpen, data, flat, structured, cur, activeMeta.name]);
|
||||
|
||||
// Reports whose export is handled internally (own PDF/CSV buttons inside the component)
|
||||
const hasOwnExport = active === "trial-balance" || active === "general-ledger";
|
||||
const hasOwnExport = active === "trial-balance" || active === "general-ledger" || active === "budget-vs-actuals";
|
||||
const anyExportable = !!(structured || flat || exportFlat);
|
||||
|
||||
const doExportPDF = async () => {
|
||||
@@ -528,7 +528,7 @@ export default function AccountingReportsPage() {
|
||||
</Card>
|
||||
</div>
|
||||
{active === "budget-vs-actuals" && (
|
||||
<BudgetVsActuals companyId={cid} from={from} to={to} currency={cur} />
|
||||
<BudgetVsActuals companyId={cid} from={from} to={to} currency={cur} companyName={associationName ?? "Company"} rangeLabel={rangeLabel} />
|
||||
)}
|
||||
{active === "trial-balance" && (
|
||||
<TrialBalanceReport companyId={cid} companyName={associationName ?? ""} />
|
||||
@@ -1184,7 +1184,7 @@ function flatToStructured(flat: Flat, title: string): StructuredReport {
|
||||
|
||||
// ---------- Budget vs Actuals ----------
|
||||
|
||||
function BudgetVsActuals({ companyId, from, to, currency }: { companyId: string; from: string; to: string; currency: string }) {
|
||||
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],
|
||||
enabled: !!companyId,
|
||||
@@ -1311,6 +1311,59 @@ function BudgetVsActuals({ companyId, from, to, currency }: { companyId: string;
|
||||
];
|
||||
}, [grouped, budgetByAcct, actualByAcct]);
|
||||
|
||||
// Flattened rows (group totals + accounts) shared by the CSV and PDF exports.
|
||||
const exportRows = useMemo(() => {
|
||||
const rows: { label: string; budget: number; actual: number; variance: number; pct: string; group: boolean }[] = [];
|
||||
for (const t of TYPES_LOCAL) {
|
||||
const accs = grouped[t.value] ?? [];
|
||||
if (!accs.length) continue;
|
||||
const tb = accs.reduce((s, a) => s + (budgetByAcct[a.id] ?? 0), 0);
|
||||
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) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}, [grouped, budgetByAcct, actualByAcct]);
|
||||
|
||||
const fileBase = `budget-vs-actuals-${from}-to-${to}`;
|
||||
|
||||
const exportCSV = () => {
|
||||
const esc = (s: any) => `"${String(s).replace(/"/g, '""')}"`;
|
||||
const lines = [["Account", "Budget", "Actual", "Variance", "Variance %"].join(",")];
|
||||
for (const r of exportRows) lines.push([esc(r.label), r.budget.toFixed(2), r.actual.toFixed(2), r.variance.toFixed(2), esc(r.pct)].join(","));
|
||||
const blob = new Blob([lines.join("\n")], { type: "text/csv;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url; a.download = `${fileBase}.csv`; a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const exportPDF = async () => {
|
||||
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);
|
||||
doc.text(`${companyName} · ${rangeLabel}`, 40, 66);
|
||||
autoTable(doc, {
|
||||
startY: 80,
|
||||
head: [["Account", "Budget", "Actual", "Variance", "Variance %"]],
|
||||
body: exportRows.map((r) => [r.label, money(r.budget, currency), money(r.actual, currency), money(r.variance, currency), r.pct]),
|
||||
styles: { font: "helvetica", fontSize: 8, textColor: [33, 37, 41], lineColor: [222, 226, 230], lineWidth: 0.1 },
|
||||
headStyles: { fillColor: [237, 239, 242], textColor: [33, 37, 41], fontStyle: "bold", lineColor: [196, 200, 205], lineWidth: 0.2 },
|
||||
columnStyles: { 1: { halign: "right" }, 2: { halign: "right" }, 3: { halign: "right" }, 4: { halign: "right" } },
|
||||
didParseCell: ({ row, cell, section }) => { if (section === "body" && exportRows[row.index]?.group) cell.styles.fontStyle = "bold"; },
|
||||
});
|
||||
doc.save(`${fileBase}.pdf`);
|
||||
};
|
||||
|
||||
if (!budgets.length) {
|
||||
return (
|
||||
<Card><CardContent className="py-12 text-center text-sm text-muted-foreground">
|
||||
@@ -1330,6 +1383,10 @@ function BudgetVsActuals({ companyId, from, to, currency }: { companyId: string;
|
||||
{budgets.map((b: any) => <SelectItem key={b.id} value={b.id}>{b.name} (FY {b.fiscal_year})</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="ml-auto flex gap-2">
|
||||
<Button variant="outline" onClick={exportCSV}><Download className="mr-1 h-4 w-4" /> CSV</Button>
|
||||
<Button onClick={exportPDF}><FileDown className="mr-1 h-4 w-4" /> PDF</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user