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`);
+}