import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { TimeSeriesChart } from "@/components/charts/TimeSeriesChart"; import { SegmentedControl } from "@/components/charts/SegmentedControl"; import { ChartTypeToggle } from "@/components/charts/ChartTypeToggle"; import { CalendarDays, LineChart } from "lucide-react"; import { useFinanceData } from "@/context/FinanceDataContext"; import { useMemo, useState } from "react"; import { cn } from "@/lib/utils"; import { toDateString } from "@/lib/format"; import { useMoney } from "@/hooks/useMoney"; import { AnimatedMoney } from "@/components/elements/AnimatedMoney"; import { accountDisplayName } from "@/lib/accounts"; const periods = [ { label: "30 days", days: 30 }, { label: "3 months", days: 90 }, { label: "6 months", days: 180 }, { label: "All time", days: 999 }, ]; const periodOptions = periods.map((p) => ({ value: p.label, label: p.label })); interface AccountBalanceChartProps { /** Accounts whose balances are summed into the chart. Empty = nothing selected. */ accountIds: string[]; /** Optional control rendered in the header (e.g. an account multi-select). */ control?: React.ReactNode; /** Transactions supplies the linked heatmap; account detail retains its line-only view. */ heatmap?: React.ReactNode; lineChart?: React.ReactNode; chartControls?: React.ReactNode; /** Optional companion chart, taking one fifth of the card on wide screens. */ sideChart?: React.ReactNode; chartType?: "line" | "heatmap"; onChartTypeChange?: (type: "line" | "heatmap") => void; /** True when every selected account's balance represents money owed * (credit cards, loans): plots the debt magnitude instead of the raw * (negative) balance, so the line rises as debt grows, in red — more debt * is unfavorable, the same convention Sure's credit card detail uses. */ debt?: boolean; } /** * Balance-over-time for a chosen set of accounts. Mirrors the dashboard * BalanceChart math (anchor the curve to today's real balance, back-compute * history from signed daily flow) but scoped to the selected accounts so each * bank account shows "how much money is in it" rather than just transactions. */ export function AccountBalanceChart({ accountIds, control, debt, heatmap, lineChart, chartControls, sideChart, chartType = "line", onChartTypeChange }: AccountBalanceChartProps) { const { transactions, accounts } = useFinanceData(); const [period, setPeriod] = useState("3 months"); const { compact, money, date: fmtDate } = useMoney(); const idSet = useMemo(() => new Set(accountIds), [accountIds]); const selectedAccounts = useMemo(() => accounts.filter((a) => idSet.has(a.id)), [accounts, idSet]); // Today's balance for the selected accounts = the number the chart ends on. const currentBalance = useMemo( () => selectedAccounts.reduce((s, a) => s + a.balance, 0), [selectedAccounts] ); const allData = useMemo(() => { if (accountIds.length === 0) return []; // A transaction with no date (a malformed import/sync row) can't be // placed on a timeline; keeping it would crash the sort below. const txns = transactions.filter((t) => idSet.has(t.accountId) && t.date); if (txns.length === 0) return []; const sorted = [...txns].sort((a, b) => a.date.localeCompare(b.date)); const dailyMap: Record = {}; sorted.forEach((t) => { const delta = t.type === "income" ? Math.abs(t.amount) : -Math.abs(t.amount); dailyMap[t.date] = (dailyMap[t.date] || 0) + delta; }); const days = Object.keys(dailyMap).sort(); let cumulative = 0; const flow = days.map((date) => { cumulative += dailyMap[date]; return { date, cumulative }; }); // Shift the running flow so its final point equals today's real balance. const offset = currentBalance - (flow.length ? flow[flow.length - 1].cumulative : 0); return flow.map(({ date, cumulative }) => ({ date, value: Math.round((cumulative + offset) * 100) / 100 })); }, [transactions, idSet, accountIds, currentBalance]); const data = useMemo(() => { const days = periods.find((p) => p.label === period)?.days; // Filter by a real date window. Slicing the array by `days` counted data // points, so "30 days" over sparse data silently showed several months. if (!days || days >= 999) return allData; const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - days); const cutoffStr = toDateString(cutoff); return allData.filter((d) => d.date >= cutoffStr); }, [allData, period]); const subtitle = selectedAccounts.length === 0 ? "No accounts selected" : selectedAccounts.length === 1 ? accountDisplayName(selectedAccounts[0]) : `${selectedAccounts.length} accounts combined`; const displayBalance = debt ? -currentBalance : currentBalance; const chartData = useMemo(() => (debt ? data.map((d) => ({ ...d, value: -d.value })) : data), [data, debt]); const calendarView = !!heatmap && chartType === "heatmap"; return (
{debt ? "Balance owed" : "Current balance"}

{subtitle}

{control}
{heatmap && onChartTypeChange && } {chartControls ?? (!calendarView && )}
{calendarView ? heatmap : lineChart ?? (chartData.length === 0 ? (

{accountIds.length === 0 ? "Select at least one account to see its balance trend." : "No transactions for the selected account(s) to chart."}

) : ( fmtDate(d)} yAxisWidth={44} colorVar={debt ? "--chart-red" : undefined} /> ))}
{sideChart &&
{sideChart}
}
); }