3import React, { useEffect, useMemo, useState } from "react";
4import { apiClient } from "@/lib/client/apiClient";
6interface AccountFinancialRow {
10 statementDate?: string;
11 statementBalance?: number;
13 recentPayments?: number;
14 recentAdjustments?: number;
26export default function AgedDebtorsPage() {
27 const [searchTerm, setSearchTerm] = useState("");
28 const [rowLimit, setRowLimit] = useState("500");
29 const [showZeroBalance, setShowZeroBalance] = useState(false);
30 const [showRetired, setShowRetired] = useState(false);
31 const [rows, setRows] = useState<AccountFinancialRow[]>([]);
32 const [loading, setLoading] = useState(false);
33 const [error, setError] = useState<string>("");
36 const handle = setTimeout(() => {
39 return () => clearTimeout(handle);
40 // eslint-disable-next-line react-hooks/exhaustive-deps
41 }, [searchTerm, rowLimit, showZeroBalance, showRetired]);
43 const loadData = async () => {
48 const limitNum = Number(rowLimit) || 0;
50 const response = await apiClient.getAccountFinancialData({
51 limit: limitNum > 0 ? limitNum : undefined,
52 search: searchTerm.trim() || undefined,
57 if (!response.success || !response.data || !Array.isArray(response.data)) {
63 const mapped: AccountFinancialRow[] = response.data.map((item: any) => {
64 const balance = Number(item.f114) || 0;
65 const stmtBal = Number(item.f260) || 0;
66 const recentSales = Number(item.f220) || 0;
67 const recentPayments = Number(item.f221) || 0;
68 const recentAdjustments = Number(item.f226) || 0;
70 const recentNet = recentSales - recentPayments - recentAdjustments;
71 const reconDiff = balance - stmtBal - recentSales + recentPayments - recentAdjustments;
74 id: Number(item.f100) || 0,
75 name: item.f101 || "",
77 statementDate: item.f261 || "",
78 statementBalance: stmtBal,
83 lastPayment: item.f228 || "",
84 overdueDays: Number(item.f229) || 0,
85 currentDue: Number(item.f232) || 0,
86 due30: Number(item.f233) || 0,
87 due60: Number(item.f234) || 0,
88 due90: Number(item.f235) || 0,
89 phone: item.f136 || "",
97 console.error("Failed to load aged debtors", err);
98 setError("Failed to load aged debtors. Please try again.");
103 const totals = useMemo(() => {
106 acc.balance += row.balance;
107 acc.recentSales += row.recentSales || 0;
108 acc.recentPayments += row.recentPayments || 0;
109 acc.recentAdjustments += row.recentAdjustments || 0;
110 acc.recentNet += (row.recentSales || 0) - (row.recentPayments || 0) + (row.recentAdjustments || 0);
111 acc.currentDue += row.currentDue || 0;
112 acc.due30 += row.due30 || 0;
113 acc.due60 += row.due60 || 0;
114 acc.due90 += row.due90 || 0;
121 recentAdjustments: 0,
131 const formatCurrency = (value: number | undefined) => {
132 return new Intl.NumberFormat("en-US", {
135 minimumFractionDigits: 2,
136 }).format(value || 0);
139 const formatDate = (value?: string) => {
140 if (!value) return "";
141 const d = new Date(value);
142 if (Number.isNaN(d.getTime())) return value;
143 return d.toLocaleDateString("en-GB", {
151 <div className="p-6 max-w-7xl mx-auto space-y-6">
152 <div className="flex items-center justify-between flex-wrap gap-3">
154 <h1 className="text-3xl font-bold text-text">Aged Debtors</h1>
155 <p className="text-sm text-muted">
156 Financial listing of customer accounts including balances and aged buckets.
159 <div className="flex items-center gap-3 text-sm text-text">
160 <label className="flex items-center gap-2">
166 onChange={(e) => setRowLimit(e.target.value)}
167 className="w-20 px-2 py-1 border border-border rounded"
170 <label className="flex items-center gap-2">
173 checked={showZeroBalance}
174 onChange={(e) => setShowZeroBalance(e.target.checked)}
179 <label className="flex items-center gap-2">
182 checked={showRetired}
183 onChange={(e) => setShowRetired(e.target.checked)}
191 <div className="bg-surface rounded-lg shadow p-4 space-y-3">
192 <div className="flex flex-wrap gap-3 items-center">
196 onChange={(e) => setSearchTerm(e.target.value)}
197 placeholder="Search by account name"
198 className="px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500 w-64"
200 {loading && <span className="text-sm text-muted">Loading...</span>}
201 {error && <span className="text-sm text-red-600">{error}</span>}
202 <div className="text-sm text-muted ml-auto">
203 Showing {rows.length} account{rows.length === 1 ? "" : "s"}
207 <div className="overflow-x-auto">
208 <table className="min-w-full text-sm border border-border">
209 <thead className="bg-surface-2">
210 <tr className="text-left text-text">
211 <th className="px-3 py-2 border-b">Id</th>
212 <th className="px-3 py-2 border-b">Name</th>
213 <th className="px-3 py-2 border-b text-right">Balance</th>
214 <th className="px-3 py-2 border-b">Statement Date</th>
215 <th className="px-3 py-2 border-b text-right">Statement Balance</th>
216 <th className="px-3 py-2 border-b text-right">Recent Net</th>
217 <th className="px-3 py-2 border-b text-right">Recent Sales</th>
218 <th className="px-3 py-2 border-b text-right">Recent Payments</th>
219 <th className="px-3 py-2 border-b text-right">Recent Adjustments</th>
220 <th className="px-3 py-2 border-b">Last Payment</th>
221 <th className="px-3 py-2 border-b text-right">Overdue (days)</th>
222 <th className="px-3 py-2 border-b text-right">Current</th>
223 <th className="px-3 py-2 border-b text-right">30 Days</th>
224 <th className="px-3 py-2 border-b text-right">60 Days</th>
225 <th className="px-3 py-2 border-b text-right">90+ Days</th>
226 <th className="px-3 py-2 border-b">Phone</th>
227 <th className="px-3 py-2 border-b text-right">Recent Recon</th>
231 {rows.map((row, idx) => {
233 (row.overdueDays || 0) > 30
235 : (row.overdueDays || 0) > 0
239 <tr key={row.id || idx} className={idx % 2 === 0 ? "bg-surface" : "bg-surface-2"}>
240 <td className="px-3 py-2 whitespace-nowrap border-b">{row.id}</td>
241 <td className="px-3 py-2 border-b">
244 <td className="px-3 py-2 border-b text-right">{formatCurrency(row.balance)}</td>
245 <td className="px-3 py-2 border-b whitespace-nowrap">{formatDate(row.statementDate)}</td>
246 <td className="px-3 py-2 border-b text-right">{formatCurrency(row.statementBalance)}</td>
247 <td className="px-3 py-2 border-b text-right">{formatCurrency(row.recentNet)}</td>
248 <td className="px-3 py-2 border-b text-right">{formatCurrency(row.recentSales)}</td>
249 <td className="px-3 py-2 border-b text-right">{formatCurrency(row.recentPayments)}</td>
250 <td className="px-3 py-2 border-b text-right">{formatCurrency(row.recentAdjustments)}</td>
251 <td className="px-3 py-2 border-b whitespace-nowrap">{formatDate(row.lastPayment)}</td>
252 <td className={`px-3 py-2 border-b text-right ${overdueClass}`}>{row.overdueDays ?? ""}</td>
253 <td className="px-3 py-2 border-b text-right">{formatCurrency(row.currentDue)}</td>
254 <td className="px-3 py-2 border-b text-right">{formatCurrency(row.due30)}</td>
255 <td className="px-3 py-2 border-b text-right">{formatCurrency(row.due60)}</td>
256 <td className="px-3 py-2 border-b text-right">{formatCurrency(row.due90)}</td>
257 <td className="px-3 py-2 border-b whitespace-nowrap">{row.phone}</td>
258 <td className="px-3 py-2 border-b text-right">{formatCurrency(row.reconDiff)}</td>
263 <tfoot className="bg-surface-2 font-semibold text-text">
265 <td className="px-3 py-2 border-t" colSpan={2}>
268 <td className="px-3 py-2 border-t text-right">{formatCurrency(totals.balance)}</td>
269 <td className="px-3 py-2 border-t" />
270 <td className="px-3 py-2 border-t" />
271 <td className="px-3 py-2 border-t text-right">{formatCurrency(totals.recentNet)}</td>
272 <td className="px-3 py-2 border-t text-right">{formatCurrency(totals.recentSales)}</td>
273 <td className="px-3 py-2 border-t text-right">{formatCurrency(totals.recentPayments)}</td>
274 <td className="px-3 py-2 border-t text-right">{formatCurrency(totals.recentAdjustments)}</td>
275 <td className="px-3 py-2 border-t" />
276 <td className="px-3 py-2 border-t" />
277 <td className="px-3 py-2 border-t text-right">{formatCurrency(totals.currentDue)}</td>
278 <td className="px-3 py-2 border-t text-right">{formatCurrency(totals.due30)}</td>
279 <td className="px-3 py-2 border-t text-right">{formatCurrency(totals.due60)}</td>
280 <td className="px-3 py-2 border-t text-right">{formatCurrency(totals.due90)}</td>
281 <td className="px-3 py-2 border-t" />
282 <td className="px-3 py-2 border-t" />