mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
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:
@@ -15,6 +15,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { jsPDF } from "jspdf";
|
||||
import autoTable from "jspdf-autotable";
|
||||
import { drawReportCoverPage } from "@/lib/reportCover";
|
||||
import { format, subMonths } from "date-fns";
|
||||
import type { Tables } from "@/integrations/supabase/types";
|
||||
import { ensureFontsLoaded } from "@/lib/googleFontsManager";
|
||||
@@ -353,123 +354,6 @@ export default function ReportGeneratorPage() {
|
||||
return cleanText(String(val));
|
||||
};
|
||||
|
||||
/** Load an image URL as a data URL for jsPDF embedding */
|
||||
const loadImageAsDataURL = (url: string, format: "jpeg" | "png" = "jpeg"): Promise<{ dataURL: string; w: number; h: number } | null> => {
|
||||
return new Promise((resolve) => {
|
||||
if (!url) return resolve(null);
|
||||
const img = new Image();
|
||||
img.crossOrigin = "anonymous";
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (format === "jpeg") {
|
||||
// Fill white background for JPEG (no transparency)
|
||||
ctx!.fillStyle = "#ffffff";
|
||||
ctx!.fillRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
ctx?.drawImage(img, 0, 0);
|
||||
const mime = format === "png" ? "image/png" : "image/jpeg";
|
||||
resolve({ dataURL: canvas.toDataURL(mime, 0.85), w: img.width, h: img.height });
|
||||
};
|
||||
img.onerror = () => resolve(null);
|
||||
img.src = url;
|
||||
});
|
||||
};
|
||||
|
||||
/** Draw a professional cover page directly with jsPDF vectors + embedded images */
|
||||
const drawCoverPage = async (doc: jsPDF, W: number, H: number) => {
|
||||
const cx = W / 2;
|
||||
const headerH = H * 0.42;
|
||||
const pdfFont = doc.getFontList()["Open Sans"] ? "Open Sans" : "helvetica";
|
||||
|
||||
// Background image or solid dark band (no title here — title goes below)
|
||||
const bgImg = coverData.bgUrl ? await loadImageAsDataURL(coverData.bgUrl, "jpeg") : null;
|
||||
if (bgImg) {
|
||||
const imgAspect = bgImg.w / bgImg.h;
|
||||
const headerAspect = W / headerH;
|
||||
let drawW = W, drawH = headerH, drawX = 0, drawY = 0;
|
||||
if (imgAspect > headerAspect) {
|
||||
drawH = headerH;
|
||||
drawW = headerH * imgAspect;
|
||||
drawX = -(drawW - W) / 2;
|
||||
} else {
|
||||
drawW = W;
|
||||
drawH = W / imgAspect;
|
||||
drawY = -(drawH - headerH) / 2;
|
||||
}
|
||||
doc.addImage(bgImg.dataURL, "JPEG", drawX, drawY, drawW, drawH);
|
||||
} else {
|
||||
doc.setFillColor(30, 41, 59);
|
||||
doc.rect(0, 0, W, headerH, "F");
|
||||
}
|
||||
|
||||
// Blue accent line flush at bottom of cover image
|
||||
doc.setFillColor(37, 99, 235);
|
||||
doc.rect(0, headerH, W, 4, "F");
|
||||
|
||||
let y = headerH + 14;
|
||||
|
||||
const logoImg = coverData.logoUrl ? await loadImageAsDataURL(coverData.logoUrl, "png") : null;
|
||||
if (logoImg) {
|
||||
const maxLogoH = 50;
|
||||
const logoAspect = logoImg.w / logoImg.h;
|
||||
const logoH = maxLogoH;
|
||||
const logoW = logoH * logoAspect;
|
||||
doc.addImage(logoImg.dataURL, "PNG", cx - logoW / 2, y, logoW, logoH);
|
||||
y += logoH + 24;
|
||||
}
|
||||
|
||||
doc.setFont(pdfFont, "bold");
|
||||
doc.setFontSize(28);
|
||||
doc.setTextColor(15, 23, 42);
|
||||
const titleLines = doc.splitTextToSize(coverData.title.replace(/\n/g, " "), W - 100);
|
||||
doc.text(titleLines, cx, y, { align: "center", lineHeightFactor: 1.3 });
|
||||
y += titleLines.length * 32 + 8;
|
||||
|
||||
doc.setFillColor(37, 99, 235);
|
||||
doc.rect(cx - 30, y, 60, 3, "F");
|
||||
y += 24;
|
||||
|
||||
doc.setFont(pdfFont, "bold");
|
||||
doc.setFontSize(9);
|
||||
doc.setTextColor(100, 116, 139);
|
||||
doc.text("PREPARED FOR", cx, y, { align: "center" });
|
||||
y += 22;
|
||||
|
||||
doc.setFont(pdfFont, "bold");
|
||||
doc.setFontSize(22);
|
||||
doc.setTextColor(30, 41, 59);
|
||||
const compLines = doc.splitTextToSize(coverData.companyName || "Association Name", W - 120);
|
||||
doc.text(compLines, cx, y, { align: "center" });
|
||||
y += compLines.length * 26 + 20;
|
||||
|
||||
doc.setFillColor(248, 250, 252);
|
||||
doc.setDrawColor(226, 232, 240);
|
||||
const dateText = coverData.date;
|
||||
const dateW = 200;
|
||||
doc.roundedRect(cx - dateW / 2, y, dateW, 32, 3, 3, "FD");
|
||||
doc.setFont(pdfFont, "bold");
|
||||
doc.setFontSize(11);
|
||||
doc.setTextColor(15, 23, 42);
|
||||
doc.text(dateText, cx, y + 21, { align: "center" });
|
||||
|
||||
const footY = H - 60;
|
||||
doc.setDrawColor(226, 232, 240);
|
||||
doc.setLineWidth(0.5);
|
||||
doc.line(60, footY, W - 60, footY);
|
||||
doc.setFont(pdfFont, "bold");
|
||||
doc.setFontSize(8);
|
||||
doc.setTextColor(100, 116, 139);
|
||||
doc.text("PREPARED BY", cx, footY + 16, { align: "center" });
|
||||
doc.setFont(pdfFont, "bold");
|
||||
doc.setFontSize(12);
|
||||
doc.setTextColor(30, 41, 59);
|
||||
doc.text(coverData.preparedBy || "", cx, footY + 32, { align: "center" });
|
||||
|
||||
};
|
||||
|
||||
/** Only enabled entries that are modules with data, in order */
|
||||
const getOrderedActiveModules = () => {
|
||||
return reportEntries
|
||||
@@ -517,7 +401,7 @@ export default function ReportGeneratorPage() {
|
||||
};
|
||||
|
||||
// ═══ COVER PAGE ═══
|
||||
await drawCoverPage(doc, W, H);
|
||||
await drawReportCoverPage(doc, W, H, coverData);
|
||||
|
||||
const orderedEntries = getOrderedEnabledEntries();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user