From 9229142e248c7b480072574cc257a816d0d1125a Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 23:55:01 +0000 Subject: [PATCH 1/5] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/cases/collections-tab.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/cases/collections-tab.tsx b/src/components/cases/collections-tab.tsx index 7331715..bf653c0 100644 --- a/src/components/cases/collections-tab.tsx +++ b/src/components/cases/collections-tab.tsx @@ -37,15 +37,19 @@ import { Calculator, ChevronRight, Download, + FileDown, GitFork, Loader2, Plus, RefreshCw, + Save, Trash2, Upload, UserPlus, Users, } from "lucide-react"; +import jsPDF from "jspdf"; +import autoTable from "jspdf-autotable"; import { toast } from "sonner"; import { BUCKETS, From be4834d7d76c687a671de697b44dd3356711d9e1 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 23:55:42 +0000 Subject: [PATCH 2/5] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/cases/collections-tab.tsx | 168 ++++++++++++++++++++++- 1 file changed, 167 insertions(+), 1 deletion(-) diff --git a/src/components/cases/collections-tab.tsx b/src/components/cases/collections-tab.tsx index bf653c0..0348806 100644 --- a/src/components/cases/collections-tab.tsx +++ b/src/components/cases/collections-tab.tsx @@ -585,12 +585,178 @@ export function CollectionDetail({ a.click(); }; + const exportPdf = () => { + const name = promptFilename(`Ledger_${ho?.last_name || "homeowner"}`, "pdf"); + if (!name) return; + const doc = new jsPDF({ orientation: "landscape", unit: "pt", format: "letter" }); + const pageWidth = doc.internal.pageSize.getWidth(); + + // Header + doc.setFont("helvetica", "bold"); + doc.setFontSize(16); + doc.text("Homeowner Account Ledger", 40, 40); + + doc.setFont("helvetica", "normal"); + doc.setFontSize(10); + const headerLines = [ + `${ho?.first_name ?? ""} ${ho?.last_name ?? ""}`.trim(), + ho?.unit_number ? `Unit ${ho.unit_number}` : null, + ho?.address || null, + ho?.email || null, + ho?.phone || null, + ].filter(Boolean) as string[]; + headerLines.forEach((line, i) => doc.text(line, 40, 60 + i * 14)); + + const rightMeta = [ + `Ledger: ${collection.name || "—"}`, + `Status: ${collection.status}`, + `Opened: ${formatDate(collection.opened_at)}`, + `Interest rate: ${effectiveRate || 0}% / yr`, + `Generated: ${formatDate(new Date().toISOString().slice(0, 10))}`, + ]; + rightMeta.forEach((line, i) => doc.text(line, pageWidth - 40, 60 + i * 14, { align: "right" })); + + const startY = 60 + Math.max(headerLines.length, rightMeta.length) * 14 + 10; + + // Summary buckets + doc.setFont("helvetica", "bold"); + doc.setFontSize(11); + doc.text(`Total Due: ${formatCurrency(Math.max(0, computed.total))}`, 40, startY); + doc.setFont("helvetica", "normal"); + doc.setFontSize(9); + const bucketLine = BUCKETS.map((b) => `${BUCKET_LABEL[b]}: ${formatCurrency(Math.max(0, computed[b]))}`).join(" "); + doc.text(bucketLine, 40, startY + 14, { maxWidth: pageWidth - 80 }); + + // Table + const body: any[] = []; + if (opening !== 0) { + body.push([ + formatDate(collection.opened_at), + "Opening balance", + "opening", + opening > 0 ? opening.toFixed(2) : "0.00", + "0.00", "0.00", "0.00", "0.00", "0.00", "0.00", "0.00", + formatCurrency(opening), + ]); + } + withRunning.forEach((e: any) => { + body.push([ + formatDate(e.entry_date), + e.description || "—", + e.account || e.transaction_type || "", + num(e.assess) ? num(e.assess).toFixed(2) : "", + num(e.late) ? num(e.late).toFixed(2) : "", + num(e.admin) ? num(e.admin).toFixed(2) : "", + num(e.legal) ? num(e.legal).toFixed(2) : "", + num(e.viol) ? num(e.viol).toFixed(2) : "", + num(e.interest) ? num(e.interest).toFixed(2) : "", + num(e.bank) ? num(e.bank).toFixed(2) : "", + num(e.payment) ? num(e.payment).toFixed(2) : "", + formatCurrency(e.runningBalance ?? 0), + ]); + }); + body.push([ + { content: "Totals", colSpan: 3, styles: { halign: "right", fontStyle: "bold" } }, + totals.assess.toFixed(2), + totals.late.toFixed(2), + totals.admin.toFixed(2), + totals.legal.toFixed(2), + totals.viol.toFixed(2), + totals.interest.toFixed(2), + totals.bank.toFixed(2), + totals.payment.toFixed(2), + formatCurrency(computed.total), + ]); + + autoTable(doc, { + startY: startY + 36, + head: [["Date", "Description", "Account", "Assess", "Late", "Admin", "Legal", "Viol", "Int", "Bank", "Payment", "Balance"]], + body, + styles: { fontSize: 8, cellPadding: 3 }, + headStyles: { fillColor: [40, 40, 40], textColor: 255 }, + columnStyles: { + 0: { cellWidth: 60 }, + 1: { cellWidth: "auto" }, + 2: { cellWidth: 55 }, + 3: { halign: "right", cellWidth: 50 }, + 4: { halign: "right", cellWidth: 45 }, + 5: { halign: "right", cellWidth: 50 }, + 6: { halign: "right", cellWidth: 50 }, + 7: { halign: "right", cellWidth: 45 }, + 8: { halign: "right", cellWidth: 45 }, + 9: { halign: "right", cellWidth: 50 }, + 10: { halign: "right", cellWidth: 55 }, + 11: { halign: "right", cellWidth: 65, fontStyle: "bold" }, + }, + didDrawPage: (data) => { + const pageCount = doc.getNumberOfPages(); + doc.setFontSize(8); + doc.setTextColor(120); + doc.text( + `Page ${data.pageNumber} of ${pageCount}`, + pageWidth - 40, + doc.internal.pageSize.getHeight() - 20, + { align: "right" }, + ); + }, + }); + + doc.save(`${name}.pdf`); + toast.success("PDF exported"); + }; + + // Force-save (re-persist all entries — useful if user wants a manual confirm) + const [saving, setSaving] = useState(false); + const saveAll = async () => { + setSaving(true); + try { + // Persist header fields + await (supabase.from("collections") as any) + .update({ + name: ledgerName || null, + interest_rate_override: rateOverride === "" ? null : parseFloat(rateOverride), + }) + .eq("id", collection.id); + // Re-persist every row's current state + const updates = entries.map((e: any) => + (supabase.from("collection_ledger_entries") as any) + .update({ + entry_date: e.entry_date, + description: e.description, + account: e.account, + transaction_type: e.transaction_type, + assess: num(e.assess), + late: num(e.late), + admin: num(e.admin), + legal: num(e.legal), + viol: num(e.viol), + interest: num(e.interest), + bank: num(e.bank), + payment: num(e.payment), + sort_order: e.sort_order, + }) + .eq("id", e.id), + ); + const results = await Promise.all(updates); + const failed = results.find((r: any) => r.error); + if (failed) { + toast.error("Some rows failed to save", { description: failed.error.message }); + } else { + toast.success("Ledger saved"); + onChange(); + } + } finally { + setSaving(false); + } + }; + const loadFromUnitLedger = async () => { // Re-pull homeowner opening balance and any other future "saved ledger" data const { data, error } = await supabase .from("homeowners").select("opening_balance").eq("id", collection.homeowner_id).single(); if (error) { toast.error(error.message); return; } - toast.success(`Unit opening balance: ${formatCurrency(Number(data.opening_balance) || 0)}`); + toast.success(`Reloaded — opening balance: ${formatCurrency(Number(data.opening_balance) || 0)}`); + await load(); onChange(); }; From a4cf7b04ae401dd1af06343ce9cd7af82fbc50de Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 23:55:55 +0000 Subject: [PATCH 3/5] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/cases/collections-tab.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/cases/collections-tab.tsx b/src/components/cases/collections-tab.tsx index 0348806..21131e1 100644 --- a/src/components/cases/collections-tab.tsx +++ b/src/components/cases/collections-tab.tsx @@ -386,6 +386,7 @@ export function CollectionDetail({ const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(true); const [importOpen, setImportOpen] = useState(false); + const [saving, setSaving] = useState(false); // Editable header fields const [ledgerName, setLedgerName] = useState(collection.name ?? ""); From c58dbad18b65c1bfe4861c211b0615c466d3e9c2 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 23:56:01 +0000 Subject: [PATCH 4/5] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/cases/collections-tab.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/cases/collections-tab.tsx b/src/components/cases/collections-tab.tsx index 21131e1..2300379 100644 --- a/src/components/cases/collections-tab.tsx +++ b/src/components/cases/collections-tab.tsx @@ -707,7 +707,6 @@ export function CollectionDetail({ }; // Force-save (re-persist all entries — useful if user wants a manual confirm) - const [saving, setSaving] = useState(false); const saveAll = async () => { setSaving(true); try { From 3195298aa116d14b59e25d68d1c15fed8a4318dd Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 23:56:19 +0000 Subject: [PATCH 5/5] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/cases/collections-tab.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/components/cases/collections-tab.tsx b/src/components/cases/collections-tab.tsx index 2300379..049d0d4 100644 --- a/src/components/cases/collections-tab.tsx +++ b/src/components/cases/collections-tab.tsx @@ -859,13 +859,27 @@ export function CollectionDetail({

Interest is calculated on the Assessment balance.

- - + + + +
+

+ Edits auto-save when you click out of a cell. Use Save Ledger to force a full re-save. +