import { useState, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query';
import { AppLayout } from '../../components/layout/AppLayout';
import { quickbooksApi, type TransactionsResponse } from '../../api/quickbooks';
import { LoadingSpinner } from '../../components/ui/LoadingSpinner';
import {
Search,
Filter,
RefreshCw,
AlertTriangle,
ChevronDown,
ChevronUp,
ArrowUpCircle,
ArrowDownCircle,
FileText,
CreditCard,
ReceiptText,
} from 'lucide-react';
// ─── Helpers ──────────────────────────────────────────────────────────────
function formatCurrency(value: number | null | undefined): string {
if (value == null || value === 0) return '$0.00';
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 txTypeIcon(type: string) {
const t = type.toLowerCase();
if (t.includes('invoice')) return <ArrowUpCircle className="h-4 w-4 text-emerald-500" />;
if (t.includes('payment') || t.includes('journal')) return <CreditCard className="h-4 w-4 text-blue-500" />;
if (t.includes('credit') || t.includes('refund')) return <ArrowDownCircle className="h-4 w-4 text-amber-500" />;
return <ReceiptText className="h-4 w-4 text-surface-400" />;
}
function txTypeLabel(type: string): string {
return type || 'Unknown';
}
// ─── Main Page ────────────────────────────────────────────────────────────
export function QuickBooksTransactionsPage() {
const [search, setSearch] = useState('');
const [typeFilter, setTypeFilter] = useState('');
const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState('');
const [sortBy, setSortBy] = useState<'tx_date' | 'amount'>('tx_date');
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc');
const { data, isLoading, error, refetch } = useQuery<TransactionsResponse>({
queryKey: ['quickbooks-transactions', search, typeFilter, dateFrom, dateTo],
queryFn: () =>
quickbooksApi.getTransactions({
tx_type: typeFilter || undefined,
date_from: dateFrom || undefined,
date_to: dateTo || undefined,
search: search || undefined,
}),
staleTime: 1 * 60 * 1000,
});
const transactions = useMemo(() => {
let items = data?.transactions || [];
return [...items].sort((a, b) => {
let cmp = 0;
if (sortBy === 'amount') {
cmp = (a.amount ?? 0) - (b.amount ?? 0);
} else {
cmp = (a.tx_date || '').localeCompare(b.tx_date || '');
}
return sortDir === 'desc' ? -cmp : cmp;
});
}, [data, sortBy, sortDir]);
// Collect unique transaction types from data
const txTypes = useMemo(() => {
const types = new Set<string>();
(data?.transactions || []).forEach((tx) => {
if (tx.tx_type) types.add(tx.tx_type);
});
return Array.from(types).sort();
}, [data]);
const totalCredits = transactions
.filter((tx) => (tx.amount ?? 0) > 0)
.reduce((sum, tx) => sum + (tx.amount ?? 0), 0);
const totalDebits = Math.abs(
transactions
.filter((tx) => (tx.amount ?? 0) < 0)
.reduce((sum, tx) => sum + (tx.amount ?? 0), 0)
);
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 Transactions">
<div className="flex items-center justify-center py-20">
<LoadingSpinner size="lg" />
</div>
</AppLayout>
);
}
if (error) {
return (
<AppLayout title="QuickBooks Transactions">
<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 Transactions</h3>
<p className="mt-1 text-sm text-surface-500">
{error instanceof Error ? error.message : 'Could not load transaction 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 Transactions">
<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">Transactions</h1>
<p className="text-sm text-surface-500">
{transactions.length} transactions
</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">
<FileText className="h-5 w-5 text-blue-500" />
<span className="text-sm font-medium text-surface-600">Total Transactions</span>
</div>
<p className="mt-1 text-2xl font-bold text-surface-900">{transactions.length}</p>
</div>
<div className="rounded-xl border border-surface-200 bg-white p-4">
<div className="flex items-center gap-2">
<ArrowUpCircle className="h-5 w-5 text-emerald-500" />
<span className="text-sm font-medium text-surface-600">Credits</span>
</div>
<p className="mt-1 text-2xl font-bold text-emerald-600">{formatCurrency(totalCredits)}</p>
</div>
<div className="rounded-xl border border-surface-200 bg-white p-4">
<div className="flex items-center gap-2">
<ArrowDownCircle className="h-5 w-5 text-red-500" />
<span className="text-sm font-medium text-surface-600">Debits</span>
</div>
<p className="mt-1 text-2xl font-bold text-red-600">{formatCurrency(-totalDebits)}</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>
</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 by ID, account, memo..."
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={typeFilter}
onChange={(e) => setTypeFilter(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 Types</option>
{txTypes.map((t) => (
<option key={t} value={t}>{t}</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="px-4 py-3 text-left text-xs font-medium text-surface-500">Type</th>
<th className="px-4 py-3 text-left text-xs font-medium text-surface-500">Account</th>
<th className="px-4 py-3 text-left text-xs font-medium text-surface-500">Memo</th>
<th
className="cursor-pointer px-4 py-3 text-right text-xs font-medium text-surface-500 hover:text-surface-700"
onClick={() => handleSort('amount')}
>
<div className="flex items-center justify-end gap-1">
Amount
<SortIcon field="amount" />
</div>
</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>
</tr>
</thead>
<tbody>
{transactions.length === 0 ? (
<tr>
<td colSpan={5} className="px-4 py-12 text-center text-surface-400">
No transactions found
</td>
</tr>
) : (
transactions.map((tx) => (
<tr
key={tx.id}
className="border-b border-surface-100 hover:bg-surface-50 last:border-b-0"
>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
{txTypeIcon(tx.tx_type)}
<span className="font-medium text-surface-700">{txTypeLabel(tx.tx_type)}</span>
</div>
</td>
<td className="px-4 py-3 text-surface-600">{tx.account_name || '—'}</td>
<td className="px-4 py-3 max-w-[200px] truncate text-surface-500">{tx.memo || '—'}</td>
<td className={`px-4 py-3 text-right font-medium ${
(tx.amount ?? 0) >= 0 ? 'text-emerald-600' : 'text-red-600'
}`}>
{formatCurrency(tx.amount)}
</td>
<td className="px-4 py-3 text-right text-surface-600">{formatDate(tx.tx_date)}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</AppLayout>
);
}