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";
5import { fieldpineApi } from "@/lib/client/fieldpineApi";
6
7interface AccountResult {
8 id: number;
9 name: string;
10 balance: number;
11}
12
13interface TransactionRow {
14 id: string;
15 date: string;
16 dateValue: number;
17 type: string;
18 store?: string;
19 saleId?: string;
20 accountId?: number;
21 accountName?: string;
22 charges?: number;
23 payments?: number;
24 netEffect?: number;
25 currentBalance?: number;
26 comments?: string;
27}
28
29type TransactionFilter = "all" | "payoff" | "charge" | "adjustment";
30
31export default function AccountTransactionsPage() {
32 const [typeFilter, setTypeFilter] = useState<TransactionFilter>("all");
33 const [startDate, setStartDate] = useState<string>(() => {
34 const d = new Date();
35 d.setDate(d.getDate() - 30);
36 return d.toISOString().slice(0, 10);
37 });
38 const [endDate, setEndDate] = useState<string>(() => {
39 const d = new Date();
40 d.setDate(d.getDate() + 1);
41 return d.toISOString().slice(0, 10);
42 });
43
44 const [accountSearch, setAccountSearch] = useState("");
45 const [accountResults, setAccountResults] = useState<AccountResult[]>([]);
46 const [accountSearchLoading, setAccountSearchLoading] = useState(false);
47 const [selectedAccount, setSelectedAccount] = useState<AccountResult | null>(null);
48
49 const [rows, setRows] = useState<TransactionRow[]>([]);
50 const [loading, setLoading] = useState(false);
51
52 // Debounced account search
53 useEffect(() => {
54 if (accountSearch.trim().length < 2) {
55 setAccountResults([]);
56 return;
57 }
58
59 const handle = setTimeout(() => {
60 const doSearch = async () => {
61 setAccountSearchLoading(true);
62 try {
63 const response = await fieldpineApi({
64 endpoint: `/buck?3=retailmax.elink.account.detail&9=f101,c,${encodeURIComponent(
65 accountSearch.trim()
66 )}&8=10`,
67 });
68 if (response?.DATS) {
69 const mapped: AccountResult[] = response.DATS.map((item: any) => ({
70 id: Number(item.f100),
71 name: item.f101,
72 balance: Number(item.f114) || 0,
73 }));
74 setAccountResults(mapped);
75 }
76 } catch (err) {
77 console.error("Account search failed", err);
78 } finally {
79 setAccountSearchLoading(false);
80 }
81 };
82
83 doSearch();
84 }, 300);
85
86 return () => clearTimeout(handle);
87 }, [accountSearch]);
88
89 const formatCurrency = (value?: number) => {
90 if (value === undefined || value === null || Number.isNaN(value)) return "-";
91 return new Intl.NumberFormat("en-US", {
92 style: "currency",
93 currency: "USD",
94 minimumFractionDigits: 2,
95 }).format(value);
96 };
97
98 const loadTransactions = async () => {
99 setLoading(true);
100 try {
101 const predicates: string[] = [];
102 if (startDate) predicates.push(`&9=f101,ge,${startDate}`);
103 if (endDate) predicates.push(`&9=f101,lt,${endDate}`);
104 if (selectedAccount) predicates.push(`&9=f903,0,${selectedAccount.id}`);
105 if (typeFilter === "payoff") predicates.push("&9=f505,0,3");
106 if (typeFilter === "charge") predicates.push("&9=f505,0,5");
107
108 const salesUrl = `/buck?3=retailmax.elink.sale.list&8=500&10=903,904,2125,117&13=completeddt${predicates.join("")}`;
109
110 const [salesResponse, adjResponse] = await Promise.all([
111 fieldpineApi({ endpoint: salesUrl }),
112 typeFilter === "adjustment" || typeFilter === "all"
113 ? fieldpineApi({
114 endpoint: `/buck?3=retailmax.elink.account.transactionlist&9=f106,ge,${startDate}&9=f106,lt,${endDate}${selectedAccount ? `&9=f101,=,${selectedAccount.id}` : ""}`,
115 })
116 : Promise.resolve(null),
117 ]);
118
119 const collected: TransactionRow[] = [];
120
121 if (salesResponse?.DATS) {
122 salesResponse.DATS.forEach((item: any) => {
123 const dt = item.f101 ? new Date(item.f101) : new Date();
124 collected.push({
125 id: `sale-${item.f100}`,
126 date: dt.toLocaleString(),
127 dateValue: dt.getTime(),
128 type: "Sale",
129 store: item.f2125,
130 saleId: item.f100?.toString(),
131 accountId: item.ACCO?.[0]?.f100,
132 accountName: item.ACCO?.[0]?.f101,
133 charges: Number(item.f909 ?? item.f904 ?? 0) || undefined,
134 payments: Number(item.f908 ?? 0) || undefined,
135 netEffect: Number(item.f904 ?? 0) || undefined,
136 currentBalance: Number(item.ACCO?.[0]?.f114 ?? NaN),
137 });
138 });
139 }
140
141 if (adjResponse?.DATS) {
142 adjResponse.DATS.forEach((item: any, idx: number) => {
143 const dt = item.f106 ? new Date(item.f106) : new Date();
144 const amt = Number(item.f103 ?? 0);
145 collected.push({
146 id: `adj-${idx}-${item.f101}-${item.f106}`,
147 date: dt.toLocaleString(),
148 dateValue: dt.getTime(),
149 type: "Adjustment",
150 store: item.ACCO?.[0]?.f2125,
151 saleId: undefined,
152 accountId: Number(item.f101) || item.ACCO?.[0]?.f100,
153 accountName: item.ACCO?.[0]?.f101,
154 charges: amt > 0 ? amt : undefined,
155 payments: amt < 0 ? Math.abs(amt) : undefined,
156 netEffect: amt,
157 currentBalance: Number(item.ACCO?.[0]?.f114 ?? NaN),
158 comments: item.f104,
159 });
160 });
161 }
162
163 collected.sort((a, b) => b.dateValue - a.dateValue);
164 setRows(collected);
165 } catch (err) {
166 console.error("Failed to load transactions", err);
167 setRows([]);
168 } finally {
169 setLoading(false);
170 }
171 };
172
173 useEffect(() => {
174 loadTransactions();
175 // eslint-disable-next-line react-hooks/exhaustive-deps
176 }, [typeFilter, startDate, endDate, selectedAccount]);
177
178 const selectedLabel = useMemo(() => {
179 if (!selectedAccount) return "All accounts";
180 return `${selectedAccount.name} (Id ${selectedAccount.id})`;
181 }, [selectedAccount]);
182
183 return (
184 <div className="p-6 max-w-[1600px] mx-auto space-y-6">
185 <div>
186 <h1 className="text-3xl font-bold text-text mb-2">Account Transactions</h1>
187 <p className="text-muted text-sm">
188 List all transactions that involve accounts. Filter by date, transaction type, store, or account.
189 </p>
190 </div>
191
192 {/* Filters */}
193 <div className="bg-surface rounded-lg shadow p-4 space-y-3">
194 <div className="grid grid-cols-1 md:grid-cols-4 gap-3">
195 <div>
196 <label className="block text-sm font-medium text-text mb-1">Type</label>
197 <select
198 value={typeFilter}
199 onChange={(e) => setTypeFilter(e.target.value as TransactionFilter)}
200 className="w-full px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
201 >
202 <option value="all">Any / All</option>
203 <option value="payoff">Paying off Account</option>
204 <option value="charge">Charged to Account</option>
205 <option value="adjustment">Adjustments</option>
206 </select>
207 </div>
208 <div>
209 <label className="block text-sm font-medium text-text mb-1">From</label>
210 <input
211 type="date"
212 value={startDate}
213 onChange={(e) => setStartDate(e.target.value)}
214 className="w-full px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
215 />
216 </div>
217 <div>
218 <label className="block text-sm font-medium text-text mb-1">To</label>
219 <input
220 type="date"
221 value={endDate}
222 onChange={(e) => setEndDate(e.target.value)}
223 className="w-full px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
224 />
225 </div>
226 <div>
227 <label className="block text-sm font-medium text-text mb-1">Selected Account</label>
228 <div className="text-sm text-text bg-surface-2 border border-border rounded-md px-3 py-2">
229 {selectedLabel}
230 </div>
231 </div>
232 </div>
233
234 <div className="grid grid-cols-1 md:grid-cols-2 gap-3 items-end">
235 <div>
236 <label className="block text-sm font-medium text-text mb-1">Account Search</label>
237 <div className="flex items-center gap-2">
238 <input
239 type="text"
240 value={accountSearch}
241 onChange={(e) => setAccountSearch(e.target.value)}
242 placeholder="Search accounts by name"
243 className="w-full px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
244 />
245 {accountSearchLoading && <span className="text-xs text-muted">Searching...</span>}
246 </div>
247 {accountResults.length > 0 && (
248 <div className="mt-2 border border-border rounded-md divide-y max-h-60 overflow-y-auto">
249 {accountResults.map((acc) => (
250 <button
251 key={acc.id}
252 onClick={() => {
253 setSelectedAccount(acc);
254 setAccountResults([]);
255 setAccountSearch(acc.name);
256 }}
257 className={`w-full text-left px-3 py-2 hover:bg-surface-2 flex items-center justify-between ${
258 selectedAccount?.id === acc.id ? "bg-green-50" : ""
259 }`}
260 >
261 <div>
262 <p className="text-sm font-medium text-text">{acc.name}</p>
263 <p className="text-xs text-muted">Id {acc.id}</p>
264 </div>
265 <div className="text-xs font-semibold text-text">{formatCurrency(acc.balance)}</div>
266 </button>
267 ))}
268 </div>
269 )}
270 </div>
271
272 <div className="flex gap-2 justify-end">
273 <button
274 onClick={loadTransactions}
275 className="px-4 py-2 bg-surface-2 text-text rounded-md text-sm font-medium hover:bg-surface-2"
276 >
277 Refresh
278 </button>
279 <button
280 onClick={() => {
281 setAccountSearch("");
282 setAccountResults([]);
283 setSelectedAccount(null);
284 }}
285 className="px-4 py-2 border border-border text-text rounded-md text-sm font-medium hover:bg-surface-2"
286 >
287 Clear Account
288 </button>
289 </div>
290 </div>
291 </div>
292
293 <div className="bg-surface rounded-lg shadow overflow-hidden">
294 <div className="p-4 border-b border-border flex items-center justify-between">
295 <div>
296 <h2 className="text-xl font-bold text-text">Results</h2>
297 <p className="text-sm text-muted">{rows.length} transactions</p>
298 </div>
299 {loading && (
300 <div className="text-sm text-muted flex items-center gap-2">
301 <span className="inline-block h-3 w-3 rounded-full border-2 border-gray-400 border-t-transparent animate-spin" />
302 Loading...
303 </div>
304 )}
305 </div>
306
307 <div className="overflow-x-auto">
308 <table className="w-full">
309 <thead className="bg-surface-2 border-b border-border">
310 <tr>
311 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase">Account</th>
312 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase">Date/Time</th>
313 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase">Type</th>
314 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase">Store</th>
315 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase">Sale/Ref</th>
316 <th className="px-4 py-3 text-right text-xs font-medium text-muted uppercase">Charges</th>
317 <th className="px-4 py-3 text-right text-xs font-medium text-muted uppercase">Payments</th>
318 <th className="px-4 py-3 text-right text-xs font-medium text-muted uppercase">Net Effect</th>
319 <th className="px-4 py-3 text-right text-xs font-medium text-muted uppercase">Current Balance</th>
320 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase">Comments</th>
321 </tr>
322 </thead>
323 <tbody className="bg-surface divide-y divide-gray-200">
324 {rows.length === 0 && !loading && (
325 <tr>
326 <td colSpan={10} className="px-4 py-8 text-center text-muted">
327 No transactions found for the selected filters.
328 </td>
329 </tr>
330 )}
331
332 {rows.map((row) => (
333 <tr key={row.id} className="hover:bg-surface-2">
334 <td className="px-4 py-3 text-sm">
335 <div className="font-medium text-text">{row.accountName ?? "-"}</div>
336 {row.accountId && (
337 <div className="text-xs text-muted">Id {row.accountId}</div>
338 )}
339 </td>
340 <td className="px-4 py-3 text-sm text-text whitespace-nowrap">{row.date}</td>
341 <td className="px-4 py-3 text-sm text-text">{row.type}</td>
342 <td className="px-4 py-3 text-sm text-text">{row.store ?? ""}</td>
343 <td className="px-4 py-3 text-sm text-text">{row.saleId ?? ""}</td>
344 <td className="px-4 py-3 text-sm text-right text-text">{formatCurrency(row.charges)}</td>
345 <td className="px-4 py-3 text-sm text-right text-text">{formatCurrency(row.payments)}</td>
346 <td className="px-4 py-3 text-sm text-right font-semibold text-text">{formatCurrency(row.netEffect)}</td>
347 <td className="px-4 py-3 text-sm text-right text-text">{formatCurrency(row.currentBalance)}</td>
348 <td className="px-4 py-3 text-sm text-text">{row.comments ?? ""}</td>
349 </tr>
350 ))}
351 </tbody>
352 </table>
353 </div>
354 </div>
355 </div>
356 );
357}