Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-18 23:55:42 +00:00
co-authored by renee-png
parent 9229142e24
commit be4834d7d7
+167 -1
View File
@@ -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();
};