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, useRef } from "react";
4import { apiClient } from "@/lib/client/apiClient";
5import { Icon } from '@/contexts/IconContext';
6
7interface Account {
8 id: number;
9 name: string;
10 balance: number;
11 creditLimit: number;
12 floorLimit: number;
13}
14
15interface Sale {
16 saleId: number;
17 accountId: number;
18 accountName: string;
19 accountBalance: number;
20 date: string;
21 time: string;
22 phase: number;
23 status: string;
24 statusColor: string;
25 effect: string;
26}
27
28interface QuickStats {
29 outstanding: number;
30 outstandingPos: number;
31 outstandingNeg: number;
32 count: number;
33 countActive: number;
34 averageBalance: number;
35 costOfCredit: number;
36 top5Accounts: Array<{ name: string; balance: number }>;
37 top5Percentage: number;
38 riskAccounts: Array<{ id: number; name: string; balance: number }>;
39}
40
41interface Customer {
42 id: number;
43 name: string;
44}
45
46interface AccountListItem {
47 id: number;
48 name: string;
49 numCustomers: number;
50 balance: number;
51 email: string;
52 lastSale: string;
53 paymentHistory: string;
54 lastStatement: string;
55 receiptFormat: string;
56 externalId: string;
57 myobId: string;
58 xeroId: string;
59}
60
61export default function CustomerAccountsPage() {
62 const [quickStats, setQuickStats] = useState<QuickStats>({
63 outstanding: 0,
64 outstandingPos: 0,
65 outstandingNeg: 0,
66 count: 0,
67 countActive: 0,
68 averageBalance: 0,
69 costOfCredit: 0,
70 top5Accounts: [],
71 top5Percentage: 0,
72 riskAccounts: [],
73 });
74 const [recentSales, setRecentSales] = useState<Sale[]>([]);
75 const [loading, setLoading] = useState(true);
76 const [salesLoading, setSalesLoading] = useState(true);
77 const [showNewAccountModal, setShowNewAccountModal] = useState(false);
78 const [showUnlinkModal, setShowUnlinkModal] = useState(false);
79 const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
80 const [linkedCustomers, setLinkedCustomers] = useState<Customer[]>([]);
81 const [searchTerm, setSearchTerm] = useState("");
82 const [searchResults, setSearchResults] = useState<Account[]>([]);
83 const [showSearchResults, setShowSearchResults] = useState(false);
84 const searchTimeout = useRef<NodeJS.Timeout | null>(null);
85
86 // Account list state
87 const [accounts, setAccounts] = useState<AccountListItem[]>([]);
88 const [allAccounts, setAllAccounts] = useState<AccountListItem[]>([]);
89 const [accountsLoading, setAccountsLoading] = useState(true);
90 const [listSearchTerm, setListSearchTerm] = useState("");
91 const [showZeroBalance, setShowZeroBalance] = useState(true);
92 const [showRetired, setShowRetired] = useState(false);
93 const [accountSortField, setAccountSortField] = useState<string>("id");
94 const [accountSortDirection, setAccountSortDirection] = useState<"asc" | "desc">("asc");
95
96 const gnapBase = (process.env.NEXT_PUBLIC_FIELDPINE_BASE_URL || "").replace(/\/$/, "");
97 const excelExportUrl = `${gnapBase ? `${gnapBase}` : ""}/gnap/o/[xl,100,Accounts.xlsx]/buck?3=retailmax.elink.account.detail&9=f100,4,0`;
98
99 // New account form
100 const [newAccountForm, setNewAccountForm] = useState({
101 name: "",
102 creditLimit: "",
103 floorLimit: "",
104 goToEdit: true,
105 });
106
107 // Sort state
108 const [sortField, setSortField] = useState<string>("date");
109 const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
110
111 // Tab state
112 const [activeTab, setActiveTab] = useState<"dashboard" | "accounts" | "actions">("dashboard");
113
114 useEffect(() => {
115 loadQuickStats();
116 loadRecentSales();
117 loadAccountList();
118 }, []);
119
120 useEffect(() => {
121 loadAccountList();
122 }, [showZeroBalance, showRetired]);
123
124 // Account search
125 useEffect(() => {
126 if (searchTimeout.current) {
127 clearTimeout(searchTimeout.current);
128 }
129
130 if (searchTerm.length >= 2) {
131 searchTimeout.current = setTimeout(() => {
132 searchAccounts();
133 }, 300);
134 } else {
135 setSearchResults([]);
136 setShowSearchResults(false);
137 setSelectedAccount(null);
138 }
139
140 return () => {
141 if (searchTimeout.current) {
142 clearTimeout(searchTimeout.current);
143 }
144 };
145 }, [searchTerm]);
146
147 const searchAccounts = async () => {
148 try {
149 const result = await apiClient.getAccounts({ type: 'search', search: searchTerm });
150 const results: Account[] = [];
151 if (result.success && result.data) {
152 result.data.forEach((item: any) => {
153 results.push({
154 id: item.f100 || 0,
155 name: item.f101 || "",
156 balance: item.f114 || 0,
157 creditLimit: item.f106 || 0,
158 floorLimit: item.f107 || 0,
159 });
160 });
161 }
162 setSearchResults(results);
163 setShowSearchResults(true);
164 } catch (error) {
165 console.error("Error searching accounts:", error);
166 }
167 };
168
169 const selectAccount = (account: Account) => {
170 setSelectedAccount(account);
171 setSearchTerm(account.name);
172 setShowSearchResults(false);
173 };
174
175 const loadQuickStats = async () => {
176 try {
177 const result = await apiClient.getAccounts({ type: 'summary' });
178 const response = result.success && result.data && result.data[0] ? result.data[0] : {};
179
180 const top5: Array<{ name: string; balance: number }> = [];
181 for (let i = 0; i < 5; i++) {
182 const name = response[`f${120 + i * 10 + 2}`] || "";
183 const balance = response[`f${120 + i * 10}`] || 0;
184 if (name) {
185 top5.push({ name, balance });
186 }
187 }
188
189 const riskAccounts: Array<{ id: number; name: string; balance: number }> =
190 [];
191 for (let i = 0; i < 5; i++) {
192 const id = response[`f${600 + i * 10}`] || 0;
193 const name = response[`f${600 + i * 10 + 2}`] || "";
194 const balance = response[`f${600 + i * 10 + 3}`] || 0;
195 if (id && name) {
196 riskAccounts.push({ id, name, balance });
197 }
198 }
199
200 const outstanding = response.f111 || 0;
201 const outstandingPos = response.f112 || 0;
202 const outstandingNeg = response.f114 || 0;
203 const count = response.f110 || 0;
204 const countActive = (response.f110 || 0) - (response.f116 || 0);
205 const avgBal = response.f112 || 0;
206 const avgCount = response.f113 || 1;
207
208 const top5Total = top5.reduce((sum, acc) => sum + acc.balance, 0);
209 const top5Pct =
210 outstanding === 0 ? 0 : Math.floor((top5Total * 100) / outstanding);
211
212 setQuickStats({
213 outstanding,
214 outstandingPos,
215 outstandingNeg,
216 count,
217 countActive,
218 averageBalance: avgBal / avgCount,
219 costOfCredit: (outstanding * 0.1) / 12,
220 top5Accounts: top5,
221 top5Percentage: top5Pct,
222 riskAccounts,
223 });
224 setLoading(false);
225 } catch (error) {
226 console.error("Error loading quick stats:", error);
227 setLoading(false);
228 }
229 };
230
231 const loadRecentSales = async () => {
232 try {
233 const result = await apiClient.getRecentAccountSales();
234
235 const sales: Sale[] = [];
236 if (result.success && result.data) {
237 result.data.forEach((item: any) => {
238 const dateStr = item.f101 || "";
239 const date = new Date(dateStr);
240 const dateFormatted = date.toLocaleDateString("en-GB", {
241 day: "2-digit",
242 month: "short",
243 year: "numeric",
244 });
245 const timeFormatted = date.toLocaleTimeString("en-US", {
246 hour: "2-digit",
247 minute: "2-digit",
248 hour12: false,
249 });
250
251 const phase = item.f108 || 0;
252 let status = "Normal";
253 let statusColor = "";
254 if (phase === 0) {
255 status = "Still Active";
256 statusColor = "bg-info";
257 } else if (phase === 200) {
258 status = "Picking";
259 statusColor = "bg-warn";
260 } else if (phase === 10000 || phase === 10002) {
261 status = "Void";
262 statusColor = "bg-brand/30";
263 } else if (phase !== 1) {
264 status = String(phase);
265 statusColor = "bg-info";
266 }
267
268 sales.push({
269 saleId: item.f100 || 0,
270 accountId: item.ACCO?.[0]?.f100 || 0,
271 accountName: item.ACCO?.[0]?.f101 || "",
272 accountBalance: item.ACCO?.[0]?.f114 || 0,
273 date: dateFormatted,
274 time: timeFormatted,
275 phase,
276 status,
277 statusColor,
278 effect: item.f904 || "",
279 });
280 });
281 }
282
283 setRecentSales(sales);
284 setSalesLoading(false);
285 } catch (error) {
286 console.error("Error loading recent sales:", error);
287 setSalesLoading(false);
288 }
289 };
290
291 const loadAccountList = async () => {
292 try {
293 setAccountsLoading(true);
294 let url = "/buck?3=retailmax.elink.account.detail&9=f100,4,0&10=1003,184,1100,1105,1107";
295
296 if (!showZeroBalance) {
297 url += "&9=f114,5,0";
298 }
299 if (showRetired) {
300 url += "&9=f147,0,1";
301 }
302
303 const result = await apiClient.getAccounts({ type: 'list' });
304
305 const accountList: AccountListItem[] = [];
306 if (result.success && result.data) {
307 result.data.forEach((item: any) => {
308 accountList.push({
309 id: item.f100 || 0,
310 name: item.f101 || "",
311 numCustomers: item.f1000 || 0,
312 balance: item.f114 || 0,
313 email: item.f152 || "",
314 lastSale: item.f1100 || "",
315 paymentHistory: item.f1105 || "",
316 lastStatement: item.f1107 || "",
317 receiptFormat: item.f111 || "",
318 externalId: item.f183 || "",
319 myobId: item.MYOB?.[0]?.f110 || "",
320 xeroId: item.XERO?.[0]?.f110 || "",
321 });
322 });
323 }
324
325 setAllAccounts(accountList);
326 setAccounts(accountList);
327 setAccountsLoading(false);
328 } catch (error) {
329 console.error("Error loading account list:", error);
330 setAccountsLoading(false);
331 }
332 };
333
334 const handleListSearch = () => {
335 if (!listSearchTerm) {
336 setAccounts(allAccounts);
337 return;
338 }
339
340 const searchLower = listSearchTerm.toLowerCase();
341 const filtered = allAccounts.filter((acc) => {
342 return (
343 acc.name.toLowerCase().includes(searchLower) ||
344 String(acc.id).includes(searchLower) ||
345 acc.email.toLowerCase().includes(searchLower)
346 );
347 });
348 setAccounts(filtered);
349 };
350
351 useEffect(() => {
352 handleListSearch();
353 }, [listSearchTerm, allAccounts]);
354
355 const handleAccountSort = (field: string) => {
356 if (accountSortField === field) {
357 setAccountSortDirection(accountSortDirection === "asc" ? "desc" : "asc");
358 } else {
359 setAccountSortField(field);
360 setAccountSortDirection("asc");
361 }
362 };
363
364 const handleSort = (field: string) => {
365 if (sortField === field) {
366 setSortDirection(sortDirection === "asc" ? "desc" : "asc");
367 } else {
368 setSortField(field);
369 setSortDirection("asc");
370 }
371 };
372
373 const sortedSales = [...recentSales].sort((a, b) => {
374 let aVal: any = a[sortField as keyof Sale];
375 let bVal: any = b[sortField as keyof Sale];
376
377 if (sortField === "date") {
378 aVal = `${a.date} ${a.time}`;
379 bVal = `${b.date} ${b.time}`;
380 }
381
382 if (typeof aVal === "string") {
383 return sortDirection === "asc"
384 ? aVal.localeCompare(bVal)
385 : bVal.localeCompare(aVal);
386 }
387 return sortDirection === "asc" ? aVal - bVal : bVal - aVal;
388 });
389
390 const sortedAccounts = [...accounts].sort((a, b) => {
391 let aVal: any = a[accountSortField as keyof AccountListItem];
392 let bVal: any = b[accountSortField as keyof AccountListItem];
393
394 if (typeof aVal === "string") {
395 return accountSortDirection === "asc"
396 ? aVal.localeCompare(bVal)
397 : bVal.localeCompare(aVal);
398 }
399 return accountSortDirection === "asc" ? aVal - bVal : bVal - aVal;
400 });
401
402 const handleNewAccount = () => {
403 setNewAccountForm({
404 name: "",
405 creditLimit: "",
406 floorLimit: "",
407 goToEdit: true,
408 });
409 setShowNewAccountModal(true);
410 };
411
412 const saveNewAccount = async () => {
413 try {
414 let xml = "<DATI><f8_s>retailmax.elink.account.edit</f8_s>";
415 xml += "<f11_B>I</f11_B>";
416 xml += `<f101_s>${newAccountForm.name.replace(/[<>&'"]/g, (c) => {
417 return {
418 "<": "&lt;",
419 ">": "&gt;",
420 "&": "&amp;",
421 "'": "&apos;",
422 '"': "&quot;",
423 }[c] as string;
424 })}</f101_s>`;
425 if (newAccountForm.creditLimit) {
426 xml += `<f106_s>${newAccountForm.creditLimit}</f106_s>`;
427 }
428 if (newAccountForm.floorLimit) {
429 xml += `<f107_s>${newAccountForm.floorLimit}</f107_s>`;
430 }
431 xml += "</DATI>";
432
433 const response = await fetch("/DATI", {
434 method: "POST",
435 headers: { "Content-Type": "application/xml" },
436 body: xml,
437 });
438
439 if (response.ok) {
440 const result = await response.json();
441 const newAccId =
442 result.APPD?.[0]?.f100 || result.DATS?.[0]?.f100 || 0;
443
444 setShowNewAccountModal(false);
445 loadQuickStats();
446
447 if (newAccountForm.goToEdit && newAccId !== 0) {
448 window.location.href = `/report/pos/customer/fieldpine/account_edit.htm?accid=${newAccId}`;
449 }
450 }
451 } catch (error) {
452 console.error("Error creating account:", error);
453 alert("Failed to create account");
454 }
455 };
456
457 const handleUnlinkCustomers = async () => {
458 if (!selectedAccount) return;
459
460 try {
461 const result = await apiClient.getAccountCustomers(selectedAccount.id);
462
463 const customers: Customer[] = [];
464 if (result.success && result.data) {
465 result.data.forEach((item: any) => {
466 if (item.f100 && item.f100 !== 0) {
467 customers.push({
468 id: item.f100,
469 name: item.f101 || "",
470 });
471 }
472 });
473 }
474
475 setLinkedCustomers(customers);
476 setShowUnlinkModal(true);
477 } catch (error) {
478 console.error("Error loading customers:", error);
479 alert("Failed to load linked customers");
480 }
481 };
482
483 const unlinkCustomer = async (customerId: number) => {
484 try {
485 let xml = "<DATI><f8_s>retailmax.elink.customers.edit</f8_s>";
486 xml += "<f11_B>E</f11_B>";
487 xml += `<f100_E>${customerId}</f100_E>`;
488 xml += "<f132_E>0</f132_E>";
489 xml += "</DATI>";
490
491 const response = await fetch("/DATI", {
492 method: "POST",
493 headers: { "Content-Type": "application/xml" },
494 body: xml,
495 });
496
497 if (response.ok) {
498 setLinkedCustomers(
499 linkedCustomers.filter((c) => c.id !== customerId)
500 );
501 if (linkedCustomers.length <= 1) {
502 setShowUnlinkModal(false);
503 }
504 }
505 } catch (error) {
506 console.error("Error unlinking customer:", error);
507 alert("Failed to unlink customer");
508 }
509 };
510
511 const formatCurrency = (value: number) => {
512 return new Intl.NumberFormat("en-US", {
513 style: "currency",
514 currency: "USD",
515 minimumFractionDigits: 0,
516 maximumFractionDigits: 0,
517 }).format(value);
518 };
519
520 const getTop5Color = () => {
521 if (quickStats.top5Percentage > 70) return "bg-danger text-surface";
522 if (quickStats.top5Percentage > 20) return "bg-warn text-surface";
523 return "";
524 };
525
526 return (
527 <div className="p-6 bg-surface-2 min-h-screen">
528 {/* Header */}
529 <div className="mb-6">
530 <h1 className="text-3xl font-bold text-text mb-2 flex items-center gap-2">
531 <Icon name="account_balance" size={32} className="text-brand" />
532 Customer Accounts
533 </h1>
534 <p className="text-muted">
535 Charge accounts are credit accounts you give to customers and allow
536 them to pay later
537 </p>
538 <p className="text-sm text-muted mt-2">
539 A customer cannot have a credit limit alone - they must belong to a
540 customer account. Think "XYZ Appliances" (account) → "XYZ Appliances
541 Brisbane" (customer).
542 </p>
543 </div>
544
545 {/* Tabs */}
546 <div className="mb-6 bg-surface rounded-lg shadow-sm border border-border p-1">
547 <nav className="flex gap-2">
548 <button
549 onClick={() => setActiveTab("dashboard")}
550 className={`flex-1 py-3 px-6 font-semibold text-base rounded-md transition-all ${
551 activeTab === "dashboard"
552 ? "bg-brand text-surface shadow-md"
553 : "bg-transparent text-muted hover:bg-surface-2 hover:text-text"
554 }`}
555 >
556 <Icon name="bar_chart" size={20} className="inline mr-2" />
557 Dashboard
558 </button>
559 <button
560 onClick={() => setActiveTab("accounts")}
561 className={`flex-1 py-3 px-6 font-semibold text-base rounded-md transition-all ${
562 activeTab === "accounts"
563 ? "bg-brand text-surface shadow-md"
564 : "bg-transparent text-muted hover:bg-surface-2 hover:text-text"
565 }`}
566 >
567 <Icon name="list_alt" size={20} className="inline mr-2" />
568 All Accounts
569 <span className={`ml-2 px-2 py-0.5 text-xs rounded-full ${
570 activeTab === "accounts"
571 ? "bg-surface/20 text-surface"
572 : "bg-surface-2 text-text"
573 }`}>
574 {accounts.length}
575 </span>
576 </button>
577 <button
578 onClick={() => setActiveTab("actions")}
579 className={`flex-1 py-3 px-6 font-semibold text-base rounded-md transition-all ${
580 activeTab === "actions"
581 ? "bg-brand text-surface shadow-md"
582 : "bg-transparent text-muted hover:bg-surface-2 hover:text-text"
583 }`}
584 >
585 <Icon name="settings" size={20} className="inline mr-2" />
586 Actions & Reports
587 </button>
588 </nav>
589 </div>
590
591 {/* Tab Content */}
592 {activeTab === "dashboard" && (
593 <div className="flex gap-6">
594 {/* Main Content */}
595 <div className="flex-1">
596 {/* Key Actions */}
597 <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
598 <button
599 onClick={handleNewAccount}
600 className="bg-success text-surface p-6 rounded-lg shadow hover:shadow-lg transition-all hover:bg-success/80 text-center"
601 >
602 <div className="text-4xl mb-2"><Icon name="person_add" size={40} /></div>
603 <div className="font-semibold">New Account</div>
604 </button>
605
606 <a
607 href="/pages/customers/customer-accounts/enter-payment"
608 className="bg-surface p-6 rounded-lg shadow hover:shadow-lg transition-shadow text-center border-2 border-success"
609 >
610 <div className="text-4xl mb-2"><Icon name="payments" size={40} /></div>
611 <div className="font-semibold text-text">Enter Payment</div>
612 </a>
613
614 <a
615 href="/pages/reports/sales-reports/sales-by-account"
616 className="bg-surface p-6 rounded-lg shadow hover:shadow-lg transition-shadow text-center"
617 >
618 <div className="text-4xl mb-2"><Icon name="trending_up" size={40} /></div>
619 <div className="font-semibold text-text">Sales Summary</div>
620 </a>
621
622 <a
623 href="/pages/customers/customer-accounts/transactions"
624 className="bg-surface p-6 rounded-lg shadow hover:shadow-lg transition-shadow text-center"
625 >
626 <div className="text-4xl mb-2"><Icon name="credit_card" size={40} /></div>
627 <div className="font-semibold text-text">Transactions</div>
628 </a>
629 </div>
630
631 {/* Selected Account */}
632 {selectedAccount && (
633 <div className="bg-gradient-to-r from-brand to-brand/80 text-surface rounded-lg shadow-lg p-6 mb-8">
634 <div className="flex justify-between items-start">
635 <div className="flex-1">
636 <div className="text-sm opacity-90 mb-1">Selected Account</div>
637 <h3 className="text-2xl font-bold mb-2">{selectedAccount.name}</h3>
638 <div className="flex gap-6 text-sm">
639 <div>
640 <div className="opacity-90">Account #</div>
641 <div className="font-semibold">{selectedAccount.id}</div>
642 </div>
643 <div>
644 <div className="opacity-90">Balance</div>
645 <div className="font-semibold">{formatCurrency(selectedAccount.balance)}</div>
646 </div>
647 <div>
648 <div className="opacity-90">Credit Limit</div>
649 <div className="font-semibold">{formatCurrency(selectedAccount.creditLimit)}</div>
650 </div>
651 </div>
652 </div>
653 <div className="flex gap-2">
654 <button
655 onClick={() =>
656 window.location.href = `/report/pos/customer/fieldpine/account_edit.htm?accid=${selectedAccount.id}`
657 }
658 className="px-4 py-2 bg-surface text-brand rounded-lg hover:bg-surface-2 transition-colors font-semibold"
659 >
660 Edit
661 </button>
662 <button
663 onClick={handleUnlinkCustomers}
664 className="px-4 py-2 bg-surface/20 hover:bg-surface/30 rounded-lg transition-colors"
665 >
666 Unlink Customers
667 </button>
668 <button
669 onClick={() => {
670 setSelectedAccount(null);
671 setSearchTerm("");
672 }}
673 className="px-3 py-2 bg-surface/20 hover:bg-surface/30 rounded-lg transition-colors"
674 >
675
676 </button>
677 </div>
678 </div>
679 </div>
680 )}
681
682 {/* Quick Account Search */}
683 <div className="bg-surface rounded-lg shadow p-6 mb-8">
684 <div className="flex gap-4 items-end">
685 <div className="flex-1 relative">
686 <label className="block text-sm font-medium text-text mb-2">
687 Quick Account Search
688 </label>
689 <input
690 type="text"
691 value={searchTerm}
692 onChange={(e) => setSearchTerm(e.target.value)}
693 onFocus={() => {
694 if (searchResults.length > 0) setShowSearchResults(true);
695 }}
696 placeholder="Type to search accounts..."
697 className="w-full px-4 py-3 border border-border rounded-lg focus:ring-2 focus:ring-brand/50 focus:border-transparent text-lg"
698 />
699
700 {/* Search Results Dropdown */}
701 {showSearchResults && searchResults.length > 0 && (
702 <div className="absolute z-10 w-full mt-1 bg-surface border border-border rounded-lg shadow-xl max-h-96 overflow-y-auto">
703 {searchResults.map((account) => (
704 <div
705 key={account.id}
706 onClick={() => selectAccount(account)}
707 className="px-4 py-3 hover:bg-brand hover:text-surface cursor-pointer border-b border-border last:border-b-0 transition-colors"
708 >
709 <div className="font-semibold">
710 {account.name}
711 </div>
712 <div className="text-sm opacity-75">
713 #{account.id} • Balance: {formatCurrency(account.balance)}
714 </div>
715 </div>
716 ))}
717 </div>
718 )}
719 </div>
720 </div>
721 </div>
722
723 {/* Recent Sales - Top 10 */}
724 <div className="bg-surface rounded-lg shadow p-6">
725 <h2 className="text-xl font-bold text-text mb-4">
726 Recent Sales
727 </h2>
728
729 {salesLoading ? (
730 <div className="text-center py-8 text-muted">
731 Loading sales...
732 </div>
733 ) : (
734 <div className="overflow-x-auto">
735 <table className="w-full">
736 <thead className="bg-[var(--brand)] text-surface">
737 <tr>
738 <th
739 onClick={() => handleSort("accountId")}
740 className="px-4 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface"
741 >
742 Account#
743 {sortField === "accountId" && (
744 <span className="ml-1">
745 {sortDirection === "asc" ? "↑" : "↓"}
746 </span>
747 )}
748 </th>
749 <th
750 onClick={() => handleSort("accountName")}
751 className="px-4 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface"
752 >
753 Account Name
754 {sortField === "accountName" && (
755 <span className="ml-1">
756 {sortDirection === "asc" ? "↑" : "↓"}
757 </span>
758 )}
759 </th>
760 <th
761 onClick={() => handleSort("date")}
762 className="px-4 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface"
763 >
764 Date
765 {sortField === "date" && (
766 <span className="ml-1">
767 {sortDirection === "asc" ? "↑" : "↓"}
768 </span>
769 )}
770 </th>
771 <th
772 onClick={() => handleSort("saleId")}
773 className="px-4 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface"
774 >
775 Sale#
776 {sortField === "saleId" && (
777 <span className="ml-1">
778 {sortDirection === "asc" ? "↑" : "↓"}
779 </span>
780 )}
781 </th>
782 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
783 Status
784 </th>
785 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
786 Effect
787 </th>
788 <th
789 onClick={() => handleSort("accountBalance")}
790 className="px-4 py-3 text-right text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface"
791 >
792 Current Balance
793 {sortField === "accountBalance" && (
794 <span className="ml-1">
795 {sortDirection === "asc" ? "↑" : "↓"}
796 </span>
797 )}
798 </th>
799 </tr>
800 </thead>
801 <tbody className="bg-surface divide-y divide-border">
802 {sortedSales.map((sale, index) => (
803 <tr
804 key={index}
805 className={index % 2 === 0 ? "bg-surface" : "bg-surface-2"}
806 >
807 <td className="px-4 py-3 whitespace-nowrap text-sm text-text">
808 {sale.accountId}
809 </td>
810 <td className="px-4 py-3 whitespace-nowrap text-sm">
811 <a
812 href={`/report/pos/customer/fieldpine/account_single_overview.htm?accid=${sale.accountId}`}
813 className="text-brand hover:underline"
814 >
815 {sale.accountName}
816 </a>
817 </td>
818 <td className="px-4 py-3 whitespace-nowrap text-sm text-text">
819 <div className="flex justify-between">
820 <span>{sale.date}</span>
821 <span className="text-muted">{sale.time}</span>
822 </div>
823 </td>
824 <td className="px-4 py-3 whitespace-nowrap text-sm">
825 <a
826 href={`/report/pos/sales/fieldpine/SingleSale.htm?sid=${sale.saleId}`}
827 className="text-brand hover:underline"
828 >
829 {sale.saleId}
830 </a>
831 </td>
832 <td className="px-4 py-3 whitespace-nowrap text-sm">
833 <span
834 className={`px-2 py-1 rounded ${
835 sale.statusColor || "bg-surface-2"
836 }`}
837 >
838 {sale.status}
839 </span>
840 </td>
841 <td className="px-4 py-3 text-sm text-text text-right">
842 {sale.effect}
843 </td>
844 <td className="px-4 py-3 whitespace-nowrap text-sm text-text text-right">
845 {formatCurrency(sale.accountBalance)}
846 </td>
847 </tr>
848 ))}
849 </tbody>
850 </table>
851
852 {sortedSales.length === 0 && (
853 <div className="text-center py-8 text-muted">
854 No recent sales found
855 </div>
856 )}
857 </div>
858 )}
859 </div>
860 </div>
861
862 {/* Quick Stats Sidebar */}
863 <div className="w-80">
864 <div className="bg-surface rounded-lg shadow p-6 sticky top-6">
865 <h3 className="text-lg font-bold text-text text-center mb-4">
866 Quick Stats
867 </h3>
868
869 {loading ? (
870 <div className="text-center py-8 text-muted">Loading...</div>
871 ) : (
872 <div className="space-y-2 text-sm">
873 <hr className="my-2" />
874
875 <div className="flex justify-between" title="Net balance of all accounts">
876 <span className="text-muted">Outstanding</span>
877 <span className="font-semibold">
878 {formatCurrency(quickStats.outstanding)}
879 </span>
880 </div>
881
882 <div className="flex justify-between" title="Total amount owing to you">
883 <span className="text-muted text-xs">Due to You</span>
884 <span className="font-semibold">
885 {formatCurrency(quickStats.outstandingPos)}
886 </span>
887 </div>
888
889 <div className="flex justify-between" title="Total amount you owe them">
890 <span className="text-muted text-xs">In Credit</span>
891 <span className="font-semibold">
892 {formatCurrency(quickStats.outstandingNeg)}
893 </span>
894 </div>
895
896 <div className="flex justify-between">
897 <span className="text-muted">Total Number</span>
898 <span className="font-semibold">{quickStats.count}</span>
899 </div>
900
901 <div className="flex justify-between">
902 <span className="text-muted">Active</span>
903 <span className="font-semibold">
904 {quickStats.countActive}
905 </span>
906 </div>
907
908 <div className="flex justify-between">
909 <span className="text-muted">Average Balance</span>
910 <span className="font-semibold">
911 {formatCurrency(quickStats.averageBalance)}
912 </span>
913 </div>
914
915 <hr className="my-2" />
916
917 <div className="flex justify-between" title="Estimate per month at 10% cost of capital">
918 <span className="text-muted text-xs">
919 Cost of Credit
920 <br />
921 (est. monthly, 10%)
922 </span>
923 <span className="font-semibold">
924 {formatCurrency(quickStats.costOfCredit)}
925 </span>
926 </div>
927
928 <hr className="my-2" />
929
930 {/* Top 5 */}
931 <div className="flex justify-between items-center mb-2">
932 <span className="font-semibold">Top 5</span>
933 <span
934 className={`px-2 py-1 rounded font-semibold ${getTop5Color()}`}
935 >
936 {quickStats.top5Percentage}%
937 </span>
938 </div>
939
940 {quickStats.top5Accounts.map((acc, idx) => (
941 <div key={idx} className="flex justify-between">
942 <span className="text-muted text-xs truncate flex-1 mr-2">
943 {acc.name}
944 </span>
945 <span className="font-semibold text-xs whitespace-nowrap">
946 {formatCurrency(acc.balance)}
947 </span>
948 </div>
949 ))}
950
951 <hr className="my-2" />
952
953 {/* At Risk */}
954 <div className="font-semibold mb-2">At Risk</div>
955
956 {quickStats.riskAccounts.map((acc, idx) => (
957 <div key={idx} className="flex justify-between">
958 <a
959 href={`/report/pos/customer/fieldpine/account_single_overview.htm?accid=${acc.id}`}
960 className="text-brand hover:underline text-xs truncate flex-1 mr-2"
961 >
962 {acc.name}
963 </a>
964 <span className="font-semibold text-xs whitespace-nowrap">
965 {formatCurrency(acc.balance)}
966 </span>
967 </div>
968 ))}
969 </div>
970 )}
971 </div>
972 </div>
973 </div>
974 )}
975
976 {/* All Accounts Tab */}
977 {activeTab === "accounts" && (
978 <div className="bg-surface rounded-lg shadow p-6">
979 <h2 className="text-xl font-bold text-text mb-4">
980 All Accounts
981 </h2>
982
983 {/* Search and Filters */}
984 <div className="flex flex-wrap gap-4 items-center mb-6 p-4 bg-surface-2 rounded-lg sticky top-0 z-10">
985 <div className="flex items-center gap-2">
986 <label className="text-sm font-medium text-text">
987 Search
988 </label>
989 <input
990 type="text"
991 value={listSearchTerm}
992 onChange={(e) => setListSearchTerm(e.target.value)}
993 placeholder="Filter accounts..."
994 className="px-3 py-2 border border-border rounded-lg focus:ring-2 focus:ring-brand focus:border-transparent text-sm"
995 />
996 </div>
997
998 <label className="flex items-center gap-2 text-sm">
999 <input
1000 type="checkbox"
1001 checked={showZeroBalance}
1002 onChange={(e) => setShowZeroBalance(e.target.checked)}
1003 className="rounded"
1004 />
1005 <span className="text-text">Show Zero Balance</span>
1006 </label>
1007
1008 <label className="flex items-center gap-2 text-sm">
1009 <input
1010 type="checkbox"
1011 checked={showRetired}
1012 onChange={(e) => setShowRetired(e.target.checked)}
1013 className="rounded"
1014 />
1015 <span className="text-text">Show Retired/Disabled</span>
1016 </label>
1017
1018 <div className="ml-auto text-sm text-muted font-medium">
1019 {accountsLoading ? (
1020 "Loading..."
1021 ) : (
1022 <>
1023 {accounts.length} of {allAccounts.length} accounts
1024 </>
1025 )}
1026 </div>
1027 </div>
1028
1029 {/* Account Table */}
1030 {accountsLoading ? (
1031 <div className="space-y-3">
1032 {[...Array(10)].map((_, i) => (
1033 <div key={i} className="animate-pulse flex gap-4">
1034 <div className="h-12 bg-surface-2 rounded flex-1"></div>
1035 </div>
1036 ))}
1037 </div>
1038 ) : (
1039 <div className="overflow-x-auto">
1040 <table className="w-full">
1041 <thead className="bg-[var(--brand)] text-surface">
1042 <tr>
1043 <th
1044 onClick={() => handleAccountSort("id")}
1045 className="px-4 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface-2"
1046 >
1047 Id#
1048 {accountSortField === "id" && (
1049 <span className="ml-1">
1050 {accountSortDirection === "asc" ? "↑" : "↓"}
1051 </span>
1052 )}
1053 </th>
1054 <th
1055 onClick={() => handleAccountSort("name")}
1056 className="px-4 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface-2"
1057 >
1058 Name
1059 {accountSortField === "name" && (
1060 <span className="ml-1">
1061 {accountSortDirection === "asc" ? "↑" : "↓"}
1062 </span>
1063 )}
1064 </th>
1065 <th
1066 onClick={() => handleAccountSort("numCustomers")}
1067 className="px-4 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface-2"
1068 >
1069 Customers
1070 {accountSortField === "numCustomers" && (
1071 <span className="ml-1">
1072 {accountSortDirection === "asc" ? "↑" : "↓"}
1073 </span>
1074 )}
1075 </th>
1076 <th
1077 onClick={() => handleAccountSort("balance")}
1078 className="px-4 py-3 text-right text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface-2"
1079 >
1080 Balance
1081 {accountSortField === "balance" && (
1082 <span className="ml-1">
1083 {accountSortDirection === "asc" ? "↑" : "↓"}
1084 </span>
1085 )}
1086 </th>
1087 <th
1088 onClick={() => handleAccountSort("email")}
1089 className="px-4 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider cursor-pointer hover:bg-surface-2"
1090 >
1091 Email
1092 {accountSortField === "email" && (
1093 <span className="ml-1">
1094 {accountSortDirection === "asc" ? "↑" : "↓"}
1095 </span>
1096 )}
1097 </th>
1098 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
1099 Actions
1100 </th>
1101 </tr>
1102 </thead>
1103 <tbody className="bg-surface divide-y divide-border">
1104 {sortedAccounts.map((account, index) => (
1105 <tr
1106 key={account.id}
1107 className={`hover:bg-surface-2 transition-colors ${
1108 index % 2 === 0 ? "bg-surface" : "bg-surface-2"
1109 }`}
1110 >
1111 <td className="px-4 py-3 whitespace-nowrap text-sm text-text font-medium">
1112 {account.id}
1113 </td>
1114 <td className="px-4 py-3 whitespace-nowrap text-sm">
1115 <a
1116 href={`/report/pos/customer/fieldpine/account_single_overview.htm?accid=${account.id}`}
1117 className="text-brand hover:underline font-medium"
1118 >
1119 {account.name}
1120 </a>
1121 </td>
1122 <td className="px-4 py-3 whitespace-nowrap text-sm text-text text-center">
1123 {account.numCustomers}
1124 </td>
1125 <td className="px-4 py-3 whitespace-nowrap text-sm text-text text-right font-semibold">
1126 {formatCurrency(account.balance)}
1127 </td>
1128 <td className="px-4 py-3 text-sm text-text">
1129 {account.email && (
1130 <a
1131 href={`mailto:${account.email}`}
1132 className="text-brand hover:underline"
1133 >
1134 {account.email}
1135 </a>
1136 )}
1137 </td>
1138 <td className="px-4 py-3 whitespace-nowrap text-sm">
1139 <button
1140 onClick={() =>
1141 (window.location.href = `/report/pos/customer/fieldpine/account_edit.htm?accid=${account.id}`)
1142 }
1143 className="px-3 py-1 bg-danger text-surface rounded hover:bg-danger/80 text-xs font-medium"
1144 >
1145 Edit
1146 </button>
1147 </td>
1148 </tr>
1149 ))}
1150 </tbody>
1151 </table>
1152
1153 {sortedAccounts.length === 0 && (
1154 <div className="text-center py-12 text-muted">
1155 <Icon name="search_off" size={48} className="mx-auto mb-2" />
1156 <div>No accounts found</div>
1157 </div>
1158 )}
1159 </div>
1160 )}
1161 </div>
1162 )}
1163
1164 {/* New Account Modal */}
1165 {showNewAccountModal && (
1166 <div className="fixed inset-0 modal-backdrop flex items-center justify-center z-50">
1167 <div className="bg-surface rounded-lg shadow-xl p-6 w-full max-w-md">
1168 <h2 className="text-xl font-semibold text-text mb-4">Create New Account</h2>
1169 <div className="space-y-4">
1170 <div>
1171 <label className="block text-sm font-medium text-text mb-1">
1172 Account Name *
1173 </label>
1174 <input
1175 type="text"
1176 value={newAccountForm.name}
1177 onChange={(e) =>
1178 setNewAccountForm({ ...newAccountForm, name: e.target.value })
1179 }
1180 className="w-full px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-brand"
1181 placeholder="Enter account name"
1182 />
1183 </div>
1184 <div>
1185 <label className="block text-sm font-medium text-text mb-1">
1186 Credit Limit ($)
1187 </label>
1188 <input
1189 type="number"
1190 value={newAccountForm.creditLimit}
1191 onChange={(e) =>
1192 setNewAccountForm({
1193 ...newAccountForm,
1194 creditLimit: e.target.value,
1195 })
1196 }
1197 className="w-full px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-brand"
1198 placeholder="0"
1199 />
1200 </div>
1201 <div>
1202 <label className="block text-sm font-medium text-text mb-1">
1203 Floor Limit ($)
1204 </label>
1205 <input
1206 type="number"
1207 value={newAccountForm.floorLimit}
1208 onChange={(e) =>
1209 setNewAccountForm({
1210 ...newAccountForm,
1211 floorLimit: e.target.value,
1212 })
1213 }
1214 className="w-full px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-brand"
1215 placeholder="0"
1216 />
1217 </div>
1218 <div className="flex items-center gap-2">
1219 <input
1220 type="checkbox"
1221 checked={newAccountForm.goToEdit}
1222 onChange={(e) =>
1223 setNewAccountForm({
1224 ...newAccountForm,
1225 goToEdit: e.target.checked,
1226 })
1227 }
1228 className="rounded"
1229 />
1230 <label className="text-sm text-text">Go to edit screen after creating</label>
1231 </div>
1232 </div>
1233 <div className="flex gap-3 mt-6">
1234 <button
1235 onClick={() => setShowNewAccountModal(false)}
1236 className="flex-1 px-4 py-2 border border-border rounded-md text-sm font-medium text-text hover:bg-surface-2"
1237 >
1238 Cancel
1239 </button>
1240 <button
1241 onClick={saveNewAccount}
1242 className="flex-1 px-4 py-2 bg-success text-surface rounded-md text-sm font-medium hover:bg-success/80 flex items-center justify-center gap-2"
1243 >
1244 <Icon name="check" size={18} />
1245 Create Account
1246 </button>
1247 </div>
1248 </div>
1249 </div>
1250 )}
1251
1252 {/* Unlink Customers Modal */}
1253 {showUnlinkModal && (
1254 <div className="fixed inset-0 modal-backdrop flex items-center justify-center z-50">
1255 <div className="bg-surface rounded-lg shadow-xl p-6 w-full max-w-2xl">
1256 <div className="flex justify-between items-center mb-4">
1257 <h2 className="text-xl font-bold text-text">
1258 Unlink Customers from Account
1259 </h2>
1260 <button
1261 onClick={() => setShowUnlinkModal(false)}
1262 className="text-muted hover:text-text"
1263 >
1264 <Icon name="close" size={24} />
1265 </button>
1266 </div>
1267
1268 <div className="mb-4">
1269 <h3 className="font-semibold mb-2">Current Customers</h3>
1270 {linkedCustomers.length === 0 ? (
1271 <p className="text-muted text-center py-4">
1272 No customers linked to this account
1273 </p>
1274 ) : (
1275 <div className="overflow-x-auto">
1276 <table className="w-full border border-border">
1277 <thead className="bg-[var(--brand)] text-surface">
1278 <tr>
1279 <th className="px-4 py-2 text-left border-b">Cid#</th>
1280 <th className="px-4 py-2 text-left border-b">
1281 Customer Name
1282 </th>
1283 <th className="px-4 py-2 text-left border-b">
1284 Options
1285 </th>
1286 </tr>
1287 </thead>
1288 <tbody>
1289 {linkedCustomers.map((customer) => (
1290 <tr key={customer.id} className="border-b">
1291 <td className="px-4 py-2">{customer.id}</td>
1292 <td className="px-4 py-2">{customer.name}</td>
1293 <td className="px-4 py-2">
1294 <button
1295 onClick={() => unlinkCustomer(customer.id)}
1296 className="px-3 py-1 bg-danger text-surface rounded hover:bg-danger/80 text-sm"
1297 >
1298 Unlink
1299 </button>
1300 </td>
1301 </tr>
1302 ))}
1303 </tbody>
1304 </table>
1305 </div>
1306 )}
1307 </div>
1308
1309 <div className="bg-warn/10 border border-warn/30 rounded p-4 mb-4">
1310 <p className="text-sm text-text mb-2">
1311 Unlinking customers that have already made purchases on account
1312 has some restrictions:
1313 </p>
1314 <ul className="list-disc list-inside text-sm text-text space-y-1">
1315 <li>
1316 Any sales already placed on a statement will not be altered.
1317 You may need to enter some manual adjustments or reverse the
1318 statement before unlinking the customer
1319 </li>
1320 <li>
1321 If the customer you are unlinking has current unbilled sales
1322 to be charged to an account, you will need to choose a new
1323 account to link them to
1324 </li>
1325 <li>
1326 Unlinking accounts may cause some reconciliation reports to
1327 complain about both the old and new accounts
1328 </li>
1329 </ul>
1330 </div>
1331
1332 <button
1333 onClick={() => setShowUnlinkModal(false)}
1334 className="w-full px-4 py-2 bg-surface-2 text-text rounded-lg hover:bg-surface-2"
1335 >
1336 Close
1337 </button>
1338 </div>
1339 </div>
1340 )}
1341
1342 {/* Actions Tab */}
1343 {activeTab === "actions" && (
1344 <div className="space-y-6">
1345 {/* Reporting */}
1346 <div className="bg-surface rounded-lg shadow p-6">
1347 <h3 className="text-lg font-bold text-text mb-4 flex items-center gap-2">
1348 <span className="text-2xl">📊</span> Reporting
1349 </h3>
1350 <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
1351 <a
1352 href={excelExportUrl}
1353 target="_blank"
1354 rel="noopener noreferrer"
1355 className="bg-surface-2 p-4 rounded-lg hover:bg-surface-2 transition-colors text-center w-full"
1356 >
1357 <div className="text-3xl mb-2">📊</div>
1358 <div className="text-sm font-semibold text-text">Excel Export</div>
1359 </a>
1360
1361 <a
1362 href="/pages/reports/sales-reports/sales-by-account"
1363 className="bg-surface-2 p-4 rounded-lg hover:bg-surface-2 transition-colors text-center"
1364 >
1365 <div className="text-3xl mb-2">📈</div>
1366 <div className="text-sm font-semibold text-text">Sales Summary</div>
1367 </a>
1368
1369 <a
1370 href="/pages/customers/customer-accounts/transactions"
1371 className="bg-surface-2 p-4 rounded-lg hover:bg-surface-2 transition-colors text-center"
1372 >
1373 <div className="text-3xl mb-2">💳</div>
1374 <div className="text-sm font-semibold text-text">Transaction List</div>
1375 </a>
1376
1377 <a
1378 href="/pages/customers/customer-accounts/aged-debtors"
1379 className="bg-surface-2 p-4 rounded-lg hover:bg-surface-2 transition-colors text-center"
1380 >
1381 <div className="text-3xl mb-2">💰</div>
1382 <div className="text-sm font-semibold text-text">Aged Debtors</div>
1383 </a>
1384
1385 <a
1386 href="/pages/reports/sales-reports/sales-by-teller"
1387 className="bg-surface-2 p-4 rounded-lg hover:bg-surface-2 transition-colors text-center"
1388 >
1389 <div className="text-3xl mb-2">👥</div>
1390 <div className="text-sm font-semibold text-text">Sales Rep Summary</div>
1391 </a>
1392 </div>
1393 </div>
1394
1395 {/* Account Management */}
1396 <div className="bg-surface rounded-lg shadow p-6">
1397 <h3 className="text-lg font-bold text-text mb-4 flex items-center gap-2">
1398 <Icon name="settings" size={24} className="text-brand" /> Account Management
1399 </h3>
1400 <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
1401 <button
1402 onClick={handleNewAccount}
1403 className="bg-success text-surface p-4 rounded-lg hover:bg-success/80 transition-colors text-center"
1404 >
1405 <Icon name="person_add" size={32} className="mx-auto mb-2" />
1406 <div className="text-sm font-semibold">New Account</div>
1407 </button>
1408
1409 </div>
1410 </div>
1411
1412 {/* Transactions */}
1413 <div className="bg-surface rounded-lg shadow p-6">
1414 <h3 className="text-lg font-bold text-text mb-4 flex items-center gap-2">
1415 <Icon name="payments" size={24} className="text-brand" /> Transactions
1416 </h3>
1417 <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
1418 <a
1419 href="/pages/customers/customer-accounts/enter-payment"
1420 className="bg-success/10 border border-success/30 p-4 rounded-lg hover:bg-success/20 transition-colors text-center"
1421 >
1422 <Icon name="payments" size={32} className="mx-auto mb-2 text-success" />
1423 <div className="text-sm font-semibold text-text">Enter Payment</div>
1424 </a>
1425
1426 <a
1427 href="/pages/customers/customer-accounts/account-adjustment"
1428 className="bg-surface-2 p-4 rounded-lg hover:bg-surface-2 transition-colors text-center"
1429 >
1430 <Icon name="edit_note" size={32} className="mx-auto mb-2 text-brand" />
1431 <div className="text-sm font-semibold text-text">Enter Adjustment</div>
1432 </a>
1433 </div>
1434 </div>
1435
1436 {/* Statements */}
1437 <div className="bg-surface rounded-lg shadow p-6">
1438 <h3 className="text-lg font-bold text-text mb-4 flex items-center gap-2">
1439 <span className="text-2xl">📄</span> Statements
1440 </h3>
1441 <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
1442 <a
1443 href="/pages/customers/customer-accounts/view-statements"
1444 className="bg-surface-2 p-4 rounded-lg hover:bg-surface-2 transition-colors text-center"
1445 >
1446 <div className="text-3xl mb-2">📄</div>
1447 <div className="text-sm font-semibold text-text">View Statements</div>
1448 </a>
1449
1450 <a
1451 href="/pages/customers/customer-accounts/run-statements"
1452 className="bg-surface-2 p-4 rounded-lg hover:bg-surface-2 transition-colors text-center"
1453 >
1454 <div className="text-3xl mb-2">📝</div>
1455 <div className="text-sm font-semibold text-text">Run Statements</div>
1456 </a>
1457 </div>
1458 </div>
1459 </div>
1460 )}
1461 </div>
1462 );
1463}