import { useState, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { AppLayout } from '../../components/layout/AppLayout';
import { quickbooksApi, type CustomersResponse } from '../../api/quickbooks';
import { LoadingSpinner } from '../../components/ui/LoadingSpinner';
import {
Search,
RefreshCw,
AlertTriangle,
Users,
Mail,
Phone,
MapPin,
ChevronDown,
ChevronUp,
Building2,
ExternalLink,
} from 'lucide-react';
// โโโ Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function formatCurrency(value: number): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(value);
}
// โโโ Main Page โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
export function QuickBooksCustomersPage() {
const [search, setSearch] = useState('');
const [sortBy, setSortBy] = useState<'display_name' | 'balance'>('display_name');
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
const { data, isLoading, error, refetch } = useQuery<CustomersResponse>({
queryKey: ['quickbooks-customers', search],
queryFn: () => quickbooksApi.getCustomers({ search: search || undefined }),
staleTime: 1 * 60 * 1000,
});
const customers = useMemo(() => {
let items = data?.customers || [];
return [...items].sort((a, b) => {
let cmp = 0;
if (sortBy === 'balance') {
cmp = a.balance - b.balance;
} else {
cmp = a.display_name.localeCompare(b.display_name);
}
return sortDir === 'desc' ? -cmp : cmp;
});
}, [data, sortBy, sortDir]);
const totalBalance = customers.reduce((sum, c) => sum + c.balance, 0);
const avgBalance = customers.length > 0 ? totalBalance / customers.length : 0;
const handleSort = (field: typeof sortBy) => {
if (sortBy === field) {
setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
} else {
setSortBy(field);
setSortDir(field === 'display_name' ? 'asc' : 'desc');
}
};
const SortIcon = ({ field }: { field: typeof sortBy }) => {
if (sortBy !== field) return <ChevronDown className="h-3 w-3 text-surface-300" />;
return sortDir === 'desc' ? <ChevronDown className="h-3 w-3" /> : <ChevronUp className="h-3 w-3" />;
};
if (isLoading) {
return (
<AppLayout title="QuickBooks Customers">
<div className="flex items-center justify-center py-20">
<LoadingSpinner size="lg" />
</div>
</AppLayout>
);
}
if (error) {
return (
<AppLayout title="QuickBooks Customers">
<div className="flex flex-col items-center justify-center py-20 text-center">
<AlertTriangle className="h-12 w-12 text-amber-400" />
<h3 className="mt-4 text-lg font-semibold text-surface-900">Failed to Load Customers</h3>
<p className="mt-1 text-sm text-surface-500">
{error instanceof Error ? error.message : 'Could not load customer data.'}
</p>
<button
onClick={() => refetch()}
className="mt-4 flex items-center gap-2 rounded-lg bg-brand-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-700 min-h-[44px]"
>
<RefreshCw className="h-4 w-4" />
Try Again
</button>
</div>
</AppLayout>
);
}
return (
<AppLayout title="QuickBooks Customers">
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-xl font-bold text-surface-900">Customers</h1>
<p className="text-sm text-surface-500">
{customers.length} customers ยท Total balance: {formatCurrency(totalBalance)}
</p>
</div>
<button
onClick={() => refetch()}
className="flex items-center gap-1.5 rounded-lg border border-surface-200 bg-white px-3 py-2 text-sm font-medium text-surface-700 hover:bg-surface-50 min-h-[44px]"
>
<RefreshCw className="h-4 w-4" />
Refresh
</button>
</div>
{/* Summary */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div className="rounded-xl border border-surface-200 bg-white p-4">
<div className="flex items-center gap-2">
<Users className="h-5 w-5 text-blue-500" />
<span className="text-sm font-medium text-surface-600">Total Customers</span>
</div>
<p className="mt-1 text-2xl font-bold text-surface-900">{customers.length}</p>
</div>
<div className="rounded-xl border border-surface-200 bg-white p-4">
<div className="flex items-center gap-2">
<Building2 className="h-5 w-5 text-amber-500" />
<span className="text-sm font-medium text-surface-600">Total Balance</span>
</div>
<p className="mt-1 text-2xl font-bold text-amber-600">{formatCurrency(totalBalance)}</p>
</div>
<div className="rounded-xl border border-surface-200 bg-white p-4">
<div className="flex items-center gap-2">
<ExternalLink className="h-5 w-5 text-indigo-500" />
<span className="text-sm font-medium text-surface-600">Avg Balance</span>
</div>
<p className="mt-1 text-2xl font-bold text-indigo-600">{formatCurrency(avgBalance)}</p>
</div>
</div>
{/* Search */}
<div className="rounded-xl border border-surface-200 bg-white p-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-surface-400" />
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search by name, email, phone..."
className="w-full rounded-lg border border-surface-200 bg-white pl-9 pr-3 py-2 text-sm outline-none focus:border-brand-500 min-h-[44px]"
/>
</div>
</div>
{/* Table */}
<div className="overflow-x-auto rounded-xl border border-surface-200 bg-white">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-surface-200 bg-surface-50">
<th
className="cursor-pointer px-4 py-3 text-left text-xs font-medium text-surface-500 hover:text-surface-700"
onClick={() => handleSort('display_name')}
>
<div className="flex items-center gap-1">
Customer Name
<SortIcon field="display_name" />
</div>
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-surface-500">Email</th>
<th className="px-4 py-3 text-left text-xs font-medium text-surface-500">Phone</th>
<th className="px-4 py-3 text-left text-xs font-medium text-surface-500">Address</th>
<th
className="cursor-pointer px-4 py-3 text-right text-xs font-medium text-surface-500 hover:text-surface-700"
onClick={() => handleSort('balance')}
>
<div className="flex items-center justify-end gap-1">
Balance
<SortIcon field="balance" />
</div>
</th>
</tr>
</thead>
<tbody>
{customers.length === 0 ? (
<tr>
<td colSpan={5} className="px-4 py-12 text-center text-surface-400">
No customers found
</td>
</tr>
) : (
customers.map((customer) => {
const addr = customer.billing_address as
| { Line1?: string; City?: string; CountrySubDivisionCode?: string; PostalCode?: string }
| null;
const addressStr = addr
? [addr.Line1, addr.City, addr.CountrySubDivisionCode, addr.PostalCode]
.filter(Boolean)
.join(', ') || 'โ'
: 'โ';
return (
<tr
key={customer.id}
className="border-b border-surface-100 hover:bg-surface-50 last:border-b-0"
>
<td className="px-4 py-3 font-medium text-surface-900">{customer.display_name || 'โ'}</td>
<td className="px-4 py-3">
{customer.email ? (
<a
href={`mailto:${customer.email}`}
className="flex items-center gap-1.5 text-sm text-brand-600 hover:text-brand-700 min-h-[44px]"
>
<Mail className="h-3.5 w-3.5" />
{customer.email}
</a>
) : (
<span className="text-surface-400">โ</span>
)}
</td>
<td className="px-4 py-3">
{customer.phone ? (
<a
href={`tel:${customer.phone}`}
className="flex items-center gap-1.5 text-sm text-surface-600 hover:text-brand-600 min-h-[44px]"
>
<Phone className="h-3.5 w-3.5" />
{customer.phone}
</a>
) : (
<span className="text-surface-400">โ</span>
)}
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-1.5 text-sm text-surface-600">
<MapPin className="h-3.5 w-3.5 shrink-0" />
<span className="truncate max-w-[200px]">{addressStr}</span>
</div>
</td>
<td
className={`px-4 py-3 text-right font-medium ${
customer.balance > 0 ? 'text-amber-600' : customer.balance < 0 ? 'text-emerald-600' : 'text-surface-600'
}`}
>
{formatCurrency(customer.balance)}
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
</AppLayout>
);
}