import { useState, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useAuth } from '../../contexts/AuthContext';
import { AppLayout } from '../../components/layout/AppLayout';
import { quickbooksApi, type InvoicesResponse } from '../../api/quickbooks';
import { LoadingSpinner } from '../../components/ui/LoadingSpinner';
import {
Search,
Filter,
RefreshCw,
AlertTriangle,
FileText,
Calendar,
ChevronDown,
ChevronUp,
Download,
} from 'lucide-react';
// โโโ Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function formatCurrency(value: number | null | undefined): string {
if (value == null || value === 0) return '$0';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(value);
}
function formatDate(dateStr: string | null): string {
if (!dateStr) return 'โ';
return new Date(dateStr).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
}
function statusColor(status: string): string {
const s = status.toLowerCase();
if (s === 'paid') return 'bg-emerald-100 text-emerald-700';
if (s === 'void' || s === 'voided') return 'bg-gray-100 text-gray-500';
if (s === 'draft') return 'bg-slate-100 text-slate-600';
if (s === 'sent') return 'bg-blue-100 text-blue-700';
return 'bg-amber-100 text-amber-700';
}
// โโโ Main Page โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
export function QuickBooksInvoicesPage() {
const [search, setSearch] = useState('');
const [statusFilter, setStatusFilter] = useState('');
const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState('');
const [sortBy, setSortBy] = useState<'tx_date' | 'total_amount' | 'invoice_num'>('tx_date');
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc');
const { data, isLoading, error, refetch } = useQuery<InvoicesResponse>({
queryKey: ['quickbooks-invoices', search, statusFilter, dateFrom, dateTo],
queryFn: () =>
quickbooksApi.getInvoices({
status: statusFilter || undefined,
date_from: dateFrom || undefined,
date_to: dateTo || undefined,
search: search || undefined,
}),
staleTime: 1 * 60 * 1000,
});
const invoices = useMemo(() => {
let items = data?.invoices || [];
// Client-side sort
return [...items].sort((a, b) => {
let cmp = 0;
if (sortBy === 'tx_date') {
cmp = (a.tx_date || '').localeCompare(b.tx_date || '');
} else if (sortBy === 'total_amount') {
cmp = (a.total_amount ?? 0) - (b.total_amount ?? 0);
} else {
cmp = a.invoice_num.localeCompare(b.invoice_num);
}
return sortDir === 'desc' ? -cmp : cmp;
});
}, [data, sortBy, sortDir]);
const totalAmount = invoices.reduce((sum, inv) => sum + (inv.total_amount ?? 0), 0);
const paidCount = invoices.filter((inv) => inv.status.toLowerCase() === 'paid').length;
const paidAmount = invoices
.filter((inv) => inv.status.toLowerCase() === 'paid')
.reduce((sum, inv) => sum + (inv.total_amount ?? 0), 0);
const hasFilters = search || statusFilter || dateFrom || dateTo;
const handleSort = (field: typeof sortBy) => {
if (sortBy === field) {
setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
} else {
setSortBy(field);
setSortDir('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 Invoices">
<div className="flex items-center justify-center py-20">
<LoadingSpinner size="lg" />
</div>
</AppLayout>
);
}
if (error) {
return (
<AppLayout title="QuickBooks Invoices">
<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 Invoices</h3>
<p className="mt-1 text-sm text-surface-500">
{error instanceof Error ? error.message : 'Could not load invoice 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 Invoices">
<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">Invoices</h1>
<p className="text-sm text-surface-500">
{invoices.length} invoices ยท Total: {formatCurrency(totalAmount)}
</p>
</div>
<div className="flex items-center gap-2">
<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>
</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">
<FileText className="h-5 w-5 text-blue-500" />
<span className="text-sm font-medium text-surface-600">Total Invoices</span>
</div>
<p className="mt-1 text-2xl font-bold text-surface-900">{invoices.length}</p>
</div>
<div className="rounded-xl border border-surface-200 bg-white p-4">
<div className="flex items-center gap-2">
<Calendar className="h-5 w-5 text-emerald-500" />
<span className="text-sm font-medium text-surface-600">Paid</span>
</div>
<p className="mt-1 text-2xl font-bold text-emerald-600">{formatCurrency(paidAmount)}</p>
<p className="text-xs text-surface-400">{paidCount} invoices paid</p>
</div>
<div className="rounded-xl border border-surface-200 bg-white p-4">
<div className="flex items-center gap-2">
<Download className="h-5 w-5 text-amber-500" />
<span className="text-sm font-medium text-surface-600">Outstanding</span>
</div>
<p className="mt-1 text-2xl font-bold text-amber-600">
{formatCurrency(totalAmount - paidAmount)}
</p>
<p className="text-xs text-surface-400">{invoices.length - paidCount} invoices outstanding</p>
</div>
</div>
{/* Filters */}
<div className="rounded-xl border border-surface-200 bg-white p-4">
<div className="flex items-center gap-2 mb-3">
<Filter className="h-4 w-4 text-surface-400" />
<span className="text-sm font-medium text-surface-700">Filters</span>
{hasFilters && (
<button
onClick={() => {
setSearch('');
setStatusFilter('');
setDateFrom('');
setDateTo('');
}}
className="text-xs text-brand-600 hover:text-brand-700 min-h-[44px]"
>
Clear all
</button>
)}
</div>
<div className="flex flex-wrap gap-3">
<div className="flex-1 min-w-[200px]">
<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 invoice #, customer..."
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>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="rounded-lg border border-surface-200 bg-white px-3 py-2 text-sm font-medium text-surface-700 min-h-[44px]"
>
<option value="">All Statuses</option>
<option value="Draft">Draft</option>
<option value="Sent">Sent</option>
<option value="Paid">Paid</option>
<option value="Void">Void</option>
</select>
<input
type="date"
value={dateFrom}
onChange={(e) => setDateFrom(e.target.value)}
className="rounded-lg border border-surface-200 bg-white px-3 py-2 text-sm font-medium text-surface-700 min-h-[44px]"
/>
<input
type="date"
value={dateTo}
onChange={(e) => setDateTo(e.target.value)}
className="rounded-lg border border-surface-200 bg-white px-3 py-2 text-sm font-medium text-surface-700 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('invoice_num')}
>
<div className="flex items-center gap-1">
Invoice #
<SortIcon field="invoice_num" />
</div>
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-surface-500">Customer</th>
<th
className="cursor-pointer px-4 py-3 text-right text-xs font-medium text-surface-500 hover:text-surface-700"
onClick={() => handleSort('total_amount')}
>
<div className="flex items-center justify-end gap-1">
Amount
<SortIcon field="total_amount" />
</div>
</th>
<th className="px-4 py-3 text-center text-xs font-medium text-surface-500">Status</th>
<th
className="cursor-pointer px-4 py-3 text-right text-xs font-medium text-surface-500 hover:text-surface-700"
onClick={() => handleSort('tx_date')}
>
<div className="flex items-center justify-end gap-1">
Date
<SortIcon field="tx_date" />
</div>
</th>
<th className="px-4 py-3 text-right text-xs font-medium text-surface-500">Due Date</th>
</tr>
</thead>
<tbody>
{invoices.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-12 text-center text-surface-400">
No invoices found
</td>
</tr>
) : (
invoices.map((inv) => (
<tr
key={inv.id}
className="border-b border-surface-100 hover:bg-surface-50 last:border-b-0"
>
<td className="px-4 py-3 font-mono text-sm font-medium text-brand-600">
{inv.invoice_num}
</td>
<td className="px-4 py-3 text-surface-700">{inv.customer_name || 'โ'}</td>
<td className="px-4 py-3 text-right font-medium text-surface-900">
{formatCurrency(inv.total_amount)}
</td>
<td className="px-4 py-3 text-center">
<span className={`inline-block rounded-full px-2 py-0.5 text-xs font-semibold ${statusColor(inv.status)}`}>
{inv.status || 'โ'}
</span>
</td>
<td className="px-4 py-3 text-right text-surface-600">{formatDate(inv.tx_date)}</td>
<td className="px-4 py-3 text-right text-surface-600">{formatDate(inv.due_date)}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</AppLayout>
);
}