mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
Bill Approvals: download a summarized bill PDF
Add a "Download PDF" action to each bill's row menu that generates a clean one-page bill summary (vendor, amount, invoice #, bill/due dates, expense account, description, status, and board approval statuses). New generator src/pages/accounting/lib/billSummaryPdf.ts (jsPDF), styled like the other report PDFs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import jsPDF from "jspdf";
|
||||
|
||||
// One-page "summarized bill" PDF for the Bill Approvals page.
|
||||
export type BillSummaryData = {
|
||||
association: string;
|
||||
vendor: string;
|
||||
invoiceNumber?: string | null;
|
||||
billDate?: string | null;
|
||||
dueDate?: string | null;
|
||||
amount: number;
|
||||
account?: string | null;
|
||||
description?: string | null;
|
||||
status?: string | null;
|
||||
createdAt?: string | null;
|
||||
approvals?: { name: string; status: string }[];
|
||||
};
|
||||
|
||||
const money = (n: number) =>
|
||||
"$" + Number(n || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
|
||||
const fmtDate = (d?: string | null) => {
|
||||
if (!d) return "—";
|
||||
const x = new Date(d.length <= 10 ? d + "T12:00:00" : d);
|
||||
return isNaN(+x) ? "—" : x.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
|
||||
};
|
||||
|
||||
const cap = (s?: string | null) => (s ? s.charAt(0).toUpperCase() + s.slice(1) : "—");
|
||||
|
||||
export function generateBillSummaryPDF(bill: BillSummaryData): void {
|
||||
const doc = new jsPDF({ unit: "pt", format: "letter" });
|
||||
const W = doc.internal.pageSize.getWidth();
|
||||
const M = 48;
|
||||
|
||||
// Header bar
|
||||
doc.setFillColor(44, 62, 80);
|
||||
doc.rect(0, 0, W, 64, "F");
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.setFontSize(18);
|
||||
doc.text("Bill Summary", M, 38);
|
||||
doc.setFont("helvetica", "normal");
|
||||
doc.setFontSize(9);
|
||||
doc.text(bill.association || "", W - M, 28, { align: "right" });
|
||||
doc.text("Generated: " + fmtDate(new Date().toISOString()), W - M, 44, { align: "right" });
|
||||
|
||||
// Vendor + amount
|
||||
let y = 100;
|
||||
doc.setTextColor(44, 62, 80);
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.setFontSize(15);
|
||||
doc.text(bill.vendor || "Vendor", M, y);
|
||||
doc.setFontSize(20);
|
||||
doc.text(money(bill.amount), W - M, y, { align: "right" });
|
||||
y += 12;
|
||||
doc.setDrawColor(236, 240, 241);
|
||||
doc.setLineWidth(1);
|
||||
doc.line(M, y, W - M, y);
|
||||
y += 26;
|
||||
|
||||
// Two-column field grid
|
||||
const col2 = M + (W - 2 * M) / 2;
|
||||
const field = (label: string, value: string, x: number, yy: number) => {
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.setFontSize(9);
|
||||
doc.setTextColor(52, 73, 94);
|
||||
doc.text(label, x, yy);
|
||||
doc.setFont("helvetica", "normal");
|
||||
doc.setFontSize(11);
|
||||
doc.setTextColor(44, 62, 80);
|
||||
doc.text(value || "—", x, yy + 15);
|
||||
};
|
||||
field("Invoice #", bill.invoiceNumber || "—", M, y);
|
||||
field("Status", cap(bill.status), col2, y);
|
||||
y += 44;
|
||||
field("Bill Date", fmtDate(bill.billDate), M, y);
|
||||
field("Due Date", fmtDate(bill.dueDate), col2, y);
|
||||
y += 44;
|
||||
field("Expense Account", bill.account || "—", M, y);
|
||||
field("Submitted", fmtDate(bill.createdAt), col2, y);
|
||||
y += 44;
|
||||
|
||||
// Description
|
||||
if (bill.description && bill.description.trim()) {
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.setFontSize(9);
|
||||
doc.setTextColor(52, 73, 94);
|
||||
doc.text("Description / Notes", M, y);
|
||||
y += 15;
|
||||
doc.setFont("helvetica", "normal");
|
||||
doc.setFontSize(11);
|
||||
doc.setTextColor(44, 62, 80);
|
||||
const lines = doc.splitTextToSize(bill.description.trim(), W - 2 * M);
|
||||
doc.text(lines, M, y);
|
||||
y += lines.length * 14 + 12;
|
||||
}
|
||||
|
||||
// Approvals
|
||||
doc.setDrawColor(236, 240, 241);
|
||||
doc.line(M, y, W - M, y);
|
||||
y += 24;
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.setFontSize(11);
|
||||
doc.setTextColor(44, 62, 80);
|
||||
doc.text("Board Approvals", M, y);
|
||||
y += 18;
|
||||
doc.setFontSize(10);
|
||||
const approvals = bill.approvals ?? [];
|
||||
if (approvals.length) {
|
||||
for (const a of approvals) {
|
||||
const mark = a.status === "approved" ? "[Approved]"
|
||||
: a.status === "denied" || a.status === "rejected" ? "[Denied]" : "[Pending]";
|
||||
doc.setFont("helvetica", "normal");
|
||||
doc.setTextColor(44, 62, 80);
|
||||
doc.text(`${mark} ${a.name}`, M, y);
|
||||
y += 16;
|
||||
}
|
||||
} else {
|
||||
doc.setFont("helvetica", "italic");
|
||||
doc.setTextColor(120, 120, 120);
|
||||
doc.text("No approvers assigned.", M, y);
|
||||
}
|
||||
|
||||
const slug = (bill.vendor || "bill").replace(/[^a-z0-9]+/gi, "-").toLowerCase().replace(/^-|-$/g, "");
|
||||
doc.save(`bill-summary-${slug}-${(bill.billDate || "").slice(0, 10) || "draft"}.pdf`);
|
||||
}
|
||||
Reference in New Issue
Block a user