Asset & Liability Composition
{/* Assets bar */}
{assetBarData.length > 0 && (
Assets
{formatCurrency(Number(data.total_assets), data.currency)}
{assetBarData.map((seg) => (
0 ? "2px" : "0",
}}
title={`${seg.name}: ${formatCurrency(seg.value, data.currency)}`}
/>
))}
{assetBarData.map((seg) => (
))}
)}
{/* Liabilities bar */}
{liabilityBarData.length > 0 && (
Liabilities
{formatCurrency(Number(data.total_liabilities), data.currency)}
{liabilityBarData.map((seg) => (
0 ? "2px" : "0",
}}
title={`${seg.name}: ${formatCurrency(seg.value, data.currency)}`}
/>
))}
{liabilityBarData.map((seg) => (
))}
)}
{/* Side-by-side account breakdown */}
{/* Assets */}
Assets
{formatCurrency(Number(data.total_assets), data.currency)}
{data.asset_groups.map((group: BalanceSheetGroup, gi: number) => (
{group.label}
{formatCurrency(Number(group.subtotal), data.currency)}
{group.accounts.map((acc) => (
))}
))}
{/* Liabilities */}
Liabilities
{formatCurrency(Number(data.total_liabilities), data.currency)}
{data.liability_groups.length === 0 ? (
No liabilities — great work!
) : (
{data.liability_groups.map((group: BalanceSheetGroup, gi: number) => (
{group.label}
{formatCurrency(Number(group.subtotal), data.currency)}
{group.accounts.map((acc) => (
))}
))}
)}
);
}
function NetWorthTab() {
const { data, isLoading } = useQuery({ queryKey: ["report-net-worth"], queryFn: () => getNetWorthReport(12) });
if (isLoading) return
;
if (!data) return null;
return (
30d Change
= 0 ? "text-success" : "text-destructive")}>
{Number(data.change_30d_pct) >= 0 ? "+" : ""}{Number(data.change_30d_pct).toFixed(2)}%
History
{data.points.length} snapshot{data.points.length !== 1 ? "s" : ""}
{data.points.length === 0 ? (
) : (
Net Worth Over Time
({ ...p, net_worth: Number(p.net_worth), total_assets: Number(p.total_assets), total_liabilities: Number(p.total_liabilities) }))}>
`£${(v/1000).toFixed(0)}k`} />
formatCurrency(v, data.base_currency)} />
)}
);
}
function IncomeExpenseTab() {
const { data, isLoading } = useQuery({ queryKey: ["report-income-expense"], queryFn: () => getIncomeExpenseReport(12) });
if (isLoading) return
;
if (!data) return null;
const chartData = data.points.map(p => ({ ...p, income: Number(p.income), expenses: Number(p.expenses), net: Number(p.net) }));
return (
{chartData.length === 0 ?
: (
Monthly Income vs Expenses
`£${(v/1000).toFixed(0)}k`} />
formatCurrency(v, data.currency)} />
)}
);
}
function CategoriesTab() {
const { data, isLoading } = useQuery({ queryKey: ["report-categories"], queryFn: () => getCategoryBreakdown() });
if (isLoading) return
;
if (!data) return null;
const pieData = data.items.slice(0, 10).map(i => ({ name: i.category_name, value: Number(i.amount) }));
return (
Expense Breakdown — This Month
Total: {formatCurrency(Number(data.total), data.currency)}
{pieData.length === 0 ?
: (
{pieData.map((_, i) => | )}
formatCurrency(v, data.currency)} />
{data.items.slice(0, 10).map((item, i) => (
{item.category_name}
{formatCurrency(Number(item.amount), data.currency)}
{item.percent}%
))}
)}
);
}
function BudgetVsActualTab() {
const { data, isLoading } = useQuery({ queryKey: ["report-budget-actual"], queryFn: getBudgetVsActual });
if (isLoading) return
;
if (!data || data.items.length === 0) return
;
const chartData = data.items.map(i => ({
name: i.budget_name,
budgeted: Number(i.budgeted),
actual: Number(i.actual),
}));
return (
Budget vs Actual Spending
`£${v}`} />
formatCurrency(v, data.currency)} />
);
}
function SpendingTrendsTab() {
const { data, isLoading } = useQuery({ queryKey: ["report-spending-trends"], queryFn: () => getSpendingTrends(6) });
if (isLoading) return
;
if (!data || data.points.length === 0) return
;
const months = [...new Set(data.points.map(p => p.month))].sort();
const chartData = months.map(month => {
const row: Record
= { month };
data.categories.forEach(cat => {
const pt = data.points.find(p => p.month === month && p.category_name === cat);
row[cat] = pt ? Number(pt.amount) : 0;
});
return row;
});
return (
Spending by Category (6 months)
`£${v}`} />
formatCurrency(v, data.currency)} />
{data.categories.slice(0, 8).map((cat, i) => (
))}
);
}
function ChartSkeleton() {
return (
);
}
function EmptyChart({ message = "No data for this period" }: { message?: string }) {
return (
);
}
export default function ReportsPage() {
const [activeTab, setActiveTab] = useState("Balance Sheet");
return (
Reports
Financial insights and analysis
{TABS.map((tab) => (
))}
{activeTab === "Balance Sheet" && }
{activeTab === "Net Worth" && }
{activeTab === "Income vs Expense" && }
{activeTab === "Categories" && }
{activeTab === "Budget vs Actual" && }
{activeTab === "Spending Trends" && }
);
}