diff --git a/src/pages/BillApprovalsPage.tsx b/src/pages/BillApprovalsPage.tsx index 1be976d..a33d4ab 100644 --- a/src/pages/BillApprovalsPage.tsx +++ b/src/pages/BillApprovalsPage.tsx @@ -258,7 +258,7 @@ export default function BillApprovalsPage({ boardAssociationIds }: { boardAssoci for (let from = 0; ; from += PAGE) { let q = supabase .from("bills") - .select("*, associations(name), chart_of_accounts(account_name, account_number), invoices:source_invoice_id(id, vendor_name, invoice_number, issue_date, due_date, amount, description, raw_pdf_url)") + .select("*, associations(name), chart_of_accounts(account_name, account_number), invoices:source_invoice_id(id, vendor_name, invoice_number, issue_date, due_date, amount, description, raw_pdf_url, line_items)") .order("created_at", { ascending: false }); if (isBoardView) { q = q.in("association_id", boardAssociationIds!).in("id", assignedBillIds!); @@ -1229,6 +1229,7 @@ export default function BillApprovalsPage({ boardAssociationIds }: { boardAssoci description: b.invoices?.description ?? b.description, status: displayStatus, createdAt: b.created_at, + lineItems: Array.isArray(b.invoices?.line_items) ? b.invoices.line_items : undefined, approvals: (approvalsByBill[b.id] || []).map((a: any) => ({ name: a.approver_name, status: a.status })), })}> Download PDF diff --git a/src/pages/accounting/lib/billSummaryPdf.ts b/src/pages/accounting/lib/billSummaryPdf.ts index ad0571c..619052e 100644 --- a/src/pages/accounting/lib/billSummaryPdf.ts +++ b/src/pages/accounting/lib/billSummaryPdf.ts @@ -1,5 +1,13 @@ 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; @@ -12,6 +20,7 @@ export type BillSummaryData = { description?: string | null; status?: string | null; createdAt?: string | null; + lineItems?: BillLineItem[]; approvals?: { name: string; status: string }[]; }; @@ -29,6 +38,7 @@ 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 @@ -43,21 +53,19 @@ export function generateBillSummaryPDF(bill: BillSummaryData): void { 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; + // Vendor + let y = 98; 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; + y += 10; doc.setDrawColor(236, 240, 241); doc.setLineWidth(1); doc.line(M, y, W - M, y); - y += 26; + y += 24; - // Two-column field grid + // Field grid const col2 = M + (W - 2 * M) / 2; const field = (label: string, value: string, x: number, yy: number) => { doc.setFont("helvetica", "bold"); @@ -71,33 +79,91 @@ export function generateBillSummaryPDF(bill: BillSummaryData): void { }; field("Invoice #", bill.invoiceNumber || "—", M, y); field("Status", cap(bill.status), col2, y); - y += 44; + y += 42; field("Bill Date", fmtDate(bill.billDate), M, y); field("Due Date", fmtDate(bill.dueDate), col2, y); - y += 44; + y += 42; field("Expense Account", bill.account || "—", M, y); field("Submitted", fmtDate(bill.createdAt), col2, y); - y += 44; + y += 46; - // 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; + // ── 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 += 24; + y += 22; doc.setFont("helvetica", "bold"); doc.setFontSize(11); doc.setTextColor(44, 62, 80);