3import React, { useState, useEffect, useRef } from "react";
4import { apiClient } from "@/lib/client/apiClient";
5import { Icon } from '@/contexts/IconContext';
19 accountBalance: number;
30 outstandingPos: number;
31 outstandingNeg: number;
34 averageBalance: number;
36 top5Accounts: Array<{ name: string; balance: number }>;
37 top5Percentage: number;
38 riskAccounts: Array<{ id: number; name: string; balance: number }>;
46interface AccountListItem {
53 paymentHistory: string;
54 lastStatement: string;
55 receiptFormat: string;
61export default function CustomerAccountsPage() {
62 const [quickStats, setQuickStats] = useState<QuickStats>({
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);
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");
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`;
100 const [newAccountForm, setNewAccountForm] = useState({
108 const [sortField, setSortField] = useState<string>("date");
109 const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
112 const [activeTab, setActiveTab] = useState<"dashboard" | "accounts" | "actions">("dashboard");
122 }, [showZeroBalance, showRetired]);
126 if (searchTimeout.current) {
127 clearTimeout(searchTimeout.current);
130 if (searchTerm.length >= 2) {
131 searchTimeout.current = setTimeout(() => {
135 setSearchResults([]);
136 setShowSearchResults(false);
137 setSelectedAccount(null);
141 if (searchTimeout.current) {
142 clearTimeout(searchTimeout.current);
147 const searchAccounts = async () => {
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) => {
155 name: item.f101 || "",
156 balance: item.f114 || 0,
157 creditLimit: item.f106 || 0,
158 floorLimit: item.f107 || 0,
162 setSearchResults(results);
163 setShowSearchResults(true);
165 console.error("Error searching accounts:", error);
169 const selectAccount = (account: Account) => {
170 setSelectedAccount(account);
171 setSearchTerm(account.name);
172 setShowSearchResults(false);
175 const loadQuickStats = async () => {
177 const result = await apiClient.getAccounts({ type: 'summary' });
178 const response = result.success && result.data && result.data[0] ? result.data[0] : {};
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;
185 top5.push({ name, balance });
189 const riskAccounts: Array<{ id: number; name: string; balance: number }> =
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;
196 riskAccounts.push({ id, name, balance });
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;
208 const top5Total = top5.reduce((sum, acc) => sum + acc.balance, 0);
210 outstanding === 0 ? 0 : Math.floor((top5Total * 100) / outstanding);
218 averageBalance: avgBal / avgCount,
219 costOfCredit: (outstanding * 0.1) / 12,
221 top5Percentage: top5Pct,
226 console.error("Error loading quick stats:", error);
231 const loadRecentSales = async () => {
233 const result = await apiClient.getRecentAccountSales();
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", {
245 const timeFormatted = date.toLocaleTimeString("en-US", {
251 const phase = item.f108 || 0;
252 let status = "Normal";
253 let statusColor = "";
255 status = "Still Active";
256 statusColor = "bg-info";
257 } else if (phase === 200) {
259 statusColor = "bg-warn";
260 } else if (phase === 10000 || phase === 10002) {
262 statusColor = "bg-brand/30";
263 } else if (phase !== 1) {
264 status = String(phase);
265 statusColor = "bg-info";
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,
278 effect: item.f904 || "",
283 setRecentSales(sales);
284 setSalesLoading(false);
286 console.error("Error loading recent sales:", error);
287 setSalesLoading(false);
291 const loadAccountList = async () => {
293 setAccountsLoading(true);
294 let url = "/buck?3=retailmax.elink.account.detail&9=f100,4,0&10=1003,184,1100,1105,1107";
296 if (!showZeroBalance) {
297 url += "&9=f114,5,0";
300 url += "&9=f147,0,1";
303 const result = await apiClient.getAccounts({ type: 'list' });
305 const accountList: AccountListItem[] = [];
306 if (result.success && result.data) {
307 result.data.forEach((item: any) => {
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 || "",
325 setAllAccounts(accountList);
326 setAccounts(accountList);
327 setAccountsLoading(false);
329 console.error("Error loading account list:", error);
330 setAccountsLoading(false);
334 const handleListSearch = () => {
335 if (!listSearchTerm) {
336 setAccounts(allAccounts);
340 const searchLower = listSearchTerm.toLowerCase();
341 const filtered = allAccounts.filter((acc) => {
343 acc.name.toLowerCase().includes(searchLower) ||
344 String(acc.id).includes(searchLower) ||
345 acc.email.toLowerCase().includes(searchLower)
348 setAccounts(filtered);
353 }, [listSearchTerm, allAccounts]);
355 const handleAccountSort = (field: string) => {
356 if (accountSortField === field) {
357 setAccountSortDirection(accountSortDirection === "asc" ? "desc" : "asc");
359 setAccountSortField(field);
360 setAccountSortDirection("asc");
364 const handleSort = (field: string) => {
365 if (sortField === field) {
366 setSortDirection(sortDirection === "asc" ? "desc" : "asc");
369 setSortDirection("asc");
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];
377 if (sortField === "date") {
378 aVal = `${a.date} ${a.time}`;
379 bVal = `${b.date} ${b.time}`;
382 if (typeof aVal === "string") {
383 return sortDirection === "asc"
384 ? aVal.localeCompare(bVal)
385 : bVal.localeCompare(aVal);
387 return sortDirection === "asc" ? aVal - bVal : bVal - aVal;
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];
394 if (typeof aVal === "string") {
395 return accountSortDirection === "asc"
396 ? aVal.localeCompare(bVal)
397 : bVal.localeCompare(aVal);
399 return accountSortDirection === "asc" ? aVal - bVal : bVal - aVal;
402 const handleNewAccount = () => {
409 setShowNewAccountModal(true);
412 const saveNewAccount = async () => {
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) => {
425 if (newAccountForm.creditLimit) {
426 xml += `<f106_s>${newAccountForm.creditLimit}</f106_s>`;
428 if (newAccountForm.floorLimit) {
429 xml += `<f107_s>${newAccountForm.floorLimit}</f107_s>`;
433 const response = await fetch("/DATI", {
435 headers: { "Content-Type": "application/xml" },
440 const result = await response.json();
442 result.APPD?.[0]?.f100 || result.DATS?.[0]?.f100 || 0;
444 setShowNewAccountModal(false);
447 if (newAccountForm.goToEdit && newAccId !== 0) {
448 window.location.href = `/report/pos/customer/fieldpine/account_edit.htm?accid=${newAccId}`;
452 console.error("Error creating account:", error);
453 alert("Failed to create account");
457 const handleUnlinkCustomers = async () => {
458 if (!selectedAccount) return;
461 const result = await apiClient.getAccountCustomers(selectedAccount.id);
463 const customers: Customer[] = [];
464 if (result.success && result.data) {
465 result.data.forEach((item: any) => {
466 if (item.f100 && item.f100 !== 0) {
469 name: item.f101 || "",
475 setLinkedCustomers(customers);
476 setShowUnlinkModal(true);
478 console.error("Error loading customers:", error);
479 alert("Failed to load linked customers");
483 const unlinkCustomer = async (customerId: number) => {
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>";
491 const response = await fetch("/DATI", {
493 headers: { "Content-Type": "application/xml" },
499 linkedCustomers.filter((c) => c.id !== customerId)
501 if (linkedCustomers.length <= 1) {
502 setShowUnlinkModal(false);
506 console.error("Error unlinking customer:", error);
507 alert("Failed to unlink customer");
511 const formatCurrency = (value: number) => {
512 return new Intl.NumberFormat("en-US", {
515 minimumFractionDigits: 0,
516 maximumFractionDigits: 0,
520 const getTop5Color = () => {
521 if (quickStats.top5Percentage > 70) return "bg-danger text-surface";
522 if (quickStats.top5Percentage > 20) return "bg-warn text-surface";
527 <div className="p-6 bg-surface-2 min-h-screen">
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" />
534 <p className="text-muted">
535 Charge accounts are credit accounts you give to customers and allow
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).
546 <div className="mb-6 bg-surface rounded-lg shadow-sm border border-border p-1">
547 <nav className="flex gap-2">
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"
556 <Icon name="bar_chart" size={20} className="inline mr-2" />
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"
567 <Icon name="list_alt" size={20} className="inline mr-2" />
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"
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"
585 <Icon name="settings" size={20} className="inline mr-2" />
592 {activeTab === "dashboard" && (
593 <div className="flex gap-6">
595 <div className="flex-1">
597 <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
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"
602 <div className="text-4xl mb-2"><Icon name="person_add" size={40} /></div>
603 <div className="font-semibold">New Account</div>
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"
610 <div className="text-4xl mb-2"><Icon name="payments" size={40} /></div>
611 <div className="font-semibold text-text">Enter Payment</div>
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"
618 <div className="text-4xl mb-2"><Icon name="trending_up" size={40} /></div>
619 <div className="font-semibold text-text">Sales Summary</div>
623 href="/pages/customers/customer-accounts/transactions"
624 className="bg-surface p-6 rounded-lg shadow hover:shadow-lg transition-shadow text-center"
626 <div className="text-4xl mb-2"><Icon name="credit_card" size={40} /></div>
627 <div className="font-semibold text-text">Transactions</div>
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">
640 <div className="opacity-90">Account #</div>
641 <div className="font-semibold">{selectedAccount.id}</div>
644 <div className="opacity-90">Balance</div>
645 <div className="font-semibold">{formatCurrency(selectedAccount.balance)}</div>
648 <div className="opacity-90">Credit Limit</div>
649 <div className="font-semibold">{formatCurrency(selectedAccount.creditLimit)}</div>
653 <div className="flex gap-2">
656 window.location.href = `/report/pos/customer/fieldpine/account_edit.htm?accid=${selectedAccount.id}`
658 className="px-4 py-2 bg-surface text-brand rounded-lg hover:bg-surface-2 transition-colors font-semibold"
663 onClick={handleUnlinkCustomers}
664 className="px-4 py-2 bg-surface/20 hover:bg-surface/30 rounded-lg transition-colors"
670 setSelectedAccount(null);
673 className="px-3 py-2 bg-surface/20 hover:bg-surface/30 rounded-lg transition-colors"
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">
692 onChange={(e) => setSearchTerm(e.target.value)}
694 if (searchResults.length > 0) setShowSearchResults(true);
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"
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) => (
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"
709 <div className="font-semibold">
712 <div className="text-sm opacity-75">
713 #{account.id} • Balance: {formatCurrency(account.balance)}
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">
730 <div className="text-center py-8 text-muted">
734 <div className="overflow-x-auto">
735 <table className="w-full">
736 <thead className="bg-[var(--brand)] text-surface">
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"
743 {sortField === "accountId" && (
744 <span className="ml-1">
745 {sortDirection === "asc" ? "↑" : "↓"}
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"
754 {sortField === "accountName" && (
755 <span className="ml-1">
756 {sortDirection === "asc" ? "↑" : "↓"}
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"
765 {sortField === "date" && (
766 <span className="ml-1">
767 {sortDirection === "asc" ? "↑" : "↓"}
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"
776 {sortField === "saleId" && (
777 <span className="ml-1">
778 {sortDirection === "asc" ? "↑" : "↓"}
782 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
785 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
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"
793 {sortField === "accountBalance" && (
794 <span className="ml-1">
795 {sortDirection === "asc" ? "↑" : "↓"}
801 <tbody className="bg-surface divide-y divide-border">
802 {sortedSales.map((sale, index) => (
805 className={index % 2 === 0 ? "bg-surface" : "bg-surface-2"}
807 <td className="px-4 py-3 whitespace-nowrap text-sm text-text">
810 <td className="px-4 py-3 whitespace-nowrap text-sm">
812 href={`/report/pos/customer/fieldpine/account_single_overview.htm?accid=${sale.accountId}`}
813 className="text-brand hover:underline"
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>
824 <td className="px-4 py-3 whitespace-nowrap text-sm">
826 href={`/report/pos/sales/fieldpine/SingleSale.htm?sid=${sale.saleId}`}
827 className="text-brand hover:underline"
832 <td className="px-4 py-3 whitespace-nowrap text-sm">
834 className={`px-2 py-1 rounded ${
835 sale.statusColor || "bg-surface-2"
841 <td className="px-4 py-3 text-sm text-text text-right">
844 <td className="px-4 py-3 whitespace-nowrap text-sm text-text text-right">
845 {formatCurrency(sale.accountBalance)}
852 {sortedSales.length === 0 && (
853 <div className="text-center py-8 text-muted">
854 No recent sales found
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">
870 <div className="text-center py-8 text-muted">Loading...</div>
872 <div className="space-y-2 text-sm">
873 <hr className="my-2" />
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)}
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)}
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)}
896 <div className="flex justify-between">
897 <span className="text-muted">Total Number</span>
898 <span className="font-semibold">{quickStats.count}</span>
901 <div className="flex justify-between">
902 <span className="text-muted">Active</span>
903 <span className="font-semibold">
904 {quickStats.countActive}
908 <div className="flex justify-between">
909 <span className="text-muted">Average Balance</span>
910 <span className="font-semibold">
911 {formatCurrency(quickStats.averageBalance)}
915 <hr className="my-2" />
917 <div className="flex justify-between" title="Estimate per month at 10% cost of capital">
918 <span className="text-muted text-xs">
923 <span className="font-semibold">
924 {formatCurrency(quickStats.costOfCredit)}
928 <hr className="my-2" />
931 <div className="flex justify-between items-center mb-2">
932 <span className="font-semibold">Top 5</span>
934 className={`px-2 py-1 rounded font-semibold ${getTop5Color()}`}
936 {quickStats.top5Percentage}%
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">
945 <span className="font-semibold text-xs whitespace-nowrap">
946 {formatCurrency(acc.balance)}
951 <hr className="my-2" />
954 <div className="font-semibold mb-2">At Risk</div>
956 {quickStats.riskAccounts.map((acc, idx) => (
957 <div key={idx} className="flex justify-between">
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"
964 <span className="font-semibold text-xs whitespace-nowrap">
965 {formatCurrency(acc.balance)}
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">
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">
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"
998 <label className="flex items-center gap-2 text-sm">
1001 checked={showZeroBalance}
1002 onChange={(e) => setShowZeroBalance(e.target.checked)}
1005 <span className="text-text">Show Zero Balance</span>
1008 <label className="flex items-center gap-2 text-sm">
1011 checked={showRetired}
1012 onChange={(e) => setShowRetired(e.target.checked)}
1015 <span className="text-text">Show Retired/Disabled</span>
1018 <div className="ml-auto text-sm text-muted font-medium">
1019 {accountsLoading ? (
1023 {accounts.length} of {allAccounts.length} accounts
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>
1039 <div className="overflow-x-auto">
1040 <table className="w-full">
1041 <thead className="bg-[var(--brand)] text-surface">
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"
1048 {accountSortField === "id" && (
1049 <span className="ml-1">
1050 {accountSortDirection === "asc" ? "↑" : "↓"}
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"
1059 {accountSortField === "name" && (
1060 <span className="ml-1">
1061 {accountSortDirection === "asc" ? "↑" : "↓"}
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"
1070 {accountSortField === "numCustomers" && (
1071 <span className="ml-1">
1072 {accountSortDirection === "asc" ? "↑" : "↓"}
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"
1081 {accountSortField === "balance" && (
1082 <span className="ml-1">
1083 {accountSortDirection === "asc" ? "↑" : "↓"}
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"
1092 {accountSortField === "email" && (
1093 <span className="ml-1">
1094 {accountSortDirection === "asc" ? "↑" : "↓"}
1098 <th className="px-4 py-3 text-left text-xs font-medium text-muted uppercase tracking-wider">
1103 <tbody className="bg-surface divide-y divide-border">
1104 {sortedAccounts.map((account, index) => (
1107 className={`hover:bg-surface-2 transition-colors ${
1108 index % 2 === 0 ? "bg-surface" : "bg-surface-2"
1111 <td className="px-4 py-3 whitespace-nowrap text-sm text-text font-medium">
1114 <td className="px-4 py-3 whitespace-nowrap text-sm">
1116 href={`/report/pos/customer/fieldpine/account_single_overview.htm?accid=${account.id}`}
1117 className="text-brand hover:underline font-medium"
1122 <td className="px-4 py-3 whitespace-nowrap text-sm text-text text-center">
1123 {account.numCustomers}
1125 <td className="px-4 py-3 whitespace-nowrap text-sm text-text text-right font-semibold">
1126 {formatCurrency(account.balance)}
1128 <td className="px-4 py-3 text-sm text-text">
1131 href={`mailto:${account.email}`}
1132 className="text-brand hover:underline"
1138 <td className="px-4 py-3 whitespace-nowrap text-sm">
1141 (window.location.href = `/report/pos/customer/fieldpine/account_edit.htm?accid=${account.id}`)
1143 className="px-3 py-1 bg-danger text-surface rounded hover:bg-danger/80 text-xs font-medium"
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>
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">
1171 <label className="block text-sm font-medium text-text mb-1">
1176 value={newAccountForm.name}
1178 setNewAccountForm({ ...newAccountForm, name: e.target.value })
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"
1185 <label className="block text-sm font-medium text-text mb-1">
1190 value={newAccountForm.creditLimit}
1194 creditLimit: e.target.value,
1197 className="w-full px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-brand"
1202 <label className="block text-sm font-medium text-text mb-1">
1207 value={newAccountForm.floorLimit}
1211 floorLimit: e.target.value,
1214 className="w-full px-3 py-2 border border-border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-brand"
1218 <div className="flex items-center gap-2">
1221 checked={newAccountForm.goToEdit}
1225 goToEdit: e.target.checked,
1230 <label className="text-sm text-text">Go to edit screen after creating</label>
1233 <div className="flex gap-3 mt-6">
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"
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"
1244 <Icon name="check" size={18} />
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
1261 onClick={() => setShowUnlinkModal(false)}
1262 className="text-muted hover:text-text"
1264 <Icon name="close" size={24} />
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
1275 <div className="overflow-x-auto">
1276 <table className="w-full border border-border">
1277 <thead className="bg-[var(--brand)] text-surface">
1279 <th className="px-4 py-2 text-left border-b">Cid#</th>
1280 <th className="px-4 py-2 text-left border-b">
1283 <th className="px-4 py-2 text-left border-b">
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">
1295 onClick={() => unlinkCustomer(customer.id)}
1296 className="px-3 py-1 bg-danger text-surface rounded hover:bg-danger/80 text-sm"
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:
1314 <ul className="list-disc list-inside text-sm text-text space-y-1">
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
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
1326 Unlinking accounts may cause some reconciliation reports to
1327 complain about both the old and new accounts
1333 onClick={() => setShowUnlinkModal(false)}
1334 className="w-full px-4 py-2 bg-surface-2 text-text rounded-lg hover:bg-surface-2"
1343 {activeTab === "actions" && (
1344 <div className="space-y-6">
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
1350 <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
1352 href={excelExportUrl}
1354 rel="noopener noreferrer"
1355 className="bg-surface-2 p-4 rounded-lg hover:bg-surface-2 transition-colors text-center w-full"
1357 <div className="text-3xl mb-2">📊</div>
1358 <div className="text-sm font-semibold text-text">Excel Export</div>
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"
1365 <div className="text-3xl mb-2">📈</div>
1366 <div className="text-sm font-semibold text-text">Sales Summary</div>
1370 href="/pages/customers/customer-accounts/transactions"
1371 className="bg-surface-2 p-4 rounded-lg hover:bg-surface-2 transition-colors text-center"
1373 <div className="text-3xl mb-2">💳</div>
1374 <div className="text-sm font-semibold text-text">Transaction List</div>
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"
1381 <div className="text-3xl mb-2">💰</div>
1382 <div className="text-sm font-semibold text-text">Aged Debtors</div>
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"
1389 <div className="text-3xl mb-2">👥</div>
1390 <div className="text-sm font-semibold text-text">Sales Rep Summary</div>
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
1400 <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
1402 onClick={handleNewAccount}
1403 className="bg-success text-surface p-4 rounded-lg hover:bg-success/80 transition-colors text-center"
1405 <Icon name="person_add" size={32} className="mx-auto mb-2" />
1406 <div className="text-sm font-semibold">New Account</div>
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
1417 <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
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"
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>
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"
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>
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
1441 <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
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"
1446 <div className="text-3xl mb-2">📄</div>
1447 <div className="text-sm font-semibold text-text">View Statements</div>
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"
1454 <div className="text-3xl mb-2">📝</div>
1455 <div className="text-sm font-semibold text-text">Run Statements</div>