Unify financial report styling with branded cover page

Extract the general Report Generator's branded cover (cover image/band,
logo, title, prepared-for/by) into shared src/lib/reportCover.ts. Financial
reports now open with the same cover: platform AccountingReportsPage via new
renderReportPdfWithCover(), and the Zoho/Board financial reports
(zohoFinancialReportPdf generators). ReportGeneratorPage refactored to use
the shared module (removes duplicated cover code).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 21:49:34 -04:00
parent 3c32f8ac47
commit 8360363a15
5 changed files with 196 additions and 125 deletions
+17 -3
View File
@@ -18,9 +18,10 @@ import { money, fmtDate } from "./lib/format";
import jsPDF from "jspdf";
import autoTable from "jspdf-autotable";
import {
renderReportPdf, fmtAmount,
renderReportPdfWithCover, 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,
@@ -334,14 +335,27 @@ export default function AccountingReportsPage() {
const hasOwnExport = active === "trial-balance" || active === "general-ledger";
const anyExportable = !!(structured || flat || exportFlat);
const doExportPDF = () => {
const doExportPDF = async () => {
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 = renderReportPdf(structured, { companyName: associationName ?? "Company", appName: APP_NAME, rangeLabel, currency: cur, showCodes, showCompare, showZero });
const doc = await renderReportPdfWithCover(
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);
+28 -4
View File
@@ -1,4 +1,5 @@
import jsPDF from "jspdf";
import { drawReportCoverPage, type ReportCoverData } from "@/lib/reportCover";
export type RowKind = "section" | "group" | "sub" | "total" | "grand" | "spacer";
@@ -47,8 +48,9 @@ export function fmtAmount(n: number | undefined | null): string {
return n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
export function renderReportPdf(report: StructuredReport, opts: RenderOpts): jsPDF {
const doc = new jsPDF({ unit: "pt", format: "letter" });
/** Draw the report body onto `doc`, starting on its current page. */
function buildReport(doc: jsPDF, report: StructuredReport, opts: RenderOpts): jsPDF {
const firstPage = doc.getNumberOfPages();
const W = doc.internal.pageSize.getWidth();
const H = doc.internal.pageSize.getHeight();
const ML = 40, MR = 40;
@@ -114,11 +116,12 @@ export function renderReportPdf(report: StructuredReport, opts: RenderOpts): jsP
const drawFooter = () => {
const total = doc.getNumberOfPages();
const created = new Date().toLocaleDateString("en-US");
for (let p = 1; p <= total; p++) {
// Footer only on content pages (skip any preceding cover page).
for (let p = firstPage; p <= total; p++) {
doc.setPage(p);
doc.setFont("helvetica", "normal"); doc.setFontSize(8); doc.setTextColor(...MUTED);
doc.text(`Created on ${created}`, ML, H - 28);
doc.text(`Page ${p}`, contentR, H - 28, { align: "right" });
doc.text(`Page ${p - firstPage + 1}`, contentR, H - 28, { align: "right" });
}
};
@@ -233,3 +236,24 @@ export function renderReportPdf(report: StructuredReport, opts: RenderOpts): jsP
drawFooter();
return doc;
}
/** Render a financial report PDF (no cover page). */
export function renderReportPdf(report: StructuredReport, opts: RenderOpts): jsPDF {
const doc = new jsPDF({ unit: "pt", format: "letter" });
return buildReport(doc, report, opts);
}
/**
* Render a financial report PDF that opens with the shared branded cover page
* (same look as the general Report Generator), followed by the report body.
*/
export async function renderReportPdfWithCover(
report: StructuredReport,
opts: RenderOpts,
cover: ReportCoverData,
): Promise<jsPDF> {
const doc = new jsPDF({ unit: "pt", format: "letter" });
await drawReportCoverPage(doc, doc.internal.pageSize.getWidth(), doc.internal.pageSize.getHeight(), cover);
doc.addPage();
return buildReport(doc, report, opts);
}