import jsPDF from "jspdf"; export type BillLineItem = { description?: string | null; name?: string | null; quantity?: number | null; unit_price?: number | null; amount?: number | null; }; // 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; lineItems?: BillLineItem[]; 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 H = doc.internal.pageSize.getHeight(); 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 let y = 98; doc.setTextColor(44, 62, 80); doc.setFont("helvetica", "bold"); doc.setFontSize(15); doc.text(bill.vendor || "Vendor", M, y); y += 10; doc.setDrawColor(236, 240, 241); doc.setLineWidth(1); doc.line(M, y, W - M, y); y += 24; // 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 += 42; field("Bill Date", fmtDate(bill.billDate), M, y); field("Due Date", fmtDate(bill.dueDate), col2, y); y += 42; field("Expense Account", bill.account || "—", M, y); field("Submitted", fmtDate(bill.createdAt), col2, y); y += 46; // ── Line items table ─────────────────────────────────────────────── const rawItems = (bill.lineItems ?? []).filter( (li) => li && (li.amount != null || (li.description ?? li.name)), ); const items: BillLineItem[] = rawItems.length ? rawItems : [{ description: bill.description || bill.account || "Bill amount", quantity: null, unit_price: null, amount: bill.amount }]; // Columns (right edges) const amtR = W - M; const unitR = amtR - 95; const qtyR = unitR - 70; const descX = M; const descW = qtyR - 70 - descX; doc.setFont("helvetica", "bold"); doc.setFontSize(11); doc.setTextColor(44, 62, 80); doc.text("Line Items", M, y); y += 14; // table header doc.setFillColor(44, 62, 80); doc.rect(M, y, W - 2 * M, 20, "F"); doc.setTextColor(255, 255, 255); doc.setFont("helvetica", "bold"); doc.setFontSize(9); doc.text("Description", descX + 6, y + 14); doc.text("Qty", qtyR, y + 14, { align: "right" }); doc.text("Unit Price", unitR, y + 14, { align: "right" }); doc.text("Amount", amtR - 4, y + 14, { align: "right" }); y += 20; doc.setFont("helvetica", "normal"); doc.setTextColor(44, 62, 80); let itemsTotal = 0; for (const li of items) { const desc = (li.description ?? li.name ?? "—").toString(); const lines = doc.splitTextToSize(desc, descW); const rowH = Math.max(18, lines.length * 12 + 6); if (y + rowH > H - 90) { doc.addPage(); y = M; } doc.setFontSize(10); doc.text(lines, descX + 6, y + 13); if (li.quantity != null) doc.text(String(li.quantity), qtyR, y + 13, { align: "right" }); if (li.unit_price != null) doc.text(money(Number(li.unit_price)), unitR, y + 13, { align: "right" }); const amt = Number(li.amount ?? 0); itemsTotal += amt; doc.text(money(amt), amtR - 4, y + 13, { align: "right" }); doc.setDrawColor(236, 240, 241); doc.setLineWidth(0.5); doc.line(M, y + rowH, W - M, y + rowH); y += rowH; } // Total row (authoritative bill amount) y += 6; doc.setFont("helvetica", "bold"); doc.setFontSize(12); doc.text("Total", unitR, y + 6, { align: "right" }); doc.text(money(bill.amount), amtR - 4, y + 6, { align: "right" }); y += 14; // Note if itemized lines don't sum to the bill total if (Math.abs(itemsTotal - Number(bill.amount)) > 0.01 && rawItems.length) { doc.setFont("helvetica", "italic"); doc.setFontSize(8); doc.setTextColor(120, 120, 120); doc.text(`(line items subtotal ${money(itemsTotal)})`, amtR - 4, y + 8, { align: "right" }); y += 12; } y += 14; // Approvals if (y > H - 110) { doc.addPage(); y = M; } doc.setDrawColor(236, 240, 241); doc.setLineWidth(1); doc.line(M, y, W - M, y); y += 22; 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`); }