3import React, { useEffect, useMemo, useState } from "react";
4import { apiClient } from "@/lib/client/apiClient";
5import { fieldpineApi } from "@/lib/client/fieldpineApi";
7interface AccountResult {
13interface TransactionRow {
25 currentBalance?: number;
29type TransactionFilter = "all" | "payoff" | "charge" | "adjustment";
31export default function AccountTransactionsPage() {
32 const [typeFilter, setTypeFilter] = useState<TransactionFilter>("all");
33 const [startDate, setStartDate] = useState<string>(() => {
35 d.setDate(d.getDate() - 30);
36 return d.toISOString().slice(0, 10);
38 const [endDate, setEndDate] = useState<string>(() => {
40 d.setDate(d.getDate() + 1);
41 return d.toISOString().slice(0, 10);
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);
49 const [rows, setRows] = useState<TransactionRow[]>([]);
50 const [loading, setLoading] = useState(false);
52 // Debounced account search
54 if (accountSearch.trim().length < 2) {
55 setAccountResults([]);
59 const handle = setTimeout(() => {
60 const doSearch = async () => {
61 setAccountSearchLoading(true);
63 const response = await fieldpineApi({
64 endpoint: `/buck?3=retailmax.elink.account.detail&9=f101,c,${encodeURIComponent(
69 const mapped: AccountResult[] = response.DATS.map((item: any) => ({
70 id: Number(item.f100),
72 balance: Number(item.f114) || 0,
74 setAccountResults(mapped);
77 console.error("Account search failed", err);
79 setAccountSearchLoading(false);
86 return () => clearTimeout(handle);
89 const formatCurrency = (value?: number) => {
90 if (value === undefined || value === null || Number.isNaN(value)) return "-";
91 return new Intl.NumberFormat("en-US", {
94 minimumFractionDigits: 2,
98 const loadTransactions = async () => {
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");
108 const salesUrl = `/buck?3=retailmax.elink.sale.list&8=500&10=903,904,2125,117&13=completeddt${predicates.join("")}`;
110 const [salesResponse, adjResponse] = await Promise.all([
111 fieldpineApi({ endpoint: salesUrl }),
112 typeFilter === "adjustment" || typeFilter === "all"
114 endpoint: `/buck?3=retailmax.elink.account.transactionlist&9=f106,ge,${startDate}&9=f106,lt,${endDate}${selectedAccount ? `&9=f101,=,${selectedAccount.id}` : ""}`,
116 : Promise.resolve(null),
119 const collected: TransactionRow[] = [];
121 if (salesResponse?.DATS) {
122 salesResponse.DATS.forEach((item: any) => {
123 const dt = item.f101 ? new Date(item.f101) : new Date();
125 id: `sale-${item.f100}`,
126 date: dt.toLocaleString(),
127 dateValue: dt.getTime(),
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),
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);
146 id: `adj-${idx}-${item.f101}-${item.f106}`,
147 date: dt.toLocaleString(),
148 dateValue: dt.getTime(),
150 store: item.ACCO?.[0]?.f2125,
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,
157 currentBalance: Number(item.ACCO?.[0]?.f114 ?? NaN),
163 collected.sort((a, b) => b.dateValue - a.dateValue);
166 console.error("Failed to load transactions", err);
175 // eslint-disable-next-line react-hooks/exhaustive-deps
176 }, [typeFilter, startDate, endDate, selectedAccount]);
178 const selectedLabel = useMemo(() => {
179 if (!selectedAccount) return "All accounts";
180 return `${selectedAccount.name} (Id ${selectedAccount.id})`;
181 }, [selectedAccount]);
184 <div className="p-6 max-w-[1600px] mx-auto space-y-6">
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.
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">
196 <label className="block text-sm font-medium text-text mb-1">Type</label>
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"
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>
209 <label className="block text-sm font-medium text-text mb-1">From</label>
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"
218 <label className="block text-sm font-medium text-text mb-1">To</label>
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"
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">
234 <div className="grid grid-cols-1 md:grid-cols-2 gap-3 items-end">
236 <label className="block text-sm font-medium text-text mb-1">Account Search</label>
237 <div className="flex items-center gap-2">
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"
245 {accountSearchLoading && <span className="text-xs text-muted">Searching...</span>}
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) => (
253 setSelectedAccount(acc);
254 setAccountResults([]);
255 setAccountSearch(acc.name);
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" : ""
262 <p className="text-sm font-medium text-text">{acc.name}</p>
263 <p className="text-xs text-muted">Id {acc.id}</p>
265 <div className="text-xs font-semibold text-text">{formatCurrency(acc.balance)}</div>
272 <div className="flex gap-2 justify-end">
274 onClick={loadTransactions}
275 className="px-4 py-2 bg-surface-2 text-text rounded-md text-sm font-medium hover:bg-surface-2"
281 setAccountSearch("");
282 setAccountResults([]);
283 setSelectedAccount(null);
285 className="px-4 py-2 border border-border text-text rounded-md text-sm font-medium hover:bg-surface-2"
293 <div className="bg-surface rounded-lg shadow overflow-hidden">
294 <div className="p-4 border-b border-border flex items-center justify-between">
296 <h2 className="text-xl font-bold text-text">Results</h2>
297 <p className="text-sm text-muted">{rows.length} transactions</p>
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" />
307 <div className="overflow-x-auto">
308 <table className="w-full">
309 <thead className="bg-surface-2 border-b border-border">
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>
323 <tbody className="bg-surface divide-y divide-gray-200">
324 {rows.length === 0 && !loading && (
326 <td colSpan={10} className="px-4 py-8 text-center text-muted">
327 No transactions found for the selected filters.
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>
337 <div className="text-xs text-muted">Id {row.accountId}</div>
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>