import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { getNetWorthReport, getIncomeExpenseReport, getCategoryBreakdown, getBudgetVsActual, getSpendingTrends, getBalanceSheet, } from "@/api/reports"; import type { BalanceSheetGroup } from "@/api/reports"; import { formatCurrency } from "@/utils/currency"; import { cn } from "@/utils/cn"; import { AreaChart, Area, BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts"; import { TrendingUp, TrendingDown, Minus, Landmark, CreditCard } from "lucide-react"; const TABS = ["Balance Sheet", "Net Worth", "Income vs Expense", "Categories", "Budget vs Actual", "Spending Trends"] as const; type Tab = typeof TABS[number]; const COLORS = [ "#6366f1", "#22c55e", "#f97316", "#ec4899", "#14b8a6", "#f59e0b", "#8b5cf6", "#06b6d4", "#84cc16", "#ef4444", ]; const ASSET_COLORS = ["#22c55e", "#14b8a6", "#6366f1", "#8b5cf6", "#06b6d4", "#84cc16"]; const LIABILITY_COLORS = ["#ef4444", "#f97316", "#ec4899"]; function StatCard({ label, value, change, currency }: { label: string; value: number; change?: number; currency: string; }) { const positive = change !== undefined ? change >= 0 : undefined; return (

{label}

{formatCurrency(value, currency)}

{change !== undefined && (
{positive ? : } {positive ? "+" : ""}{formatCurrency(change, currency)} (30d)
)}
); } function GroupRow({ account, color }: { account: { name: string; type: string; balance: number; currency: string }; color: string }) { return (

{account.name}

{account.type.replace(/_/g, " ")}

{formatCurrency(account.balance, account.currency)}

); } function BalanceSheetTab() { const { data, isLoading } = useQuery({ queryKey: ["report-balance-sheet"], queryFn: getBalanceSheet }); if (isLoading) return ; if (!data) return null; const noAccounts = data.asset_groups.length === 0 && data.liability_groups.length === 0; if (noAccounts) return ; // Build stacked bar data: one bar for assets, one for liabilities const assetBarData = data.asset_groups.map((g, i) => ({ name: g.label, value: Number(g.subtotal), color: ASSET_COLORS[i % ASSET_COLORS.length], })); const liabilityBarData = data.liability_groups.map((g, i) => ({ name: g.label, value: Number(g.subtotal), color: LIABILITY_COLORS[i % LIABILITY_COLORS.length], })); // Single stacked bar chart showing asset composition vs liability composition const maxVal = Math.max(Number(data.total_assets), Number(data.total_liabilities), 1); return (
{/* Summary KPIs */}

Total Assets

{formatCurrency(Number(data.total_assets), data.currency)}

Total Liabilities

{formatCurrency(Number(data.total_liabilities), data.currency)}

Net Worth

= 0 ? "text-primary" : "text-destructive")}> {formatCurrency(Number(data.net_worth), data.currency)}

{/* Visual proportion bars */}

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) => (
{seg.name}
))}
)} {/* 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) => (
{seg.name}
))}
)}
{/* 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 (
{[1, 2, 3].map(i =>
)}
); } function EmptyChart({ message = "No data for this period" }: { message?: string }) { return (

{message}

); } 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" && }
); }