"use client"; import { useMemo, useState } from "react"; import { ComposedChart, Bar, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, } from "recharts"; import { useMonthlyAnalytics, useSubscriptions, useFees, useTransactions, useUpdateTransaction } from "@/lib/hooks"; import { CATEGORIES, REGULAR_CATEGORIES, formatCategory } from "@/lib/categories"; const SPEND_TYPES = new Set(["debit", "fee", "interest"]); function fmt(n: number) { return new Intl.NumberFormat("en-AU", { style: "currency", currency: "AUD", maximumFractionDigits: 0 }).format(n); } function fmtTx(amount: number, type: string) { const formatted = new Intl.NumberFormat("en-AU", { style: "currency", currency: "AUD", minimumFractionDigits: 2 }).format(amount); return SPEND_TYPES.has(type) ? formatted : `+${formatted}`; } function fmtExact(n: number) { return new Intl.NumberFormat("en-AU", { style: "currency", currency: "AUD", minimumFractionDigits: 2 }).format(n); } function fmtDate(d: string) { return new Date(d).toLocaleDateString("en-AU", { month: "short", year: "numeric" }); } function trend(values: number[]): { pct: number; dir: "up" | "down" | "flat" } { if (values.length < 2) return { pct: 0, dir: "flat" }; const recent = values.slice(-3).reduce((a, b) => a + b, 0) / 3; const prior = values.slice(0, 3).reduce((a, b) => a + b, 0) / 3; if (prior === 0) return { pct: 0, dir: "flat" }; const pct = Math.round(((recent - prior) / prior) * 100); return { pct: Math.abs(pct), dir: pct > 2 ? "up" : pct < -2 ? "down" : "flat" }; } const FREQ_LABEL: Record = { weekly: "Weekly", fortnightly: "Fortnightly", monthly: "Monthly", quarterly: "Quarterly", annual: "Annual", }; // ─── Section wrapper ──────────────────────────────────────────────── function Section({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
); } // ─── Custom tooltip ────────────────────────────────────────────────── function RegularTooltip({ active, payload, label }: any) { if (!active || !payload?.length) return null; const regular = payload.find((p: any) => p.dataKey === "regular")?.value ?? 0; const occasional = payload.find((p: any) => p.dataKey === "occasional")?.value ?? 0; return (
{label}
Regular{fmt(regular)}
Occasional{fmt(occasional)}
Total{fmt(regular + occasional)}
); } // ─── Drill-down row ────────────────────────────────────────────────── function DrillDownRow({ category, from, to, }: { category: string; from: string; to: string; }) { const { data, isLoading } = useTransactions({ categories: category ? [category] : [], from, to, limit: 200 }); const updateTx = useUpdateTransaction(); if (isLoading) { return ( Loading... ); } const txns = data?.data ?? []; return (
{txns.length === 0 ? (

No transactions found.

) : ( {txns.map((t) => { const sharePct = t.my_share_percent ?? 100; const effectiveAmt = t.amount * sharePct / 100; const isDebit = SPEND_TYPES.has(t.transaction_type); return ( ); })}
Date Merchant My share Category % mine
{new Date(t.transaction_date).toLocaleDateString("en-AU", { day: "2-digit", month: "short" })} {t.merchant_name || t.description} {fmtTx(effectiveAmt, t.transaction_type)} {sharePct < 100 && ( {fmtExact(t.amount)} )}
)}
); } // ─── Monthly Spend Breakdown ───────────────────────────────────────── function MonthlyBreakdown({ analytics }: { analytics: NonNullable["data"]> }) { // analytics.months is newest-first; show last 6 const months = useMemo(() => analytics.months.slice(0, 6), [analytics.months]); const [selectedMonth, setSelectedMonth] = useState(months[0] ?? ""); const [expandedCategory, setExpandedCategory] = useState(null); // Reset expanded when month changes const handleSelectMonth = (m: string) => { setSelectedMonth(m); setExpandedCategory(null); }; const from = selectedMonth + "-01"; const lastDay = new Date(parseInt(selectedMonth.slice(0, 4)), parseInt(selectedMonth.slice(5, 7)), 0).getDate(); const to = selectedMonth + "-" + String(lastDay).padStart(2, "0"); const categoryData = useMemo(() => { return analytics.rows .map((row) => ({ category: row.category, amount: Number(row.spent[selectedMonth] ?? 0) })) .filter((r) => r.amount > 0) .sort((a, b) => b.amount - a.amount); }, [analytics.rows, selectedMonth]); const regularRows = categoryData.filter((r) => REGULAR_CATEGORIES.has(r.category as any)); const occasionalRows = categoryData.filter((r) => !REGULAR_CATEGORIES.has(r.category as any)); const regularTotal = regularRows.reduce((s, r) => s + r.amount, 0); const occasionalTotal = occasionalRows.reduce((s, r) => s + r.amount, 0); function monthLabel(m: string) { const [year, month] = m.split("-"); return new Date(parseInt(year), parseInt(month) - 1).toLocaleDateString("en-AU", { month: "short", year: "2-digit" }); } function renderRows(rows: typeof categoryData, dotClass: string) { return rows.map((row) => ( <> setExpandedCategory(expandedCategory === row.category ? null : row.category)} > {formatCategory(row.category)} {expandedCategory === row.category ? "▲" : "▼"} {fmt(row.amount)} {expandedCategory === row.category && ( )} )); } return (
{/* Month tabs */}
{months.map((m) => ( ))}
{regularRows.length > 0 && ( <> {renderRows(regularRows, "bg-indigo-500")} )} {occasionalRows.length > 0 && ( <> {renderRows(occasionalRows, "bg-zinc-500")} )} {categoryData.length === 0 && ( )}
Category Spend
Regular {fmt(regularTotal)}
Occasional {fmt(occasionalTotal)}
No spend data for this month.
); } // ─── Main page ──────────────────────────────────────────────────────── export default function InsightsPage() { const { data: analytics } = useMonthlyAnalytics(12); const { data: analytics6 } = useMonthlyAnalytics(6); const { data: subData } = useSubscriptions(); const { data: feesData } = useFees(); // Build regular/occasional chart data const chartData = useMemo(() => { if (!analytics) return []; return [...analytics.months].reverse().map((month) => { let regular = 0; let occasional = 0; for (const row of analytics.rows) { const spend = Number(row.spent[month] ?? 0); if (REGULAR_CATEGORIES.has(row.category as any)) regular += spend; else occasional += spend; } return { month: month.slice(5) + "/" + month.slice(2, 4), regular: Math.round(regular), occasional: Math.round(occasional), total: Math.round(regular + occasional), }; }); }, [analytics]); const regularValues = chartData.map((d) => d.regular); const regularTrend = trend(regularValues); const avgRegular = regularValues.length ? Math.round(regularValues.reduce((a, b) => a + b, 0) / regularValues.length) : 0; const latestRegular = regularValues[regularValues.length - 1] ?? 0; const activeSubscriptions = subData?.subscriptions.filter((s) => s.is_active) ?? []; const inactiveSubscriptions = subData?.subscriptions.filter((s) => !s.is_active) ?? []; return (

Insights

{/* ── 1. Regular vs Occasional ── */}
This month — regular spend
{fmt(latestRegular)}
12-month avg regular
{fmt(avgRegular)}
Trend (first 3 vs last 3 mo)
{regularTrend.dir === "up" ? "↑" : regularTrend.dir === "down" ? "↓" : "→"} {regularTrend.pct}%
`$${(v / 1000).toFixed(0)}k`} width={44} /> } />
Regular (groceries, dining, transport…) Occasional
{/* ── 2. Monthly Spend Breakdown ── */}
{!analytics6 ? (

Loading...

) : ( )}
{/* ── 3. Recurring Charges ── */}
{!subData ? (

Loading...

) : subData.subscriptions.length === 0 ? (

No recurring patterns detected yet — more transaction history needed.

) : ( <>
{activeSubscriptions.length} active · {inactiveSubscriptions.length} inactive {fmtExact(subData.total_monthly_equiv)}/ month committed
{[...activeSubscriptions, ...inactiveSubscriptions].map((s) => ( ))}
Merchant Category Frequency My $/mo equiv Avg charge Since Total paid Count
{s.merchant} {formatCategory(s.category ?? "other")} {FREQ_LABEL[s.frequency] ?? s.frequency} {fmtExact(s.monthly_equiv)} {fmtExact(s.avg_amount)} {fmtDate(s.first_seen)} {fmtExact(s.total_paid)} {s.occurrences}
)}
{/* ── 4. Fees & Interest ── */}
{!feesData ? (

Loading...

) : feesData.by_bank.length === 0 && feesData.transactions.length === 0 ? (

No fees or interest recorded across your statements.

) : (
{feesData.by_bank.length > 0 && (
{feesData.by_bank.map((r) => ( ))}
Bank Fees Interest Total
{r.bank_name} {r.fees > 0 ? fmtExact(r.fees) : } {r.interest > 0 ? fmtExact(r.interest) : } {fmtExact(r.total)}
Total {fmtExact(feesData.total_fees)} {fmtExact(feesData.total_interest)} {fmtExact(feesData.total_fees + feesData.total_interest)}
)} {feesData.transactions.length > 0 && (

Individual fee / interest transactions

{feesData.transactions.map((t) => ( ))}
Date Bank Description Type Amount
{new Date(t.transaction_date).toLocaleDateString("en-AU", { day: "2-digit", month: "short", year: "numeric" })} {t.bank_name} {t.description} {t.transaction_type} {fmtExact(t.my_amount)}
)}
)}
); }