mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
PDF financial reports: add Comparative / Change / Change % columns
renderReportPdf now matches the on-screen comparison: when a comparison period is active it renders Amount | Comparative | Change | Change % columns (was Previous | Amount), switches to landscape for room, and underlines each numeric column on totals. Applies to P&L, Cash Flow, Movement of Equity, and Balance Sheet exports. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -56,9 +56,16 @@ function buildReport(doc: jsPDF, report: StructuredReport, opts: RenderOpts): js
|
|||||||
const ML = 40, MR = 40;
|
const ML = 40, MR = 40;
|
||||||
const contentR = W - MR;
|
const contentR = W - MR;
|
||||||
const amtRight = contentR - 6;
|
const amtRight = contentR - 6;
|
||||||
const cmpRight = opts.showCompare ? amtRight - 108 : 0;
|
const COLW = 86; // width of each numeric column
|
||||||
const amtUnderL = amtRight - 92;
|
const UNDER = 78; // underline width under a numeric column
|
||||||
const cmpUnderL = opts.showCompare ? cmpRight - 92 : 0;
|
// Numeric column right edges. With a comparison: Amount | Comparative | Change | Change %.
|
||||||
|
const colPct = amtRight;
|
||||||
|
const colChange = amtRight - COLW;
|
||||||
|
const colCompare = amtRight - 2 * COLW;
|
||||||
|
const colAmount = opts.showCompare ? amtRight - 3 * COLW : amtRight;
|
||||||
|
const numericLeft = colAmount - UNDER; // left boundary of the numeric block (for divider)
|
||||||
|
const pctStr = (a?: number, c?: number) =>
|
||||||
|
a === undefined || c === undefined || Math.abs(c) < 0.005 ? "—" : `${(((a - c) / Math.abs(c)) * 100).toFixed(1)}%`;
|
||||||
const isBalanceSheet = /balance sheet/i.test(report.title);
|
const isBalanceSheet = /balance sheet/i.test(report.title);
|
||||||
const rightHeader = isBalanceSheet ? "Balance" : "Amount";
|
const rightHeader = isBalanceSheet ? "Balance" : "Amount";
|
||||||
let y = 0;
|
let y = 0;
|
||||||
@@ -69,13 +76,16 @@ function buildReport(doc: jsPDF, report: StructuredReport, opts: RenderOpts): js
|
|||||||
doc.rect(ML, y, contentR - ML, h, "F");
|
doc.rect(ML, y, contentR - ML, h, "F");
|
||||||
doc.setDrawColor(...BORDER); doc.setLineWidth(0.5);
|
doc.setDrawColor(...BORDER); doc.setLineWidth(0.5);
|
||||||
doc.rect(ML, y, contentR - ML, h);
|
doc.rect(ML, y, contentR - ML, h);
|
||||||
// vertical dividers before numeric columns
|
// vertical divider before the numeric block
|
||||||
if (opts.showCompare) doc.line(cmpRight - 96, y, cmpRight - 96, y + h);
|
doc.line(numericLeft - 6, y, numericLeft - 6, y + h);
|
||||||
doc.line(amtRight - 96, y, amtRight - 96, y + h);
|
doc.setFont("helvetica", "bold"); doc.setFontSize(opts.showCompare ? 8 : 9); doc.setTextColor(...TEXT);
|
||||||
doc.setFont("helvetica", "bold"); doc.setFontSize(9); doc.setTextColor(...TEXT);
|
|
||||||
doc.text("Account Name", ML + 6, y + 12);
|
doc.text("Account Name", ML + 6, y + 12);
|
||||||
if (opts.showCompare) doc.text("Previous", cmpRight, y + 12, { align: "right" });
|
doc.text(rightHeader, colAmount, y + 12, { align: "right" });
|
||||||
doc.text(rightHeader, amtRight, y + 12, { align: "right" });
|
if (opts.showCompare) {
|
||||||
|
doc.text("Comparative", colCompare, y + 12, { align: "right" });
|
||||||
|
doc.text("Change", colChange, y + 12, { align: "right" });
|
||||||
|
doc.text("Change %", colPct, y + 12, { align: "right" });
|
||||||
|
}
|
||||||
y += h;
|
y += h;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -167,8 +177,8 @@ function buildReport(doc: jsPDF, report: StructuredReport, opts: RenderOpts): js
|
|||||||
if (row.kind === "total" || row.kind === "grand") {
|
if (row.kind === "total" || row.kind === "grand") {
|
||||||
doc.setDrawColor(...RULE);
|
doc.setDrawColor(...RULE);
|
||||||
doc.setLineWidth(row.kind === "grand" ? 1.4 : 0.7);
|
doc.setLineWidth(row.kind === "grand" ? 1.4 : 0.7);
|
||||||
doc.line(amtUnderL, top + 1, amtRight, top + 1);
|
const underCols = opts.showCompare ? [colAmount, colCompare, colChange] : [colAmount];
|
||||||
if (opts.showCompare) doc.line(cmpUnderL, top + 1, cmpRight, top + 1);
|
for (const x of underCols) doc.line(x - UNDER, top + 1, x, top + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// label
|
// label
|
||||||
@@ -185,16 +195,20 @@ function buildReport(doc: jsPDF, report: StructuredReport, opts: RenderOpts): js
|
|||||||
}
|
}
|
||||||
|
|
||||||
// amounts
|
// amounts
|
||||||
doc.setFont("helvetica", bold ? "bold" : "normal"); doc.setFontSize(9);
|
const drawNum = (val: number | undefined, x: number) => {
|
||||||
if (row.amount !== undefined) {
|
if (val === undefined) return;
|
||||||
const c: RGB = row.amount < 0 ? RED : TEXT;
|
doc.setFont("helvetica", bold ? "bold" : "normal"); doc.setFontSize(9);
|
||||||
doc.setTextColor(...c);
|
doc.setTextColor(...(val < 0 ? RED : TEXT));
|
||||||
doc.text(fmtAmount(row.amount), amtRight, top + 11, { align: "right" });
|
doc.text(fmtAmount(val), x, top + 11, { align: "right" });
|
||||||
}
|
};
|
||||||
if (opts.showCompare && row.compare !== undefined) {
|
drawNum(row.amount, colAmount);
|
||||||
const c: RGB = row.compare < 0 ? RED : TEXT;
|
if (opts.showCompare) {
|
||||||
doc.setTextColor(...c);
|
drawNum(row.compare, colCompare);
|
||||||
doc.text(fmtAmount(row.compare), cmpRight, top + 11, { align: "right" });
|
if (row.amount !== undefined && row.compare !== undefined) drawNum(row.amount - row.compare, colChange);
|
||||||
|
if (row.amount !== undefined) {
|
||||||
|
doc.setFont("helvetica", bold ? "bold" : "normal"); doc.setFontSize(9); doc.setTextColor(...MUTED);
|
||||||
|
doc.text(pctStr(row.amount, row.compare), colPct, top + 11, { align: "right" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
y += h;
|
y += h;
|
||||||
@@ -229,7 +243,7 @@ function buildReport(doc: jsPDF, report: StructuredReport, opts: RenderOpts): js
|
|||||||
const amt = report.cashHighlight.amount;
|
const amt = report.cashHighlight.amount;
|
||||||
const c: RGB = amt < 0 ? RED : TEXT;
|
const c: RGB = amt < 0 ? RED : TEXT;
|
||||||
doc.setTextColor(...c);
|
doc.setTextColor(...c);
|
||||||
doc.text(fmtAmount(amt), amtRight, y + 14, { align: "right" });
|
doc.text(fmtAmount(amt), colAmount, y + 14, { align: "right" });
|
||||||
y += 28;
|
y += 28;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +253,8 @@ function buildReport(doc: jsPDF, report: StructuredReport, opts: RenderOpts): js
|
|||||||
|
|
||||||
/** Render a financial report PDF (no cover page). */
|
/** Render a financial report PDF (no cover page). */
|
||||||
export function renderReportPdf(report: StructuredReport, opts: RenderOpts): jsPDF {
|
export function renderReportPdf(report: StructuredReport, opts: RenderOpts): jsPDF {
|
||||||
const doc = new jsPDF({ unit: "pt", format: "letter" });
|
// Landscape when a comparison period is shown — room for the extra columns.
|
||||||
|
const doc = new jsPDF({ unit: "pt", format: "letter", orientation: opts.showCompare ? "landscape" : "portrait" });
|
||||||
return buildReport(doc, report, opts);
|
return buildReport(doc, report, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,7 +267,7 @@ export async function renderReportPdfWithCover(
|
|||||||
opts: RenderOpts,
|
opts: RenderOpts,
|
||||||
cover: ReportCoverData,
|
cover: ReportCoverData,
|
||||||
): Promise<jsPDF> {
|
): Promise<jsPDF> {
|
||||||
const doc = new jsPDF({ unit: "pt", format: "letter" });
|
const doc = new jsPDF({ unit: "pt", format: "letter", orientation: opts.showCompare ? "landscape" : "portrait" });
|
||||||
await drawReportCoverPage(doc, doc.internal.pageSize.getWidth(), doc.internal.pageSize.getHeight(), cover);
|
await drawReportCoverPage(doc, doc.internal.pageSize.getWidth(), doc.internal.pageSize.getHeight(), cover);
|
||||||
doc.addPage();
|
doc.addPage();
|
||||||
return buildReport(doc, report, opts);
|
return buildReport(doc, report, opts);
|
||||||
|
|||||||
Reference in New Issue
Block a user