mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
Accounting statement matches main account-statement; fix payments cut off
- Homeowner "Export/Email Statement" now uses the same generateLedgerStatement layout as the main-app account statement (account holder, amounts-due breakdown, categorized columns incl. Pay (AR), no cover page) instead of the branded-cover table. - Ledger view: default the "To" date to the latest entry when it's beyond today, so a payment dated when an invoice was marked paid (updated_at) is no longer filtered out of the rows / Total Paid. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,9 +14,7 @@ import { Badge } from "@/components/ui/badge";
|
|||||||
import { ArrowLeft, Download, Mail, FileText, Pencil, Check, X } from "lucide-react";
|
import { ArrowLeft, Download, Mail, FileText, Pencil, Check, X } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { money, fmtDate } from "./lib/format";
|
import { money, fmtDate } from "./lib/format";
|
||||||
import jsPDF from "jspdf";
|
import { generateLedgerStatement } from "@/components/unit-profile/UnitLedgerStatementPDF";
|
||||||
import autoTable from "jspdf-autotable";
|
|
||||||
import { drawReportCoverPage } from "@/lib/reportCover";
|
|
||||||
|
|
||||||
function today() {
|
function today() {
|
||||||
return new Date().toLocaleDateString("en-CA", { timeZone: "America/New_York" });
|
return new Date().toLocaleDateString("en-CA", { timeZone: "America/New_York" });
|
||||||
@@ -47,6 +45,7 @@ export default function AccountingCustomerDetailPage() {
|
|||||||
const [from, setFrom] = useState("");
|
const [from, setFrom] = useState("");
|
||||||
const [fromTouched, setFromTouched] = useState(false);
|
const [fromTouched, setFromTouched] = useState(false);
|
||||||
const [to, setTo] = useState(today());
|
const [to, setTo] = useState(today());
|
||||||
|
const [toTouched, setToTouched] = useState(false);
|
||||||
const [drawer, setDrawer] = useState<LedgerRow | null>(null);
|
const [drawer, setDrawer] = useState<LedgerRow | null>(null);
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [editForm, setEditForm] = useState<any>(null);
|
const [editForm, setEditForm] = useState<any>(null);
|
||||||
@@ -147,6 +146,14 @@ export default function AccountingCustomerDetailPage() {
|
|||||||
if (!fromTouched && !from && allRows.length) setFrom(allRows[0].date);
|
if (!fromTouched && !from && allRows.length) setFrom(allRows[0].date);
|
||||||
}, [allRows, fromTouched, from]);
|
}, [allRows, fromTouched, from]);
|
||||||
|
|
||||||
|
// Extend "To" to the latest entry if it's beyond today, so recently-dated
|
||||||
|
// payments (e.g. an invoice marked paid today) aren't filtered out.
|
||||||
|
useEffect(() => {
|
||||||
|
if (toTouched || !allRows.length) return;
|
||||||
|
const latest = allRows[allRows.length - 1].date;
|
||||||
|
if (latest > to) setTo(latest);
|
||||||
|
}, [allRows, toTouched, to]);
|
||||||
|
|
||||||
const openingBalance = useMemo(
|
const openingBalance = useMemo(
|
||||||
() => allRows.filter((r) => r.date < from).reduce((s, r) => s + r.debit - r.credit, 0),
|
() => allRows.filter((r) => r.date < from).reduce((s, r) => s + r.debit - r.credit, 0),
|
||||||
[allRows, from]
|
[allRows, from]
|
||||||
@@ -242,54 +249,33 @@ export default function AccountingCustomerDetailPage() {
|
|||||||
qc.invalidateQueries({ queryKey: ["customers", cid] });
|
qc.invalidateQueries({ queryKey: ["customers", cid] });
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportStatement = async () => {
|
const exportStatement = () => {
|
||||||
if (!homeowner) return;
|
if (!homeowner) return;
|
||||||
const doc = new jsPDF({ unit: "pt", format: "letter" });
|
if (!allRows.length) { toast.error("No ledger activity to export"); return; }
|
||||||
// Shared branded cover page (matches all other report exports), then detail.
|
// Use the same account-statement layout as the main-app owner ledger
|
||||||
await drawReportCoverPage(doc, doc.internal.pageSize.getWidth(), doc.internal.pageSize.getHeight(), {
|
// (account holder, amounts-due breakdown, categorized columns, no cover).
|
||||||
title: "Homeowner Statement",
|
const entries = allRows.map((r) => ({
|
||||||
date: `${fmtDate(from)} – ${fmtDate(to)}`,
|
date: r.date,
|
||||||
companyName: associationName ?? "Association",
|
debit: r.debit,
|
||||||
preparedBy: "Avria Community Management, LLC",
|
credit: r.credit,
|
||||||
|
transaction_type: r.credit > 0 ? "payment" : (r.description || ""),
|
||||||
|
description: r.description,
|
||||||
|
}));
|
||||||
|
generateLedgerStatement({
|
||||||
|
unitData: {
|
||||||
|
unit_number: homeowner.unit_number,
|
||||||
|
address: homeowner.property_address,
|
||||||
|
account_number: homeowner.account_number,
|
||||||
|
},
|
||||||
|
owners: [{ is_primary: true, first_name: homeowner.name, last_name: "" }],
|
||||||
|
entries,
|
||||||
|
associationName: associationName ?? "Association",
|
||||||
});
|
});
|
||||||
doc.addPage();
|
|
||||||
doc.setFontSize(16);
|
|
||||||
doc.text(associationName ?? "Association", 40, 50);
|
|
||||||
doc.setFontSize(14);
|
|
||||||
doc.text("Homeowner Statement", 40, 100);
|
|
||||||
doc.setFontSize(10);
|
|
||||||
doc.text(homeowner.name, 40, 118);
|
|
||||||
if (homeowner.property_address) doc.text(`Property: ${homeowner.property_address}${homeowner.unit_number ? `, ${homeowner.unit_number}` : ""}`, 40, 132);
|
|
||||||
if (homeowner.lot_number) doc.text(`Lot: ${homeowner.lot_number}`, 40, 146);
|
|
||||||
doc.text(`Period: ${fmtDate(from)} – ${fmtDate(to)}`, 40, homeowner.lot_number ? 162 : 146);
|
|
||||||
|
|
||||||
const startY = homeowner.lot_number ? 182 : 166;
|
|
||||||
autoTable(doc, {
|
|
||||||
startY,
|
|
||||||
head: [["Date", "Type", "Ref #", "Description", "Charges", "Payments", "Balance"]],
|
|
||||||
body: [
|
|
||||||
[fmtDate(from), "", "", "Opening Balance", "", "", money(openingBalance, cur)],
|
|
||||||
...withRunning.map((r) => [
|
|
||||||
fmtDate(r.date),
|
|
||||||
r.type,
|
|
||||||
r.ref,
|
|
||||||
r.description,
|
|
||||||
r.debit ? money(r.debit, cur) : "",
|
|
||||||
r.credit ? money(r.credit, cur) : "",
|
|
||||||
money(r.running, cur),
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
foot: [["", "", "", "Current Balance Due", "", "", money(currentBalance, cur)]],
|
|
||||||
styles: { fontSize: 9 },
|
|
||||||
headStyles: { fillColor: [240, 240, 240], textColor: 20 },
|
|
||||||
footStyles: { fillColor: [240, 240, 240], textColor: 20, fontStyle: "bold" },
|
|
||||||
});
|
|
||||||
doc.save(`statement-${homeowner.name.replace(/\s+/g, "_")}.pdf`);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const emailStatement = async () => {
|
const emailStatement = () => {
|
||||||
if (!homeowner) return;
|
if (!homeowner) return;
|
||||||
await exportStatement();
|
exportStatement();
|
||||||
const subject = encodeURIComponent(`Statement from ${associationName ?? "us"}`);
|
const subject = encodeURIComponent(`Statement from ${associationName ?? "us"}`);
|
||||||
const body = encodeURIComponent(
|
const body = encodeURIComponent(
|
||||||
`Hello ${homeowner.name},\n\nPlease find attached your homeowner statement for the period ${fmtDate(from)} – ${fmtDate(to)}.\nCurrent balance due: ${money(currentBalance, cur)}.\n\nThank you.`
|
`Hello ${homeowner.name},\n\nPlease find attached your homeowner statement for the period ${fmtDate(from)} – ${fmtDate(to)}.\nCurrent balance due: ${money(currentBalance, cur)}.\n\nThank you.`
|
||||||
@@ -494,7 +480,7 @@ export default function AccountingCustomerDetailPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4 flex flex-wrap items-end gap-3">
|
<CardContent className="p-4 flex flex-wrap items-end gap-3">
|
||||||
<div><Label className="text-xs">From</Label><Input type="date" value={from} onChange={(e) => { setFromTouched(true); setFrom(e.target.value); }} className="w-40" /></div>
|
<div><Label className="text-xs">From</Label><Input type="date" value={from} onChange={(e) => { setFromTouched(true); setFrom(e.target.value); }} className="w-40" /></div>
|
||||||
<div><Label className="text-xs">To</Label><Input type="date" value={to} onChange={(e) => setTo(e.target.value)} className="w-40" /></div>
|
<div><Label className="text-xs">To</Label><Input type="date" value={to} onChange={(e) => { setToTouched(true); setTo(e.target.value); }} className="w-40" /></div>
|
||||||
<div className="ml-auto flex gap-2">
|
<div className="ml-auto flex gap-2">
|
||||||
<Button variant="outline" onClick={exportStatement}><Download className="h-4 w-4 mr-1" /> Export Statement</Button>
|
<Button variant="outline" onClick={exportStatement}><Download className="h-4 w-4 mr-1" /> Export Statement</Button>
|
||||||
<Button variant="outline" onClick={emailStatement}><Mail className="h-4 w-4 mr-1" /> Email Statement</Button>
|
<Button variant="outline" onClick={emailStatement}><Mail className="h-4 w-4 mr-1" /> Email Statement</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user