3import React, { useState, useEffect } from "react";
4import { apiClient } from "@/lib/client/apiClient";
5import { fieldpineApi } from "@/lib/client/fieldpineApi";
8 f100: number; // Account ID
10 f114: number; // Current Balance
11 f152?: string; // Email
12 f2001?: string; // Payment History HTML
13 LastStatementDt?: string;
14 NextStatementDt?: string;
16 StatementRunActive?: number;
17 PaymentOverdueDays?: number;
18 _RunStatement?: boolean;
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);
31 // Statement run options
32 const [endDate, setEndDate] = useState("");
33 const [endByFrequency, setEndByFrequency] = useState(true);
34 const [emailControl, setEmailControl] = useState("4"); // Send Immediately
38 }, [showAll, showRetired]);
40 const loadAccounts = async () => {
43 let url = "/buck?3=retailmax.elink.account.financial&9=f100,ne,0&10=112,9000&100=statementrun";
45 if (showRetired) url += "&9=f147,0,1";
46 if (showAll) url += "&101=1";
48 const response = await fieldpineApi({ endpoint: url });
51 const now = new Date();
52 now.setDate(now.getDate() + 1);
54 const due: Account[] = [];
55 const soon: Account[] = [];
56 const autoSelected: number[] = [];
58 response.DATS.forEach((account: any) => {
59 const nextStatementDate = account.NextStatementDt ? new Date(account.NextStatementDt) : null;
61 // Filter by search term if present
62 if (searchTerm && !account.f101?.toLowerCase().includes(searchTerm.toLowerCase())) {
66 if (nextStatementDate && nextStatementDate > now) {
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);
79 setAccountsSoon(soon);
80 setSelectedAccounts(autoSelected);
83 console.error("Failed to load accounts:", error);
89 const toggleAccount = (accountId: number) => {
90 if (processing) return;
92 setSelectedAccounts(prev => {
93 if (prev.includes(accountId)) {
94 return prev.filter(id => id !== accountId);
96 return [...prev, accountId];
101 const selectAll = () => {
102 if (processing) return;
103 const allDueIds = accountsDue
104 .filter(acc => !acc.StatementRunActive)
105 .map(acc => acc.f100);
106 setSelectedAccounts(allDueIds);
109 const selectNone = () => {
110 if (processing) return;
111 setSelectedAccounts([]);
114 const runStatements = async () => {
115 if (!window.confirm("Do you really want to run these statements?")) {
122 f8_s: "retailmax.elink.account.request",
124 f100_s: selectedAccounts.join(","),
126 f301_E: endByFrequency ? 1 : 0,
127 f302_E: parseInt(emailControl),
130 await fetch("/DATI", {
133 "Content-Type": "application/json",
134 Accept: "application/json",
136 body: JSON.stringify(payload),
137 credentials: "include",
140 // Reload the page after successful submission
141 window.location.reload();
143 console.error("Failed to run statements:", error);
144 alert("Failed to run statements. Please try again.");
145 setProcessing(false);
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";
156 const formatCurrency = (value: number) => {
157 return new Intl.NumberFormat("en-US", {
160 minimumFractionDigits: 2,
164 const formatDate = (dateStr?: string) => {
165 if (!dateStr) return "";
166 return new Date(dateStr).toLocaleDateString("en-US", {
174 <div className="p-6 max-w-[1600px] mx-auto">
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.
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">
190 placeholder="Search accounts..."
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"
196 onClick={loadAccounts}
197 className="px-4 py-2 bg-surface-2 text-text rounded-md hover:bg-surface-2"
203 <label className="flex items-center gap-2 text-sm">
207 onChange={(e) => setShowAll(e.target.checked)}
213 <label className="flex items-center gap-2 text-sm">
216 checked={showRetired}
217 onChange={(e) => setShowRetired(e.target.checked)}
220 Show retired/disabled accounts
224 <span className="text-sm text-muted">Loading from server...</span>
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>
234 <div className="flex flex-wrap gap-3 items-center mb-4">
237 disabled={processing}
238 className="px-4 py-2 bg-surface-2 text-text rounded-md hover:bg-surface-2 disabled:opacity-50"
244 disabled={processing}
245 className="px-4 py-2 bg-surface-2 text-text rounded-md hover:bg-surface-2 disabled:opacity-50"
249 <span className="text-sm font-medium text-text">
250 Selected {selectedAccounts.length} account{selectedAccounts.length !== 1 ? "s" : ""}
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"
257 {processing ? "Processing..." : "Run Statements Now"}
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>
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>
271 onChange={(e) => setEndDate(e.target.value)}
272 className="px-3 py-1 border border-border rounded-md"
274 <label className="flex items-center gap-2">
277 checked={endByFrequency}
278 onChange={(e) => setEndByFrequency(e.target.checked)}
281 Also stop based on statement frequency
285 <div className="flex items-center gap-3">
286 <span className="text-text w-20">Email</span>
289 onChange={(e) => setEmailControl(e.target.value)}
290 className="px-3 py-1 border border-border rounded-md"
292 <option value="4">Send Immediately</option>
293 <option value="1">Queue, but do not send</option>
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">
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>
315 <tbody className="bg-surface divide-y divide-gray-200">
316 {accountsDue.length === 0 ? (
318 <td colSpan={8} className="px-4 py-8 text-center text-muted">
319 No statements due at this time
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">
328 href={`/report/pos/customer/fieldpine/account_single_overview.htm?accid=${account.f100}`}
329 className="text-[#00543b] hover:underline font-medium"
335 className={`px-4 py-3 text-sm text-right font-medium ${getOverdueStyle(account)}`}
337 account.PaymentOverdueDays
338 ? `This account is overdue ${account.PaymentOverdueDays} days`
342 {formatCurrency(account.f114)}
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>
351 <span className="text-muted">Processing</span>
353 <label className="flex items-center justify-center gap-2">
356 checked={selectedAccounts.includes(account.f100)}
357 onChange={() => toggleAccount(account.f100)}
364 <td className="px-4 py-3 text-sm">
366 <a href={`mailto:${account.f152}`} className="text-[#00543b] hover:underline">
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>
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">
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>
398 <tbody className="bg-surface divide-y divide-gray-200">
399 {accountsSoon.length === 0 ? (
401 <td colSpan={7} className="px-4 py-8 text-center text-muted">
402 No upcoming statements
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">
411 href={`/report/pos/customer/fieldpine/account_single_overview.htm?accid=${account.f100}`}
412 className="text-[#00543b] hover:underline font-medium"
417 <td className="px-4 py-3 text-sm text-right font-medium">
418 {formatCurrency(account.f114)}
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">
425 <a href={`mailto:${account.f152}`} className="text-[#00543b] hover:underline">