feat: finance page overhaul, file access fix for clients and subcontractors

- Finance (TeamInvoices): chart + stat cards, Overview/Expenses/Subcontractors/Invoices/Legacy tabs
- Expenses tab: 70/30 card split with expense list + category breakdown, + Expense button
- Overview tab: 3 cards dynamic height filling window, .md-compliant table headers and sticky heads
- TaskDetail FileBrowser: role-based virtual paths so clients and subcontractors no longer get 403

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Krao Hasanee
2026-06-01 10:48:40 -04:00
parent 70ad8d0cef
commit ac3f08122d
8 changed files with 401 additions and 91 deletions
+290 -36
View File
@@ -30,7 +30,7 @@ const poStatusLabel = {
cancelled: 'Cancelled',
};
const RECEIPT_BUCKET = 'expense-receipts';
const FIELD_LABEL_STYLE = { fontSize: 11, fontWeight: 400, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 0.5, display: 'block', marginBottom: 4 };
const FIELD_LABEL_STYLE = { fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: 0.8, display: 'block', marginBottom: 4 };
const FIELD_INPUT_STYLE = { minHeight: 42, margin: 0 };
const blankExpense = () => ({
@@ -71,10 +71,14 @@ export default function Invoices() {
const [invoices, setInvoices] = useState(() => cached?.invoices || []);
const [loading, setLoading] = useState(() => !cached);
const [activeTab, setActiveTab] = useState(() => location.state?.tab || 'invoices');
const [financeTab, setFinanceTab] = useState('overview');
const [filter, setFilter] = useState('all');
const { sortKey: invSortKey, sortDir: invSortDir, toggle: invToggle, sort: invSort } = useSortable('invoice_date');
const { sortKey: expSortKey, sortDir: expSortDir, toggle: expToggle, sort: expSort } = useSortable('date');
const { sortKey: subSortKey, sortDir: subSortDir, toggle: subToggle, sort: subSort } = useSortable('submitted_at');
const { sortKey: ovExpKey, sortDir: ovExpDir, toggle: ovExpToggle } = useSortable('date');
const { sortKey: ovSubKey, sortDir: ovSubDir, toggle: ovSubToggle } = useSortable('date');
const { sortKey: ovInvKey, sortDir: ovInvDir, toggle: ovInvToggle } = useSortable('invoice_date');
const [filterCompany, setFilterCompany] = useState('');
const [exportYear, setExportYear] = useState(new Date().getFullYear());
const [exporting, setExporting] = useState(false);
@@ -498,46 +502,294 @@ export default function Invoices() {
return (
<Layout>
<div className="page-header" style={{ flexShrink: 0 }}>
<div className="page-header-left">
<DashboardBanner />
<div className="page-title dashboard-greeting">Finances</div>
<div className="page-subtitle">{invoices.length} invoice{invoices.length !== 1 ? 's' : ''} · {expenses.length} expense{expenses.length !== 1 ? 's' : ''} · {subcontractorPOs.length} subcontractor PO{subcontractorPOs.length !== 1 ? 's' : ''}</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{paidYears.length > 0 && (
<select className="filter-select" value={exportYear} onChange={e => setExportYear(Number(e.target.value))} style={{ width: 120 }}>
{paidYears.map(y => <option key={y} value={y}>{y}</option>)}
</select>
<div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
{/* Top row: 60% chart + 40% stat cards */}
{(() => {
const yr = chartData.reduce((s, m) => s + m.Revenue, 0);
const yp = chartData.reduce((s, m) => s + m.Profit, 0);
const ye = chartData.reduce((s, m) => s + m.Expenses, 0);
const yo = chartData.reduce((s, m) => s + m.Outstanding, 0);
const fmt = v => v >= 1000 ? `$${(v/1000).toFixed(1)}k` : `$${v.toFixed(0)}`;
const CARD = { background: 'var(--card-bg)', border: '1px solid var(--border)', borderRadius: 8, padding: '18px 21px', backdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)' };
const StatCard = ({ label, value, sub, iconBg, iconColor, iconSvg }) => (
<div style={{ ...CARD, display: 'flex', alignItems: 'stretch', gap: 21, minHeight: 120 }}>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
<div style={{ fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)', letterSpacing: 0.8, textTransform: 'uppercase', marginBottom: 5 }}>{label}</div>
<div style={{ flex: 1, display: 'flex', alignItems: 'center' }}>
<div style={{ fontSize: 30, fontWeight: 400, color: 'var(--text-primary)', letterSpacing: -0.5, lineHeight: 1.1 }}>{value}</div>
</div>
{sub && <div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 5 }}>{sub}</div>}
</div>
<div style={{ flexShrink: 0, display: 'flex', flexDirection: 'column', alignItems: 'flex-end', justifyContent: 'flex-start' }}>
<div style={{ width: 27, height: 27, borderRadius: '50%', background: iconBg, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke={iconColor} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">{iconSvg}</svg>
</div>
</div>
</div>
);
return (
<div style={{ display: 'grid', gridTemplateColumns: '3fr 2fr', gap: 24, flexShrink: 0 }}>
<div style={{ ...CARD }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
<span style={{ fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)', letterSpacing: 0.8, textTransform: 'uppercase' }}>Revenue Overview</span>
<select className="filter-select" value={chartYear} onChange={e => setChartYear(Number(e.target.value))} style={{ width: 90, borderRadius: 8, fontSize: 11, fontWeight: 500 }}>
{(paidYears.length > 0 ? paidYears : [new Date().getFullYear()]).map(y => <option key={y} value={y}>{y}</option>)}
</select>
</div>
<ResponsiveContainer width="100%" height={160}>
<AreaChart data={chartData} margin={{ top: 4, right: 8, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="gc2Revenue" x1="0" y1="0" x2="0" y2="1"><stop offset="5%" stopColor="#F5A523" stopOpacity={0.25}/><stop offset="95%" stopColor="#F5A523" stopOpacity={0}/></linearGradient>
<linearGradient id="gc2Profit" x1="0" y1="0" x2="0" y2="1"><stop offset="5%" stopColor="#4ade80" stopOpacity={0.25}/><stop offset="95%" stopColor="#4ade80" stopOpacity={0}/></linearGradient>
<linearGradient id="gc2Expenses" x1="0" y1="0" x2="0" y2="1"><stop offset="5%" stopColor="#ef4444" stopOpacity={0.2}/><stop offset="95%" stopColor="#ef4444" stopOpacity={0}/></linearGradient>
<linearGradient id="gc2Outstanding" x1="0" y1="0" x2="0" y2="1"><stop offset="5%" stopColor="#60a5fa" stopOpacity={0.2}/><stop offset="95%" stopColor="#60a5fa" stopOpacity={0}/></linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" vertical={false} />
<XAxis dataKey="month" tick={{ fontSize: 11, fill: 'var(--text-muted)' }} axisLine={false} tickLine={false} />
<YAxis tick={{ fontSize: 11, fill: 'var(--text-muted)' }} axisLine={false} tickLine={false} tickFormatter={v => `$${v >= 1000 ? (v/1000).toFixed(0)+'k' : v}`} width={45} />
<Tooltip content={<FinancesChartTooltip year={chartYear} />} />
<Legend wrapperStyle={{ fontSize: 11, paddingTop: 8 }} />
<Area type="monotone" dataKey="Revenue" stroke="#F5A523" strokeWidth={2} fill="url(#gc2Revenue)" dot={false} activeDot={{ r: 4 }} />
<Area type="monotone" dataKey="Profit" stroke="#4ade80" strokeWidth={2} fill="url(#gc2Profit)" dot={false} activeDot={{ r: 4 }} />
<Area type="monotone" dataKey="Expenses" stroke="#ef4444" strokeWidth={2} fill="url(#gc2Expenses)" dot={false} activeDot={{ r: 4 }} />
<Area type="monotone" dataKey="Outstanding" stroke="#60a5fa" strokeWidth={2} fill="url(#gc2Outstanding)" dot={false} activeDot={{ r: 4 }} />
</AreaChart>
</ResponsiveContainer>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24, alignContent: 'start' }}>
<StatCard label="Revenue" value={fmt(yr)} sub={`${chartYear} paid`} iconBg="rgba(245,165,35,0.15)" iconColor="#F5A523" iconSvg={<><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5,12 12,5 19,12"/></>} />
<StatCard label="Outstanding" value={fmt(yo)} sub="sent invoices" iconBg="rgba(96,165,250,0.15)" iconColor="#60a5fa" iconSvg={<><circle cx="12" cy="12" r="8"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></>} />
<StatCard label="Expenses" value={fmt(ye)} sub="total spend" iconBg="rgba(239,68,68,0.15)" iconColor="#ef4444" iconSvg={<><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19,12 12,19 5,12"/></>} />
<StatCard label="Net Profit" value={fmt(yp)} sub="after expenses" iconBg="rgba(74,222,128,0.15)" iconColor="#4ade80" iconSvg={<><polyline points="4,13 9,18 20,7"/></>} />
</div>
</div>
);
})()}
<div style={{ display: 'grid', gridTemplateColumns: '7fr 3fr', gap: 24, marginTop: 24, marginBottom: 10, flexShrink: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
{[
{ id: 'overview', label: 'Overview' },
{ id: 'expenses', label: 'Expenses' },
{ id: 'subcontractors', label: 'Subcontractors' },
{ id: 'invoices', label: 'Invoices' },
{ id: 'legacy', label: 'Legacy' },
].map(t => (
<button
key={t.id}
type="button"
onClick={() => setFinanceTab(t.id)}
style={{
fontFamily: "Fourge, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
fontSize: 13, fontWeight: 500, letterSpacing: 0.2,
padding: '2px 0 5px', margin: '0 8px',
borderRadius: 0, border: 'none',
borderBottom: financeTab === t.id ? '2px solid var(--accent)' : '2px solid transparent',
cursor: 'pointer', background: 'transparent',
color: financeTab === t.id ? 'var(--text-primary)' : 'var(--text-secondary)',
transition: 'all 160ms',
}}
>{t.label}</button>
))}
{financeTab === 'expenses' && (
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 8 }}>
<button className="btn btn-outline" onClick={() => { setEditingExpenseId(''); setNewExpense(blankExpense()); setExpensesError(''); setShowExpenseForm(true); }}>+ Expense</button>
</div>
)}
<button className="btn btn-primary btn-sm" onClick={handleCPAExport} disabled={exporting}>
{exporting ? 'Exporting…' : 'Export for CPA'}
</button>
</div>
<div />
</div>
<div style={{ background: 'var(--card-bg)', border: '1px solid var(--border)', borderRadius: 4, padding: '16px 16px 8px', marginBottom: 18 }}>
<ResponsiveContainer width="100%" height={200}>
<AreaChart data={chartData} margin={{ top: 4, right: 8, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="gradRevenue" x1="0" y1="0" x2="0" y2="1"><stop offset="5%" stopColor="#F5A523" stopOpacity={0.25}/><stop offset="95%" stopColor="#F5A523" stopOpacity={0}/></linearGradient>
<linearGradient id="gradProfit" x1="0" y1="0" x2="0" y2="1"><stop offset="5%" stopColor="#4ade80" stopOpacity={0.25}/><stop offset="95%" stopColor="#4ade80" stopOpacity={0}/></linearGradient>
<linearGradient id="gradExpenses" x1="0" y1="0" x2="0" y2="1"><stop offset="5%" stopColor="#ef4444" stopOpacity={0.2}/><stop offset="95%" stopColor="#ef4444" stopOpacity={0}/></linearGradient>
<linearGradient id="gradOutstanding" x1="0" y1="0" x2="0" y2="1"><stop offset="5%" stopColor="#60a5fa" stopOpacity={0.2}/><stop offset="95%" stopColor="#60a5fa" stopOpacity={0}/></linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" vertical={false} />
<XAxis dataKey="month" tick={{ fontSize: 11, fill: 'var(--text-muted)' }} axisLine={false} tickLine={false} />
<YAxis tick={{ fontSize: 11, fill: 'var(--text-muted)' }} axisLine={false} tickLine={false} tickFormatter={v => `$${v >= 1000 ? (v/1000).toFixed(0)+'k' : v}`} width={45} />
<Tooltip content={<FinancesChartTooltip year={chartYear} />} />
<Legend wrapperStyle={{ fontSize: 11, paddingTop: 8 }} />
<Area type="monotone" dataKey="Revenue" stroke="#F5A523" strokeWidth={2} fill="url(#gradRevenue)" dot={false} activeDot={{ r: 4 }} />
<Area type="monotone" dataKey="Profit" stroke="#4ade80" strokeWidth={2} fill="url(#gradProfit)" dot={false} activeDot={{ r: 4 }} />
<Area type="monotone" dataKey="Expenses" stroke="#ef4444" strokeWidth={2} fill="url(#gradExpenses)" dot={false} activeDot={{ r: 4 }} />
<Area type="monotone" dataKey="Outstanding" stroke="#60a5fa" strokeWidth={2} fill="url(#gradOutstanding)" dot={false} activeDot={{ r: 4 }} />
</AreaChart>
</ResponsiveContainer>
</div>
{financeTab === 'overview' && (() => {
const CARD = { background: 'var(--card-bg)', border: '1px solid var(--border)', borderRadius: 8, padding: '18px 21px', backdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)' };
const TH = { fontSize: 10, fontWeight: 500, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 0.6, padding: '0 0 12px 0', border: 'none', background: 'transparent', textAlign: 'left' };
const TD = { fontSize: 13, color: 'var(--text-primary)', padding: '5px 0', border: 'none', background: 'transparent', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
const sortRows = (arr, key, dir, getter) => [...arr].sort((a, b) => {
const av = getter(a, key), bv = getter(b, key);
if (av < bv) return dir === 'asc' ? -1 : 1;
if (av > bv) return dir === 'asc' ? 1 : -1;
return 0;
});
const recentExp = sortRows(expenses, ovExpKey, ovExpDir, (e, k) => {
if (k === 'description') return (e.description || '').toLowerCase();
if (k === 'category') return (e.category || '').toLowerCase();
if (k === 'amount') return Number(e.amount) || 0;
return e.date || '';
}).slice(0, 5);
const recentSub = sortRows(subcontractorPOs, ovSubKey, ovSubDir, (po, k) => {
if (k === 'name') return (po.profile?.name || '').toLowerCase();
if (k === 'project') return (po.project?.name || '').toLowerCase();
if (k === 'amount') return Number(po.amount) || 0;
return po.paid_at || po.date || '';
}).slice(0, 5);
const recentInv = sortRows(invoices, ovInvKey, ovInvDir, (inv, k) => {
if (k === 'company') return (inv.company?.name || '').toLowerCase();
if (k === 'total') return Number(inv.total) || 0;
return inv.invoice_date || '';
}).slice(0, 5);
const fmtDate = d => d ? new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : '—';
const fmtAmt = v => `$${Number(v).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
return (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 24, flex: 1, minHeight: 0 }}>
{/* Recent Expenses */}
<div style={{ ...CARD, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
<div style={{ fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)', letterSpacing: 0.8, textTransform: 'uppercase', marginBottom: 14, flexShrink: 0 }}>Recent Expenses</div>
{recentExp.length === 0 ? <div className="card-empty-center">No expenses</div> : (
<div className="scrollbar-thin-theme table-scroll-hidden" style={{ flex: 1, minHeight: 0, overflowY: 'auto' }}>
<table className="table-sticky-head" style={{ width: '100%', borderCollapse: 'collapse', tableLayout: 'fixed' }}>
<colgroup><col style={{ width: '45%' }}/><col style={{ width: '30%' }}/><col style={{ width: '25%' }}/></colgroup>
<thead><tr>
<SortTh col="description" sortKey={ovExpKey} sortDir={ovExpDir} onSort={ovExpToggle} style={TH}>Description</SortTh>
<SortTh col="category" sortKey={ovExpKey} sortDir={ovExpDir} onSort={ovExpToggle} style={{ ...TH, textAlign: 'center' }}>Category</SortTh>
<SortTh col="amount" sortKey={ovExpKey} sortDir={ovExpDir} onSort={ovExpToggle} style={{ ...TH, textAlign: 'right' }}>Amount</SortTh>
</tr></thead>
<tbody>{recentExp.map(e => (
<tr key={e.id}>
<td style={{ ...TD }}>{e.description || '—'}</td>
<td style={{ ...TD, fontSize: 12, color: 'var(--text-muted)', textAlign: 'center' }}>{e.category || '—'}</td>
<td style={{ ...TD, textAlign: 'right', color: '#ef4444' }}>{fmtAmt(e.amount)}</td>
</tr>
))}</tbody>
</table>
</div>
)}
</div>
{/* Recent Subcontractor Payments */}
<div style={{ ...CARD, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
<div style={{ fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)', letterSpacing: 0.8, textTransform: 'uppercase', marginBottom: 14, flexShrink: 0 }}>Recent Subcontractor Payments</div>
{recentSub.length === 0 ? <div className="card-empty-center">No payments</div> : (
<div className="scrollbar-thin-theme table-scroll-hidden" style={{ flex: 1, minHeight: 0, overflowY: 'auto' }}>
<table className="table-sticky-head" style={{ width: '100%', borderCollapse: 'collapse', tableLayout: 'fixed' }}>
<colgroup><col style={{ width: '35%' }}/><col style={{ width: '35%' }}/><col style={{ width: '30%' }}/></colgroup>
<thead><tr>
<SortTh col="name" sortKey={ovSubKey} sortDir={ovSubDir} onSort={ovSubToggle} style={TH}>Name</SortTh>
<SortTh col="project" sortKey={ovSubKey} sortDir={ovSubDir} onSort={ovSubToggle} style={{ ...TH, textAlign: 'center' }}>Project</SortTh>
<SortTh col="amount" sortKey={ovSubKey} sortDir={ovSubDir} onSort={ovSubToggle} style={{ ...TH, textAlign: 'right' }}>Amount</SortTh>
</tr></thead>
<tbody>{recentSub.map(po => (
<tr key={po.id}>
<td style={{ ...TD }}>{po.profile?.name || '—'}</td>
<td style={{ ...TD, fontSize: 12, color: 'var(--text-muted)', textAlign: 'center' }}>{po.project?.name || '—'}</td>
<td style={{ ...TD, textAlign: 'right', color: '#F5A523' }}>{fmtAmt(po.amount)}</td>
</tr>
))}</tbody>
</table>
</div>
)}
</div>
{/* Recent Invoices */}
<div style={{ ...CARD, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
<div style={{ fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)', letterSpacing: 0.8, textTransform: 'uppercase', marginBottom: 14, flexShrink: 0 }}>Recent Invoices</div>
{recentInv.length === 0 ? <div className="card-empty-center">No invoices</div> : (
<div className="scrollbar-thin-theme table-scroll-hidden" style={{ flex: 1, minHeight: 0, overflowY: 'auto' }}>
<table className="table-sticky-head" style={{ width: '100%', borderCollapse: 'collapse', tableLayout: 'fixed' }}>
<colgroup><col style={{ width: '40%' }}/><col style={{ width: '25%' }}/><col style={{ width: '35%' }}/></colgroup>
<thead><tr>
<SortTh col="company" sortKey={ovInvKey} sortDir={ovInvDir} onSort={ovInvToggle} style={TH}>Company</SortTh>
<SortTh col="invoice_date" sortKey={ovInvKey} sortDir={ovInvDir} onSort={ovInvToggle} style={{ ...TH, textAlign: 'center' }}>Date</SortTh>
<SortTh col="total" sortKey={ovInvKey} sortDir={ovInvDir} onSort={ovInvToggle} style={{ ...TH, textAlign: 'right' }}>Total</SortTh>
</tr></thead>
<tbody>{recentInv.map(inv => (
<tr key={inv.id}>
<td style={{ ...TD }}>{inv.company?.name || '—'}</td>
<td style={{ ...TD, fontSize: 12, color: 'var(--text-muted)', textAlign: 'center' }}>{fmtDate(inv.invoice_date)}</td>
<td style={{ ...TD, textAlign: 'right', color: '#4ade80' }}>{fmtAmt(inv.total)}</td>
</tr>
))}</tbody>
</table>
</div>
)}
</div>
</div>
);
})()}
{financeTab === 'expenses' && (() => {
const CARD = { background: 'var(--card-bg)', border: '1px solid var(--border)', borderRadius: 8, padding: '18px 21px', backdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)' };
const TH = { fontSize: 10, fontWeight: 500, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 0.6, padding: '0 0 12px 0', border: 'none', background: 'transparent', textAlign: 'left' };
const TD = { fontSize: 13, color: 'var(--text-primary)', padding: '5px 0', border: 'none', background: 'transparent', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
const fmtDate = d => d ? new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—';
const fmtAmt = v => `$${Number(v).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
const sortedExpenses = expSort(expenses, (exp, key) => {
if (key === 'amount') return Number(exp.amount) || 0;
if (key === 'date') return exp.date || '';
return exp[key] || '';
});
const categoryTotals = CATEGORIES
.map(cat => ({ cat, total: expenses.filter(e => e.category === cat).reduce((s, e) => s + Number(e.amount || 0), 0) }))
.filter(x => x.total > 0)
.sort((a, b) => b.total - a.total);
const grandTotal = expenses.reduce((s, e) => s + Number(e.amount || 0), 0);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 0, flex: 1, minHeight: 0 }}>
<div style={{ display: 'grid', gridTemplateColumns: '7fr 3fr', gap: 24, flex: 1, minHeight: 0 }}>
{/* Left: expense list */}
<div style={{ ...CARD, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
<div style={{ fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)', letterSpacing: 0.8, textTransform: 'uppercase', marginBottom: 14, flexShrink: 0 }}>Expenses</div>
{expenses.length === 0 ? <div className="card-empty-center">No expenses</div> : (
<div className="scrollbar-thin-theme table-scroll-hidden" style={{ flex: 1, minHeight: 0, overflowY: 'auto' }}>
<table className="table-sticky-head" style={{ width: '100%', borderCollapse: 'collapse', tableLayout: 'fixed' }}>
<colgroup>
<col style={{ width: '14%' }} />
<col style={{ width: '34%' }} />
<col style={{ width: '16%' }} />
<col style={{ width: '24%' }} />
<col style={{ width: '12%' }} />
</colgroup>
<thead><tr>
<SortTh col="date" sortKey={expSortKey} sortDir={expSortDir} onSort={expToggle} style={TH}>Date</SortTh>
<SortTh col="description" sortKey={expSortKey} sortDir={expSortDir} onSort={expToggle} style={TH}>Description</SortTh>
<SortTh col="category" sortKey={expSortKey} sortDir={expSortDir} onSort={expToggle} style={{ ...TH, textAlign: 'center' }}>Category</SortTh>
<SortTh col="notes" sortKey={expSortKey} sortDir={expSortDir} onSort={expToggle} style={TH}>Notes</SortTh>
<SortTh col="amount" sortKey={expSortKey} sortDir={expSortDir} onSort={expToggle} style={{ ...TH, textAlign: 'right' }}>Amount</SortTh>
</tr></thead>
<tbody>{sortedExpenses.map(exp => (
<tr key={exp.id} style={{ cursor: 'pointer' }} onClick={() => startEditExpense(exp)}>
<td style={{ ...TD, fontSize: 12, color: 'var(--text-muted)' }}>{fmtDate(exp.date)}</td>
<td style={{ ...TD }}>{exp.description || '—'}</td>
<td style={{ ...TD, fontSize: 12, color: 'var(--text-muted)', textAlign: 'center' }}>{exp.category || '—'}</td>
<td style={{ ...TD, fontSize: 12, color: 'var(--text-muted)' }}>{exp.notes || '—'}</td>
<td style={{ ...TD, textAlign: 'right', color: '#ef4444' }}>{fmtAmt(exp.amount)}</td>
</tr>
))}</tbody>
</table>
</div>
)}
</div>
{/* Right: category breakdown */}
<div style={{ ...CARD, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
<div style={{ fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)', letterSpacing: 0.8, textTransform: 'uppercase', marginBottom: 14, flexShrink: 0 }}>By Category</div>
{categoryTotals.length === 0 ? <div className="card-empty-center">No expenses</div> : (
<div className="scrollbar-thin-theme table-scroll-hidden" style={{ flex: 1, minHeight: 0, overflowY: 'auto' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{categoryTotals.map(({ cat, total }) => {
const pct = grandTotal > 0 ? (total / grandTotal) * 100 : 0;
return (
<div key={cat}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 5 }}>
<span style={{ fontSize: 12, color: 'var(--text-primary)' }}>{cat}</span>
<span style={{ fontSize: 12, color: '#ef4444', fontVariantNumeric: 'tabular-nums' }}>{fmtAmt(total)}</span>
</div>
<div style={{ height: 4, borderRadius: 2, background: 'var(--border)', overflow: 'hidden' }}>
<div style={{ height: '100%', width: `${pct}%`, background: '#ef4444', borderRadius: 2, opacity: 0.7, transition: 'width 400ms ease' }} />
</div>
</div>
);
})}
<div style={{ borderTop: '1px solid var(--border)', paddingTop: 12, display: 'flex', justifyContent: 'space-between', marginTop: 4 }}>
<span style={{ fontSize: 12, fontWeight: 500, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: 0.6 }}>Total</span>
<span style={{ fontSize: 13, fontWeight: 500, color: '#ef4444', fontVariantNumeric: 'tabular-nums' }}>{fmtAmt(grandTotal)}</span>
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
})()}
{financeTab === 'legacy' && <div>
<div style={{ display: 'flex', alignItems: 'center', gap: 0, marginBottom: 16 }}>
{[
{ id: 'invoices', label: 'INVOICES' },
@@ -886,7 +1138,9 @@ export default function Invoices() {
</div>
</div>
)}
</div>}{/* end legacy wrapper */}
</div>{/* end flex column wrapper */}
</Layout>
);
}