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, { useState, useEffect } from "react";
4import { apiClient } from "@/lib/client/apiClient";
5import { fieldpineApi } from "@/lib/client/fieldpineApi";
6
7interface Account {
8 f100: number; // Account ID
9 f101: string; // Name
10 f114: number; // Current Balance
11 f152?: string; // Email
12 f2001?: string; // Payment History HTML
13 LastStatementDt?: string;
14 NextStatementDt?: string;
15 BillFreqStr?: string;
16 StatementRunActive?: number;
17 PaymentOverdueDays?: number;
18 _RunStatement?: boolean;
19}
20
21export default function RunStatementsPage() {
22 const [accountsDue, setAccountsDue] = useState<Account[]>([]);
23 const [accountsSoon, setAccountsSoon] = useState<Account[]>([]);
24 const [selectedAccounts, setSelectedAccounts] = useState<number[]>([]);
25 const [loading, setLoading] = useState(true);
26 const [searchTerm, setSearchTerm] = useState("");
27 const [showAll, setShowAll] = useState(false);
28 const [showRetired, setShowRetired] = useState(false);
29 const [processing, setProcessing] = useState(false);
30
31 // Statement run options
32 const [endDate, setEndDate] = useState("");
33 const [endByFrequency, setEndByFrequency] = useState(true);
34 const [emailControl, setEmailControl] = useState("4"); // Send Immediately
35
36 useEffect(() => {
37 loadAccounts();
38 }, [showAll, showRetired]);
39
40 const loadAccounts = async () => {
41 setLoading(true);
42 try {
43 let url = "/buck?3=retailmax.elink.account.financial&9=f100,ne,0&10=112,9000&100=statementrun";
44
45 if (showRetired) url += "&9=f147,0,1";
46 if (showAll) url += "&101=1";
47
48 const response = await fieldpineApi({ endpoint: url });
49
50 if (response?.DATS) {
51 const now = new Date();
52 now.setDate(now.getDate() + 1);
53
54 const due: Account[] = [];
55 const soon: Account[] = [];
56 const autoSelected: number[] = [];
57
58 response.DATS.forEach((account: any) => {
59 const nextStatementDate = account.NextStatementDt ? new Date(account.NextStatementDt) : null;
60
61 // Filter by search term if present
62 if (searchTerm && !account.f101?.toLowerCase().includes(searchTerm.toLowerCase())) {
63 return;
64 }
65
66 if (nextStatementDate && nextStatementDate > now) {
67 soon.push(account);
68 } else {
69 // Auto-select accounts that are due and not already in progress
70 if (!account.StatementRunActive) {
71 account._RunStatement = true;
72 autoSelected.push(account.f100);
73 }
74 due.push(account);
75 }
76 });
77
78 setAccountsDue(due);
79 setAccountsSoon(soon);
80 setSelectedAccounts(autoSelected);
81 }
82 } catch (error) {
83 console.error("Failed to load accounts:", error);
84 } finally {
85 setLoading(false);
86 }
87 };
88
89 const toggleAccount = (accountId: number) => {
90 if (processing) return;
91
92 setSelectedAccounts(prev => {
93 if (prev.includes(accountId)) {
94 return prev.filter(id => id !== accountId);
95 } else {
96 return [...prev, accountId];
97 }
98 });
99 };
100
101 const selectAll = () => {
102 if (processing) return;
103 const allDueIds = accountsDue
104 .filter(acc => !acc.StatementRunActive)
105 .map(acc => acc.f100);
106 setSelectedAccounts(allDueIds);
107 };
108
109 const selectNone = () => {
110 if (processing) return;
111 setSelectedAccounts([]);
112 };
113
114 const runStatements = async () => {
115 if (!window.confirm("Do you really want to run these statements?")) {
116 return;
117 }
118
119 setProcessing(true);
120 try {
121 const payload = {
122 f8_s: "retailmax.elink.account.request",
123 f101_E: 140,
124 f100_s: selectedAccounts.join(","),
125 f300_s: endDate,
126 f301_E: endByFrequency ? 1 : 0,
127 f302_E: parseInt(emailControl),
128 };
129
130 await fetch("/DATI", {
131 method: "POST",
132 headers: {
133 "Content-Type": "application/json",
134 Accept: "application/json",
135 },
136 body: JSON.stringify(payload),
137 credentials: "include",
138 });
139
140 // Reload the page after successful submission
141 window.location.reload();
142 } catch (error) {
143 console.error("Failed to run statements:", error);
144 alert("Failed to run statements. Please try again.");
145 setProcessing(false);
146 }
147 };
148
149 const getOverdueStyle = (account: Account) => {
150 const overdueDays = account.PaymentOverdueDays || 0;
151 if (overdueDays > 30) return "bg-pink-300";
152 if (overdueDays > 0) return "bg-yellow-200";
153 return "";
154 };
155
156 const formatCurrency = (value: number) => {
157 return new Intl.NumberFormat("en-US", {
158 style: "currency",
159 currency: "USD",
160 minimumFractionDigits: 2,
161 }).format(value);
162 };
163
164 const formatDate = (dateStr?: string) => {
165 if (!dateStr) return "";
166 return new Date(dateStr).toLocaleDateString("en-US", {
167 day: "2-digit",
168 month: "short",
169 year: "numeric",
170 });
171 };
172
173 return (
174 <div className="p-6 max-w-[1600px] mx-auto">
175 {/* Header */}
176 <div className="mb-6">
177 <h1 className="text-3xl font-bold text-text mb-2">Run Account Statements</h1>
178 <p className="text-muted">
179 If you need bulk access to individual statements, these are stored in the folder
180 \fieldpine\pos\statements for direct access by external programs.
181 </p>
182 </div>
183
184 {/* Search and Filters */}
185 <div className="bg-surface rounded-lg shadow p-4 mb-6">
186 <div className="flex flex-wrap gap-4 items-center">
187 <div className="flex gap-2">
188 <input
189 type="text"
190 placeholder="Search accounts..."
191 value={searchTerm}
192 onChange={(e) => setSearchTerm(e.target.value)}
193 className="px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
194 />
195 <button
196 onClick={loadAccounts}
197 className="px-4 py-2 bg-surface-2 text-text rounded-md hover:bg-surface-2"
198 >
199 Search
200 </button>
201 </div>
202
203 <label className="flex items-center gap-2 text-sm">
204 <input
205 type="checkbox"
206 checked={showAll}
207 onChange={(e) => setShowAll(e.target.checked)}
208 className="rounded"
209 />
210 Show All Accounts
211 </label>
212
213 <label className="flex items-center gap-2 text-sm">
214 <input
215 type="checkbox"
216 checked={showRetired}
217 onChange={(e) => setShowRetired(e.target.checked)}
218 className="rounded"
219 />
220 Show retired/disabled accounts
221 </label>
222
223 {loading && (
224 <span className="text-sm text-muted">Loading from server...</span>
225 )}
226 </div>
227 </div>
228
229 {/* Statements Due Now */}
230 <div className="bg-surface rounded-lg shadow mb-6">
231 <div className="p-4 border-b border-border">
232 <h2 className="text-xl font-bold text-text mb-4">Statements Due Now</h2>
233
234 <div className="flex flex-wrap gap-3 items-center mb-4">
235 <button
236 onClick={selectAll}
237 disabled={processing}
238 className="px-4 py-2 bg-surface-2 text-text rounded-md hover:bg-surface-2 disabled:opacity-50"
239 >
240 Select All Due
241 </button>
242 <button
243 onClick={selectNone}
244 disabled={processing}
245 className="px-4 py-2 bg-surface-2 text-text rounded-md hover:bg-surface-2 disabled:opacity-50"
246 >
247 Select None
248 </button>
249 <span className="text-sm font-medium text-text">
250 Selected {selectedAccounts.length} account{selectedAccounts.length !== 1 ? "s" : ""}
251 </span>
252 <button
253 onClick={runStatements}
254 disabled={selectedAccounts.length === 0 || processing}
255 className="px-6 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:bg-surface-2 disabled:cursor-not-allowed font-semibold"
256 >
257 {processing ? "Processing..." : "Run Statements Now"}
258 </button>
259 </div>
260
261 {/* Statement Run Options */}
262 <div className="border border-border rounded-lg p-4 bg-surface-2">
263 <p className="font-bold text-text mb-3">Statement Run Options</p>
264
265 <div className="space-y-2 text-sm">
266 <div className="flex items-center gap-3">
267 <span className="text-text">Include transactions until</span>
268 <input
269 type="date"
270 value={endDate}
271 onChange={(e) => setEndDate(e.target.value)}
272 className="px-3 py-1 border border-border rounded-md"
273 />
274 <label className="flex items-center gap-2">
275 <input
276 type="checkbox"
277 checked={endByFrequency}
278 onChange={(e) => setEndByFrequency(e.target.checked)}
279 className="rounded"
280 />
281 Also stop based on statement frequency
282 </label>
283 </div>
284
285 <div className="flex items-center gap-3">
286 <span className="text-text w-20">Email</span>
287 <select
288 value={emailControl}
289 onChange={(e) => setEmailControl(e.target.value)}
290 className="px-3 py-1 border border-border rounded-md"
291 >
292 <option value="4">Send Immediately</option>
293 <option value="1">Queue, but do not send</option>
294 </select>
295 </div>
296 </div>
297 </div>
298 </div>
299
300 {/* Table */}
301 <div className="overflow-x-auto" style={{ opacity: processing ? 0.5 : 1 }}>
302 <table className="w-full">
303 <thead className="bg-surface-2 border-b border-border">
304 <tr>
305 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase">Id#</th>
306 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase">Name</th>
307 <th className="px-4 py-3 text-right text-xs font-medium text-muted uppercase">Current Balance</th>
308 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase">Last Statement</th>
309 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase">Statement Frequency</th>
310 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase">Next Statement</th>
311 <th className="px-4 py-3 text-center text-xs font-medium text-muted uppercase">Options</th>
312 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase">Email</th>
313 </tr>
314 </thead>
315 <tbody className="bg-surface divide-y divide-gray-200">
316 {accountsDue.length === 0 ? (
317 <tr>
318 <td colSpan={8} className="px-4 py-8 text-center text-muted">
319 No statements due at this time
320 </td>
321 </tr>
322 ) : (
323 accountsDue.map((account) => (
324 <tr key={account.f100} className="hover:bg-surface-2">
325 <td className="px-4 py-3 text-sm">{account.f100}</td>
326 <td className="px-4 py-3 text-sm">
327 <a
328 href={`/report/pos/customer/fieldpine/account_single_overview.htm?accid=${account.f100}`}
329 className="text-[#00543b] hover:underline font-medium"
330 >
331 {account.f101}
332 </a>
333 </td>
334 <td
335 className={`px-4 py-3 text-sm text-right font-medium ${getOverdueStyle(account)}`}
336 title={
337 account.PaymentOverdueDays
338 ? `This account is overdue ${account.PaymentOverdueDays} days`
339 : ""
340 }
341 >
342 {formatCurrency(account.f114)}
343 </td>
344 <td className="px-4 py-3 text-sm whitespace-nowrap">{formatDate(account.LastStatementDt)}</td>
345 <td className="px-4 py-3 text-sm">{account.BillFreqStr || ""}</td>
346 <td className="px-4 py-3 text-sm whitespace-nowrap">{formatDate(account.NextStatementDt)}</td>
347 <td className="px-4 py-3 text-sm text-center">
348 {account.StatementRunActive ? (
349 <span className="text-brand font-medium">In Progress</span>
350 ) : processing ? (
351 <span className="text-muted">Processing</span>
352 ) : (
353 <label className="flex items-center justify-center gap-2">
354 <input
355 type="checkbox"
356 checked={selectedAccounts.includes(account.f100)}
357 onChange={() => toggleAccount(account.f100)}
358 className="rounded"
359 />
360 Run
361 </label>
362 )}
363 </td>
364 <td className="px-4 py-3 text-sm">
365 {account.f152 && (
366 <a href={`mailto:${account.f152}`} className="text-[#00543b] hover:underline">
367 {account.f152}
368 </a>
369 )}
370 </td>
371 </tr>
372 ))
373 )}
374 </tbody>
375 </table>
376 </div>
377 </div>
378
379 {/* Statements Not Yet Due */}
380 <div className="bg-surface rounded-lg shadow">
381 <div className="p-4 border-b border-border">
382 <h2 className="text-xl font-bold text-text">Statements Not Yet Due</h2>
383 </div>
384
385 <div className="overflow-x-auto" style={{ opacity: processing ? 0.5 : 1 }}>
386 <table className="w-full">
387 <thead className="bg-surface-2 border-b border-border">
388 <tr>
389 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase">Id#</th>
390 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase">Name</th>
391 <th className="px-4 py-3 text-right text-xs font-medium text-muted uppercase">Current Balance</th>
392 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase">Last Statement</th>
393 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase">Statement Frequency</th>
394 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase">Next Statement</th>
395 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase">Email</th>
396 </tr>
397 </thead>
398 <tbody className="bg-surface divide-y divide-gray-200">
399 {accountsSoon.length === 0 ? (
400 <tr>
401 <td colSpan={7} className="px-4 py-8 text-center text-muted">
402 No upcoming statements
403 </td>
404 </tr>
405 ) : (
406 accountsSoon.map((account) => (
407 <tr key={account.f100} className="hover:bg-surface-2">
408 <td className="px-4 py-3 text-sm">{account.f100}</td>
409 <td className="px-4 py-3 text-sm">
410 <a
411 href={`/report/pos/customer/fieldpine/account_single_overview.htm?accid=${account.f100}`}
412 className="text-[#00543b] hover:underline font-medium"
413 >
414 {account.f101}
415 </a>
416 </td>
417 <td className="px-4 py-3 text-sm text-right font-medium">
418 {formatCurrency(account.f114)}
419 </td>
420 <td className="px-4 py-3 text-sm whitespace-nowrap">{formatDate(account.LastStatementDt)}</td>
421 <td className="px-4 py-3 text-sm">{account.BillFreqStr || ""}</td>
422 <td className="px-4 py-3 text-sm whitespace-nowrap">{formatDate(account.NextStatementDt)}</td>
423 <td className="px-4 py-3 text-sm">
424 {account.f152 && (
425 <a href={`mailto:${account.f152}`} className="text-[#00543b] hover:underline">
426 {account.f152}
427 </a>
428 )}
429 </td>
430 </tr>
431 ))
432 )}
433 </tbody>
434 </table>
435 </div>
436 </div>
437 </div>
438 );
439}