import { useState, useMemo, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useAuth } from '../../contexts/AuthContext';
import { AppLayout } from '../../components/layout/AppLayout';
import { LoadingSpinner } from '../../components/ui/LoadingSpinner';
import {
quickbooksApi,
type QuickbooksInvoice,
type QuickbooksTransaction,
type QuickbooksCustomer,
type QuickbooksExpense,
type InvoiceStatus,
type QuickbooksAnalytics,
} from '../../api/quickbooks';
import {
DollarSign,
FileText,
TrendingUp,
TrendingDown,
AlertTriangle,
AlertCircle,
CheckCircle2,
Users,
RefreshCw,
Search,
Filter,
X,
Calendar,
ArrowUpRight,
ArrowDownRight,
CreditCard,
ShoppingCart,
Package,
BarChart3,
} from 'lucide-react';
// โโโ Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function formatCurrency(value: number | null): string {
if (value == null) return 'โ';
if (Math.abs(value) >= 1_000_000) return `${value > 0 ? '+' : ''}$${(value / 1_000_000).toFixed(1)}M`;
if (Math.abs(value) >= 1_000) return `${value > 0 ? '+' : ''}$${(value / 1_000).toFixed(1)}K`;
return `$${value.toFixed(2)}`;
}
function statusColor(status: InvoiceStatus): string {
const map: Record<InvoiceStatus, string> = {
draft: 'bg-slate-100 text-slate-700',
sent: 'bg-blue-100 text-blue-700',
paid: 'bg-emerald-100 text-emerald-700',
partial: 'bg-amber-100 text-amber-700',
void: 'bg-red-100 text-red-700',
uncollectible: 'bg-gray-100 text-gray-500',
};
return map[status] || 'bg-gray-100 text-gray-600';
}
function statusLabel(status: InvoiceStatus): string {
return status.charAt(0).toUpperCase() + status.slice(1);
}
function timeAgo(dateStr: string | null | undefined): string {
if (!dateStr) return 'โ';
const now = Date.now();
const then = new Date(dateStr).getTime();
const diff = Math.abs(now - then);
const days = Math.floor(diff / 86400000);
if (days === 0) return 'today';
if (days === 1) return 'yesterday';
if (days < 30) return `${days}d ago`;
return new Date(dateStr).toLocaleDateString();
}
// โโโ Tab Types โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
type TabKey = 'overview' | 'invoices' | 'transactions' | 'customers' | 'expenses';
// โโโ Overview Tab โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function OverviewTab({ analytics }: { analytics: QuickbooksAnalytics }) {
const stats = [
{ label: 'Outstanding', value: formatCurrency(analytics.total_outstanding), icon: AlertTriangle, color: 'text-amber-600 bg-amber-50' },
{ label: 'Total Paid', value: formatCurrency(analytics.total_paid), icon: CheckCircle2, color: 'text-emerald-600 bg-emerald-50' },
{ label: 'Revenue (30d)', value: formatCurrency(analytics.revenue_last_30d), icon: TrendingUp, color: 'text-blue-600 bg-blue-50' },
{ label: 'Profit Margin', value: `${analytics.profit_margin.toFixed(1)}%`, icon: BarChart3, color: 'text-violet-600 bg-violet-50' },
{ label: 'Avg Invoice', value: formatCurrency(analytics.avg_invoice_amount), icon: FileText, color: 'text-slate-600 bg-slate-50' },
{ label: 'Collection Days', value: `${analytics.avg_collection_days.toFixed(0)}d`, icon: Calendar, color: 'text-cyan-600 bg-cyan-50' },
];
return (
<div className="space-y-6">
{/* Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
{stats.map((stat) => (
<div key={stat.label} className="rounded-xl border border-surface-200 bg-white p-4">
<div className={`inline-flex rounded-lg p-2 mb-2 ${stat.color}`}>
<stat.icon className="h-4 w-4" />
</div>
<p className="text-xs text-surface-500">{stat.label}</p>
<p className="text-lg font-bold text-surface-900 mt-0.5">{stat.value}</p>
</div>
))}
</div>
{/* Revenue & Expenses */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="rounded-xl border border-surface-200 bg-white p-6">
<h3 className="text-sm font-medium text-surface-500 mb-4">Revenue</h3>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-surface-600">Last 30 days</span>
<span className="font-semibold text-emerald-600">{formatCurrency(analytics.revenue_last_30d)}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-surface-600">Last 90 days</span>
<span className="font-semibold text-emerald-600">{formatCurrency(analytics.revenue_last_90d)}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-surface-600">Year to date</span>
<span className="font-semibold text-emerald-600">{formatCurrency(analytics.revenue_ytd)}</span>
</div>
</div>
</div>
<div className="rounded-xl border border-surface-200 bg-white p-6">
<h3 className="text-sm font-medium text-surface-500 mb-4">Expenses</h3>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-surface-600">Last 30 days</span>
<span className="font-semibold text-red-600">{formatCurrency(analytics.expenses_last_30d)}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-surface-600">Last 90 days</span>
<span className="font-semibold text-red-600">{formatCurrency(analytics.expenses_last_90d)}</span>
</div>
</div>
</div>
</div>
{/* Invoices by Status */}
{analytics.invoices_by_status && analytics.invoices_by_status.length > 0 && (
<div className="rounded-xl border border-surface-200 bg-white p-6">
<h3 className="text-sm font-medium text-surface-500 mb-4">Invoices by Status</h3>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
{analytics.invoices_by_status.map((item) => (
<div key={item.status} className="rounded-lg bg-surface-50 p-3">
<p className="text-xs text-surface-500 capitalize">{item.status}</p>
<p className="text-lg font-bold text-surface-900">{item.count}</p>
<p className="text-xs text-surface-400">{formatCurrency(item.total)}</p>
</div>
))}
</div>
</div>
)}
{/* Top Customers */}
{analytics.top_customers && analytics.top_customers.length > 0 && (
<div className="rounded-xl border border-surface-200 bg-white p-6">
<h3 className="text-sm font-medium text-surface-500 mb-4">Top Customers</h3>
<div className="space-y-2">
{analytics.top_customers.map((customer) => (
<div key={customer.name} className="flex justify-between items-center py-2 border-b border-surface-100 last:border-0">
<div>
<p className="text-sm font-medium text-surface-900">{customer.name}</p>
<p className="text-xs text-surface-400">{customer.invoice_count} invoices</p>
</div>
<span className="font-semibold text-emerald-600">{formatCurrency(customer.total_paid)}</span>
</div>
))}
</div>
</div>
)}
</div>
);
}
// โโโ Invoices Tab โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function InvoicesTab({ companyId }: { companyId: string }) {
const [search, setSearch] = useState('');
const [statusFilter, setStatusFilter] = useState('');
const [selectedInvoice, setSelectedInvoice] = useState<QuickbooksInvoice | null>(null);
const { data, isLoading, refetch } = useQuery({
queryKey: ['quickbooks-invoices', companyId, search, statusFilter],
queryFn: () =>
quickbooksApi.getInvoices(companyId, {
search: search || undefined,
status: statusFilter || undefined,
}),
staleTime: 60_000,
});
const invoices = data?.data?.items || [];
return (
<div>
{/* Filters */}
<div className="flex flex-wrap items-center gap-3 mb-4">
<div className="relative flex-1 min-w-[200px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-surface-400" />
<input
type="text"
placeholder="Search invoices..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full rounded-lg border border-surface-300 bg-white pl-10 pr-3 py-2 text-sm outline-none focus:border-brand-500"
/>
</div>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="rounded-lg border border-surface-300 bg-white px-3 py-2 text-sm outline-none focus:border-brand-500"
>
<option value="">All Statuses</option>
<option value="draft">Draft</option>
<option value="sent">Sent</option>
<option value="paid">Paid</option>
<option value="partial">Partial</option>
<option value="void">Void</option>
</select>
</div>
{/* Table */}
{isLoading ? (
<div className="flex justify-center py-12"><LoadingSpinner /></div>
) : (
<div className="overflow-x-auto rounded-xl border border-surface-200">
<table className="w-full text-sm">
<thead>
<tr className="bg-surface-50">
<th className="text-left px-4 py-3 font-medium text-surface-500">Invoice</th>
<th className="text-left px-4 py-3 font-medium text-surface-500">Customer</th>
<th className="text-right px-4 py-3 font-medium text-surface-500">Amount</th>
<th className="text-left px-4 py-3 font-medium text-surface-500">Status</th>
<th className="text-left px-4 py-3 font-medium text-surface-500">Due</th>
</tr>
</thead>
<tbody>
{invoices.length === 0 ? (
<tr><td colSpan={5} className="text-center py-8 text-surface-400">No invoices found</td></tr>
) : (
invoices.map((inv) => (
<tr
key={inv.id}
onClick={() => setSelectedInvoice(inv)}
className="border-t border-surface-100 hover:bg-surface-50 cursor-pointer"
>
<td className="px-4 py-3 font-medium text-surface-900">{inv.invoice_number}</td>
<td className="px-4 py-3 text-surface-600">{inv.customer_name}</td>
<td className="px-4 py-3 text-right font-medium">{formatCurrency(inv.total_amount)}</td>
<td className="px-4 py-3">
<span className={`inline-block rounded-full px-2 py-0.5 text-xs font-semibold ${statusColor(inv.status)}`}>
{statusLabel(inv.status)}
</span>
</td>
<td className="px-4 py-3 text-surface-500">{timeAgo(inv.due_date)}</td>
</tr>
))
)}
</tbody>
</table>
</div>
)}
{/* Detail Panel */}
{selectedInvoice && (
<div className="fixed inset-0 z-50 flex justify-end" role="dialog" aria-modal="true">
<div className="absolute inset-0 bg-black/30" onClick={() => setSelectedInvoice(null)} />
<div className="relative z-10 flex h-full w-full max-w-lg flex-col overflow-y-auto bg-white shadow-xl border-l border-surface-200">
<div className="flex items-center justify-between border-b border-surface-200 px-6 py-4">
<div>
<h2 className="text-lg font-semibold text-surface-900">{selectedInvoice.invoice_number}</h2>
<p className="text-sm text-surface-500">{selectedInvoice.customer_name}</p>
</div>
<button
onClick={() => setSelectedInvoice(null)}
className="rounded-lg p-2 text-surface-400 hover:bg-surface-100 min-h-[44px] min-w-[44px] flex items-center justify-center"
>
<X className="h-5 w-5" />
</button>
</div>
<div className="grid grid-cols-2 gap-3 border-b border-surface-200 px-6 py-4">
<div className="rounded-lg bg-surface-50 p-3">
<p className="text-xs text-surface-500">Total</p>
<p className="text-lg font-bold text-surface-900">{formatCurrency(selectedInvoice.total_amount)}</p>
</div>
<div className="rounded-lg bg-surface-50 p-3">
<p className="text-xs text-surface-500">Status</p>
<span className={`inline-block mt-1 rounded-full px-2 py-0.5 text-xs font-semibold ${statusColor(selectedInvoice.status)}`}>
{statusLabel(selectedInvoice.status)}
</span>
</div>
<div className="rounded-lg bg-surface-50 p-3">
<p className="text-xs text-surface-500">Subtotal</p>
<p className="text-sm font-medium">{formatCurrency(selectedInvoice.subtotal)}</p>
</div>
<div className="rounded-lg bg-surface-50 p-3">
<p className="text-xs text-surface-500">Tax</p>
<p className="text-sm font-medium">{formatCurrency(selectedInvoice.tax_amount)}</p>
</div>
</div>
<div className="border-b border-surface-200 px-6 py-4">
<p className="text-xs font-medium text-surface-500 uppercase tracking-wide mb-2">Dates</p>
<div className="space-y-1 text-sm text-surface-600">
<div className="flex justify-between"><span>Issued</span><span>{timeAgo(selectedInvoice.issue_date)}</span></div>
<div className="flex justify-between"><span>Due</span><span>{timeAgo(selectedInvoice.due_date)}</span></div>
</div>
</div>
{selectedInvoice.line_items && selectedInvoice.line_items.length > 0 && (
<div className="border-b border-surface-200 px-6 py-4">
<p className="text-xs font-medium text-surface-500 uppercase tracking-wide mb-2">Line Items</p>
<div className="space-y-2">
{selectedInvoice.line_items.map((item, i) => (
<div key={i} className="flex justify-between text-sm">
<span className="text-surface-600">{item.description}</span>
<span className="font-medium">{formatCurrency(item.amount)}</span>
</div>
))}
</div>
</div>
)}
{selectedInvoice.notes && (
<div className="px-6 py-4">
<p className="text-xs font-medium text-surface-500 uppercase tracking-wide mb-2">Notes</p>
<p className="text-sm text-surface-600">{selectedInvoice.notes}</p>
</div>
)}
</div>
</div>
)}
</div>
);
}
// โโโ Transactions Tab โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function TransactionsTab({ companyId }: { companyId: string }) {
const [search, setSearch] = useState('');
const [typeFilter, setTypeFilter] = useState('');
const { data, isLoading } = useQuery({
queryKey: ['quickbooks-transactions', companyId, search, typeFilter],
queryFn: () =>
quickbooksApi.getTransactions(companyId, {
type: typeFilter || undefined,
}),
staleTime: 60_000,
});
const transactions = data?.data?.items || [];
return (
<div>
<div className="flex flex-wrap items-center gap-3 mb-4">
<div className="relative flex-1 min-w-[200px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-surface-400" />
<input
type="text"
placeholder="Search transactions..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full rounded-lg border border-surface-300 bg-white pl-10 pr-3 py-2 text-sm outline-none focus:border-brand-500"
/>
</div>
<select
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
className="rounded-lg border border-surface-300 bg-white px-3 py-2 text-sm outline-none focus:border-brand-500"
>
<option value="">All Types</option>
<option value="sale">Sale</option>
<option value="payment">Payment</option>
<option value="refund">Refund</option>
<option value="credit">Credit</option>
<option value="debit">Debit</option>
</select>
</div>
{isLoading ? (
<div className="flex justify-center py-12"><LoadingSpinner /></div>
) : (
<div className="space-y-2">
{transactions.length === 0 ? (
<div className="text-center py-8 text-surface-400">No transactions found</div>
) : (
transactions.map((tx) => (
<div key={tx.id} className="flex items-center justify-between rounded-lg border border-surface-200 bg-white p-4">
<div className="flex items-center gap-3">
{tx.amount >= 0 ? (
<div className="rounded-full bg-emerald-100 p-2">
<ArrowUpRight className="h-4 w-4 text-emerald-600" />
</div>
) : (
<div className="rounded-full bg-red-100 p-2">
<ArrowDownRight className="h-4 w-4 text-red-600" />
</div>
)}
<div>
<p className="text-sm font-medium text-surface-900">{tx.description}</p>
<p className="text-xs text-surface-400">
{timeAgo(tx.transaction_date)} ยท {tx.transaction_type}
{tx.account_name ? ` ยท ${tx.account_name}` : ''}
</p>
</div>
</div>
<span className={`font-semibold ${tx.amount >= 0 ? 'text-emerald-600' : 'text-red-600'}`}>
{tx.amount >= 0 ? '+' : ''}{formatCurrency(tx.amount)}
</span>
</div>
))
)}
</div>
)}
</div>
);
}
// โโโ Customers Tab โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function CustomersTab({ companyId }: { companyId: string }) {
const [search, setSearch] = useState('');
const { data, isLoading } = useQuery({
queryKey: ['quickbooks-customers', companyId, search],
queryFn: () =>
quickbooksApi.getCustomers(companyId, {
search: search || undefined,
}),
staleTime: 60_000,
});
const customers = data?.data?.items || [];
return (
<div>
<div className="flex flex-wrap items-center gap-3 mb-4">
<div className="relative flex-1 min-w-[200px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-surface-400" />
<input
type="text"
placeholder="Search customers..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full rounded-lg border border-surface-300 bg-white pl-10 pr-3 py-2 text-sm outline-none focus:border-brand-500"
/>
</div>
</div>
{isLoading ? (
<div className="flex justify-center py-12"><LoadingSpinner /></div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{customers.length === 0 ? (
<div className="col-span-full text-center py-8 text-surface-400">No customers found</div>
) : (
customers.map((customer) => (
<div key={customer.id} className="rounded-xl border border-surface-200 bg-white p-5">
<div className="flex items-start justify-between mb-3">
<div>
<h3 className="text-sm font-semibold text-surface-900">{customer.name}</h3>
{customer.email && <p className="text-xs text-surface-400 mt-0.5">{customer.email}</p>}
</div>
<div className="rounded-full bg-blue-100 p-2">
<Users className="h-4 w-4 text-blue-600" />
</div>
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="rounded-lg bg-surface-50 p-2">
<p className="text-surface-400">Balance</p>
<p className="font-medium">{formatCurrency(customer.balance)}</p>
</div>
<div className="rounded-lg bg-surface-50 p-2">
<p className="text-surface-400">Invoices</p>
<p className="font-medium">{customer.total_invoices}</p>
</div>
<div className="rounded-lg bg-surface-50 p-2 col-span-2">
<p className="text-surface-400">Total Paid</p>
<p className="font-medium text-emerald-600">{formatCurrency(customer.total_paid)}</p>
</div>
</div>
</div>
))
)}
</div>
)}
</div>
);
}
// โโโ Expenses Tab โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function ExpensesTab({ companyId }: { companyId: string }) {
const [search, setSearch] = useState('');
const [categoryFilter, setCategoryFilter] = useState('');
const { data, isLoading } = useQuery({
queryKey: ['quickbooks-expenses', companyId, search, categoryFilter],
queryFn: () =>
quickbooksApi.getExpenses(companyId, {
category: categoryFilter || undefined,
}),
staleTime: 60_000,
});
const expenses = data?.data?.items || [];
return (
<div>
<div className="flex flex-wrap items-center gap-3 mb-4">
<div className="relative flex-1 min-w-[200px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-surface-400" />
<input
type="text"
placeholder="Search expenses..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full rounded-lg border border-surface-300 bg-white pl-10 pr-3 py-2 text-sm outline-none focus:border-brand-500"
/>
</div>
<select
value={categoryFilter}
onChange={(e) => setCategoryFilter(e.target.value)}
className="rounded-lg border border-surface-300 bg-white px-3 py-2 text-sm outline-none focus:border-brand-500"
>
<option value="">All Categories</option>
<option value="supplies">Supplies</option>
<option value="labor">Labor</option>
<option value="equipment">Equipment</option>
<option value="marketing">Marketing</option>
<option value="overhead">Overhead</option>
</select>
</div>
{isLoading ? (
<div className="flex justify-center py-12"><LoadingSpinner /></div>
) : (
<div className="overflow-x-auto rounded-xl border border-surface-200">
<table className="w-full text-sm">
<thead>
<tr className="bg-surface-50">
<th className="text-left px-4 py-3 font-medium text-surface-500">Vendor</th>
<th className="text-left px-4 py-3 font-medium text-surface-500">Description</th>
<th className="text-left px-4 py-3 font-medium text-surface-500">Category</th>
<th className="text-right px-4 py-3 font-medium text-surface-500">Amount</th>
<th className="text-left px-4 py-3 font-medium text-surface-500">Date</th>
</tr>
</thead>
<tbody>
{expenses.length === 0 ? (
<tr><td colSpan={5} className="text-center py-8 text-surface-400">No expenses found</td></tr>
) : (
expenses.map((exp) => (
<tr key={exp.id} className="border-t border-surface-100">
<td className="px-4 py-3 font-medium text-surface-900">{exp.vendor_name}</td>
<td className="px-4 py-3 text-surface-600 max-w-[200px] truncate">{exp.description}</td>
<td className="px-4 py-3">
<span className="inline-block rounded-full bg-surface-100 px-2 py-0.5 text-xs font-medium text-surface-600 capitalize">
{exp.category}
</span>
</td>
<td className="px-4 py-3 text-right font-medium text-red-600">{formatCurrency(exp.amount)}</td>
<td className="px-4 py-3 text-surface-500">{timeAgo(exp.expense_date)}</td>
</tr>
))
)}
</tbody>
</table>
</div>
)}
</div>
);
}
// โโโ Main Page โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
export function QuickBooksPage() {
const { user } = useAuth();
const companyId = user?.tenantId as string;
const [activeTab, setActiveTab] = useState<TabKey>('overview');
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null);
const queryClient = useQueryClient();
useEffect(() => {
if (toast) {
const timer = setTimeout(() => setToast(null), 4000);
return () => clearTimeout(timer);
}
}, [toast]);
const { data: analyticsData, isLoading: analyticsLoading } = useQuery({
queryKey: ['quickbooks-analytics', companyId],
queryFn: () => quickbooksApi.getAnalytics(companyId),
staleTime: 120_000,
});
const syncMutation = useMutation({
mutationFn: () => quickbooksApi.sync(companyId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['quickbooks'] });
setToast({ message: 'QuickBooks synced successfully', type: 'success' });
},
onError: (err: unknown) => {
const message = (err as { response?: { data?: { error?: string } } })?.response?.data?.error || 'QuickBooks sync failed';
setToast({ message, type: 'error' });
},
});
const tabs: { key: TabKey; label: string; icon: typeof Search }[] = [
{ key: 'overview', label: 'Overview', icon: BarChart3 },
{ key: 'invoices', label: 'Invoices', icon: FileText },
{ key: 'transactions', label: 'Transactions', icon: CreditCard },
{ key: 'customers', label: 'Customers', icon: Users },
{ key: 'expenses', label: 'Expenses', icon: ShoppingCart },
];
return (
<AppLayout>
{/* Toast */}
{toast && (
<div className="fixed top-4 right-4 z-[70] flex items-center gap-2 rounded-lg bg-surface-900 px-4 py-3 text-sm text-white shadow-lg">
{toast.type === 'success' ? (
<CheckCircle2 className="h-4 w-4 text-green-400" />
) : (
<AlertCircle className="h-4 w-4 text-red-400" />
)}
{toast.message}
</div>
)}
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-surface-900">QuickBooks</h1>
<p className="text-sm text-surface-500 mt-1">Financial data synced from QuickBooks Online</p>
</div>
<button
onClick={() => syncMutation.mutate()}
disabled={syncMutation.isPending}
className="inline-flex items-center gap-2 rounded-lg bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700 disabled:opacity-50 min-h-[44px] self-start"
>
<RefreshCw className={`h-4 w-4 ${syncMutation.isPending ? 'animate-spin' : ''}`} />
{syncMutation.isPending ? 'Syncing...' : 'Sync Now'}
</button>
</div>
{/* Tabs */}
<div className="flex gap-1 overflow-x-auto border-b border-surface-200">
{tabs.map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap min-h-[44px] ${
activeTab === tab.key
? 'border-brand-600 text-brand-600'
: 'border-transparent text-surface-500 hover:text-surface-700'
}`}
>
<tab.icon className="h-4 w-4" />
{tab.label}
</button>
))}
</div>
{/* Content */}
{activeTab === 'overview' ? (
analyticsLoading ? (
<div className="flex justify-center py-12"><LoadingSpinner /></div>
) : analyticsData?.data ? (
<OverviewTab analytics={analyticsData.data} />
) : (
<div className="rounded-xl border border-surface-200 bg-white p-12 text-center">
<DollarSign className="h-12 w-12 text-surface-300 mx-auto mb-4" />
<h3 className="text-lg font-medium text-surface-700 mb-2">No QuickBooks data</h3>
<p className="text-sm text-surface-500 mb-4">Connect your QuickBooks account to see your financial data here.</p>
<button
onClick={() => syncMutation.mutate()}
disabled={syncMutation.isPending}
className="inline-flex items-center gap-2 rounded-lg bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700 disabled:opacity-50 min-h-[44px]"
>
<RefreshCw className="h-4 w-4" />
Sync Now
</button>
</div>
)
) : activeTab === 'invoices' ? (
<InvoicesTab companyId={companyId} />
) : activeTab === 'transactions' ? (
<TransactionsTab companyId={companyId} />
) : activeTab === 'customers' ? (
<CustomersTab companyId={companyId} />
) : (
<ExpensesTab companyId={companyId} />
)}
</div>
</AppLayout>
);
}