Reconciliation Checks: self-contained PDF button in the report

The page-level exporter wasn't reliably producing the Reconciliation
Checks PDF. Add a dedicated 'PDF' button inside the report card that
generates it directly from the checks (Check/Assertion/Residual/Status,
failing rows in red), independent of the toolbar export path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 00:33:07 -04:00
parent 1f6abf07ae
commit e11a48e8bf
+30 -3
View File
@@ -698,7 +698,7 @@ export default function AccountingReportsPage({ association }: { association?: {
)} )}
{active === "reconciliation" && ( {active === "reconciliation" && (
<ReportSheet title="Reconciliation Checks" companyName={associationName ?? "Company"} period={rangeLabel} logoUrl={logoUrl}> <ReportSheet title="Reconciliation Checks" companyName={associationName ?? "Company"} period={rangeLabel} logoUrl={logoUrl}>
<ReconciliationReport d={data} currency={cur} /> <ReconciliationReport d={data} currency={cur} companyName={associationName ?? "Company"} rangeLabel={rangeLabel} />
</ReportSheet> </ReportSheet>
)} )}
{isFinancial && !(active === "pnl" && pnlMonthView) && ( {isFinancial && !(active === "pnl" && pnlMonthView) && (
@@ -1294,21 +1294,48 @@ function buildReconChecks(d: any): RecCheck[] {
}); });
} }
function ReconciliationReport({ d, currency }: { d: any; currency: string }) { function ReconciliationReport({ d, currency, companyName, rangeLabel }: { d: any; currency: string; companyName?: string; rangeLabel?: string }) {
if (!d) return <div className="text-sm text-muted-foreground">Loading</div>; if (!d) return <div className="text-sm text-muted-foreground">Loading</div>;
const checks = buildReconChecks(d); const checks = buildReconChecks(d);
const ok = (r: number) => Math.abs(r) < 0.005; const ok = (r: number) => Math.abs(r) < 0.005;
const allPass = checks.every((c) => c.pass); const allPass = checks.every((c) => c.pass);
// Self-contained PDF (doesn't depend on the page-level exporter).
const downloadPdf = () => {
const doc = new jsPDF({ unit: "pt", format: "letter" });
doc.setFontSize(16); doc.setFont("helvetica", "bold");
doc.text("Reconciliation Checks", 40, 50);
doc.setFontSize(10); doc.setFont("helvetica", "normal"); doc.setTextColor(90);
if (companyName) doc.text(companyName, 40, 68);
if (rangeLabel) doc.text(rangeLabel, 40, 82);
doc.setTextColor(0);
autoTable(doc, {
startY: 96,
head: [["Check", "Assertion", "Residual", "Status"]],
body: checks.map((c) => [c.id, c.label, money(c.residual, currency), c.pass ? "Pass" : "FAIL"]),
styles: { font: "helvetica", fontSize: 9, cellPadding: 5 },
headStyles: { fillColor: [30, 41, 59], textColor: 255, fontStyle: "bold" },
columnStyles: { 0: { cellWidth: 50 }, 2: { halign: "right", cellWidth: 90 }, 3: { halign: "right", cellWidth: 60 } },
didParseCell: ({ section, row, column, cell }) => {
if (section === "body" && !checks[row.index]?.pass && (column.index === 2 || column.index === 3)) {
cell.styles.textColor = [220, 38, 38]; cell.styles.fontStyle = "bold";
}
},
margin: { left: 40, right: 40 },
});
doc.save(`reconciliation-checks-${(companyName || "company").replace(/[^a-z0-9]+/gi, "_")}.pdf`);
};
return ( return (
<Card> <Card>
<CardHeader> <CardHeader className="flex-row items-center justify-between space-y-0">
<CardTitle className="text-base flex items-center gap-2"> <CardTitle className="text-base flex items-center gap-2">
Reconciliation Checks Reconciliation Checks
<span className={`text-xs rounded px-2 py-0.5 ${allPass ? "bg-emerald-100 text-emerald-700" : "bg-red-100 text-red-700"}`}> <span className={`text-xs rounded px-2 py-0.5 ${allPass ? "bg-emerald-100 text-emerald-700" : "bg-red-100 text-red-700"}`}>
{allPass ? "All passing" : "Residuals present"} {allPass ? "All passing" : "Residuals present"}
</span> </span>
</CardTitle> </CardTitle>
<Button variant="outline" size="sm" onClick={downloadPdf}><FileDown className="mr-1 h-4 w-4" /> PDF</Button>
</CardHeader> </CardHeader>
<CardContent className="p-0"> <CardContent className="p-0">
<Table> <Table>