diff --git a/src/pages/BillApprovalsPage.tsx b/src/pages/BillApprovalsPage.tsx index c38fe0f..1be976d 100644 --- a/src/pages/BillApprovalsPage.tsx +++ b/src/pages/BillApprovalsPage.tsx @@ -5,6 +5,7 @@ import { useToast } from "@/hooks/use-toast"; import { useAuth } from "@/contexts/AuthContext"; import { UserCheck, Plus, Search, Eye, Upload, X, ArrowUpDown, Edit, Trash2, MoreHorizontal, AlertTriangle, Loader2, Bell, Printer, Sparkles, Download, FileText, Save } from "lucide-react"; import { generateCheckPDF, type CheckData } from "@/pages/accounting/lib/checkPdf"; +import { generateBillSummaryPDF } from "@/pages/accounting/lib/billSummaryPdf"; import { accounting } from "@/lib/accountingClient"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Checkbox } from "@/components/ui/checkbox"; @@ -1217,6 +1218,21 @@ export default function BillApprovalsPage({ boardAssociationIds }: { boardAssoci { setDetailBill(b); setDetailOpen(true); }}> View Details + generateBillSummaryPDF({ + association: getClientName(b), + vendor: getVendorName(b), + invoiceNumber: b.invoices?.invoice_number ?? b.invoice_number, + billDate: b.bill_date, + dueDate: b.invoices?.due_date ?? b.due_date, + amount: Number(b.amount), + account: b.chart_of_accounts ? `${b.chart_of_accounts.account_number} - ${b.chart_of_accounts.account_name}` : null, + description: b.invoices?.description ?? b.description, + status: displayStatus, + createdAt: b.created_at, + approvals: (approvalsByBill[b.id] || []).map((a: any) => ({ name: a.approver_name, status: a.status })), + })}> + Download PDF + {!isBoardView && ( <> openEdit(b)}> diff --git a/src/pages/accounting/lib/billSummaryPdf.ts b/src/pages/accounting/lib/billSummaryPdf.ts new file mode 100644 index 0000000..ad0571c --- /dev/null +++ b/src/pages/accounting/lib/billSummaryPdf.ts @@ -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`); +}