EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
page.tsx
Go to the documentation of this file.
1"use client";
2
3import React, { useEffect, useMemo, useState } from "react";
4import { apiClient } from "@/lib/client/apiClient";
5
6interface AccountFinancialRow {
7 id: number;
8 name: string;
9 balance: number;
10 statementDate?: string;
11 statementBalance?: number;
12 recentSales?: number;
13 recentPayments?: number;
14 recentAdjustments?: number;
15 recentNet?: number;
16 lastPayment?: string;
17 overdueDays?: number;
18 currentDue?: number;
19 due30?: number;
20 due60?: number;
21 due90?: number;
22 phone?: string;
23 reconDiff?: number;
24}
25
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>("");
34
35 useEffect(() => {
36 const handle = setTimeout(() => {
37 loadData();
38 }, 200);
39 return () => clearTimeout(handle);
40 // eslint-disable-next-line react-hooks/exhaustive-deps
41 }, [searchTerm, rowLimit, showZeroBalance, showRetired]);
42
43 const loadData = async () => {
44 try {
45 setLoading(true);
46 setError("");
47
48 const limitNum = Number(rowLimit) || 0;
49
50 const response = await apiClient.getAccountFinancialData({
51 limit: limitNum > 0 ? limitNum : undefined,
52 search: searchTerm.trim() || undefined,
53 showZeroBalance,
54 showRetired
55 });
56
57 if (!response.success || !response.data || !Array.isArray(response.data)) {
58 setRows([]);
59 setLoading(false);
60 return;
61 }
62
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;
69
70 const recentNet = recentSales - recentPayments - recentAdjustments;
71 const reconDiff = balance - stmtBal - recentSales + recentPayments - recentAdjustments;
72
73 return {
74 id: Number(item.f100) || 0,
75 name: item.f101 || "",
76 balance,
77 statementDate: item.f261 || "",
78 statementBalance: stmtBal,
79 recentNet,
80 recentSales,
81 recentPayments,
82 recentAdjustments,
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 || "",
90 reconDiff,
91 };
92 });
93
94 setRows(mapped);
95 setLoading(false);
96 } catch (err) {
97 console.error("Failed to load aged debtors", err);
98 setError("Failed to load aged debtors. Please try again.");
99 setLoading(false);
100 }
101 };
102
103 const totals = useMemo(() => {
104 return rows.reduce(
105 (acc, row) => {
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;
115 return acc;
116 },
117 {
118 balance: 0,
119 recentSales: 0,
120 recentPayments: 0,
121 recentAdjustments: 0,
122 recentNet: 0,
123 currentDue: 0,
124 due30: 0,
125 due60: 0,
126 due90: 0,
127 }
128 );
129 }, [rows]);
130
131 const formatCurrency = (value: number | undefined) => {
132 return new Intl.NumberFormat("en-US", {
133 style: "currency",
134 currency: "USD",
135 minimumFractionDigits: 2,
136 }).format(value || 0);
137 };
138
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", {
144 day: "2-digit",
145 month: "short",
146 year: "numeric",
147 });
148 };
149
150 return (
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">
153 <div>
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.
157 </p>
158 </div>
159 <div className="flex items-center gap-3 text-sm text-text">
160 <label className="flex items-center gap-2">
161 Row Limit
162 <input
163 type="number"
164 min={1}
165 value={rowLimit}
166 onChange={(e) => setRowLimit(e.target.value)}
167 className="w-20 px-2 py-1 border border-border rounded"
168 />
169 </label>
170 <label className="flex items-center gap-2">
171 <input
172 type="checkbox"
173 checked={showZeroBalance}
174 onChange={(e) => setShowZeroBalance(e.target.checked)}
175 className="rounded"
176 />
177 Show zero balance
178 </label>
179 <label className="flex items-center gap-2">
180 <input
181 type="checkbox"
182 checked={showRetired}
183 onChange={(e) => setShowRetired(e.target.checked)}
184 className="rounded"
185 />
186 Show retired
187 </label>
188 </div>
189 </div>
190
191 <div className="bg-surface rounded-lg shadow p-4 space-y-3">
192 <div className="flex flex-wrap gap-3 items-center">
193 <input
194 type="text"
195 value={searchTerm}
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"
199 />
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"}
204 </div>
205 </div>
206
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>
228 </tr>
229 </thead>
230 <tbody>
231 {rows.map((row, idx) => {
232 const overdueClass =
233 (row.overdueDays || 0) > 30
234 ? "bg-pink-100"
235 : (row.overdueDays || 0) > 0
236 ? "bg-yellow-100"
237 : "";
238 return (
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">
242 {row.name}
243 </td>
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>
259 </tr>
260 );
261 })}
262 </tbody>
263 <tfoot className="bg-surface-2 font-semibold text-text">
264 <tr>
265 <td className="px-3 py-2 border-t" colSpan={2}>
266 Totals
267 </td>
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" />
283 </tr>
284 </tfoot>
285 </table>
286 </div>
287 </div>
288 </div>
289 );
290}