import { useState } from "react"; import { Link } from "react-router-dom"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { getAccounts, createAccount, updateAccount, deleteAccount, getNetWorth, type Account } from "@/api/accounts"; import { getPortfolio } from "@/api/investments"; import { formatCurrency } from "@/utils/currency"; import { cn } from "@/utils/cn"; import { Plus, Trash2, Pencil, TrendingUp, Wallet, CreditCard, PiggyBank, Building2, Coins, Bitcoin, Landmark, ShieldCheck, Sprout } from "lucide-react"; import AccountFormModal from "./AccountFormModal"; const TYPE_ICONS: Record = { checking: Wallet, savings: PiggyBank, cash_isa: Sprout, stocks_shares_isa: TrendingUp, credit_card: CreditCard, investment: TrendingUp, cash: Coins, crypto_wallet: Bitcoin, loan: Building2, mortgage: Landmark, pension: ShieldCheck, other: Wallet, }; const TYPE_LABELS: Record = { checking: "Checking", savings: "Savings", cash_isa: "Cash ISA", stocks_shares_isa: "S&S ISA", credit_card: "Credit Card", investment: "Investment", cash: "Cash", crypto_wallet: "Crypto", loan: "Loan", mortgage: "Mortgage", pension: "Pension", other: "Other", }; const LIABILITY_TYPES = new Set(["credit_card", "loan", "mortgage"]); export default function AccountList() { const qc = useQueryClient(); const [showCreate, setShowCreate] = useState(false); const [editing, setEditing] = useState(null); const { data: accounts = [], isLoading } = useQuery({ queryKey: ["accounts"], queryFn: getAccounts, }); const { data: nw } = useQuery({ queryKey: ["net-worth"], queryFn: getNetWorth, }); const { data: portfolio } = useQuery({ queryKey: ["portfolio"], queryFn: getPortfolio, }); const holdingValueByAccount = (portfolio?.holdings ?? []).reduce>((acc, h) => { const v = Number(h.current_value || h.cost_basis_total); acc[h.account_id] = (acc[h.account_id] ?? 0) + v; return acc; }, {}); const invalidate = () => { qc.invalidateQueries({ queryKey: ["accounts"] }); qc.invalidateQueries({ queryKey: ["net-worth"] }); }; const deleteMutation = useMutation({ mutationFn: deleteAccount, onSuccess: invalidate, }); const createMutation = useMutation({ mutationFn: createAccount, onSuccess: () => { invalidate(); setShowCreate(false); }, }); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: string; data: Parameters[1] }) => updateAccount(id, data), onSuccess: () => { invalidate(); setEditing(null); }, }); const assets = accounts.filter(a => !LIABILITY_TYPES.has(a.type) && a.is_active); const liabilities = accounts.filter(a => LIABILITY_TYPES.has(a.type) && a.is_active); const inactive = accounts.filter(a => !a.is_active); return (

Accounts

Manage your financial accounts

{nw && (
{[ { label: "Total Assets", value: nw.total_assets, positive: true }, { label: "Total Liabilities", value: nw.total_liabilities, positive: nw.total_liabilities === 0 }, { label: "Net Worth", value: nw.net_worth, positive: nw.net_worth >= 0 }, ].map(({ label, value, positive }) => (

{label}

{formatCurrency(value, nw.base_currency)}

))}
)} {isLoading && (
{[1, 2, 3].map(i => (
))}
)} {assets.length > 0 && ( deleteMutation.mutate(id)} holdingValueByAccount={holdingValueByAccount} /> )} {liabilities.length > 0 && ( deleteMutation.mutate(id)} /> )} {inactive.length > 0 && ( deleteMutation.mutate(id)} holdingValueByAccount={holdingValueByAccount} muted /> )} {accounts.length === 0 && !isLoading && (

No accounts yet

Add your first account to get started

)} {showCreate && ( setShowCreate(false)} onSubmit={data => createMutation.mutate(data)} isLoading={createMutation.isPending} /> )} {editing && ( setEditing(null)} onSubmit={data => updateMutation.mutate({ id: editing.id, data })} isLoading={updateMutation.isPending} /> )}
); } const INVESTMENT_TYPES = new Set(["investment", "pension", "stocks_shares_isa", "crypto_wallet"]); function AccountGroup({ title, accounts, onEdit, onDelete, holdingValueByAccount = {}, muted = false, }: { title: string; accounts: Account[]; onEdit: (a: Account) => void; onDelete: (id: string) => void; holdingValueByAccount?: Record; muted?: boolean; }) { return (

{title}

{accounts.map(account => { const Icon = TYPE_ICONS[account.type] || Wallet; const isLiability = LIABILITY_TYPES.has(account.type); const utilPct = account.credit_limit && account.credit_limit > 0 ? Math.min(100, (Math.abs(account.current_balance) / account.credit_limit) * 100) : null; return (
{account.name} {TYPE_LABELS[account.type] || account.type} {!account.include_in_net_worth && ( excluded from net worth )}
{account.institution && (

{account.institution}

)} {account.interest_rate != null && (

{Number(account.interest_rate).toFixed(2)}% p.a.

)} {account.notes && (

{account.notes}

)}
{(() => { const holdingVal = INVESTMENT_TYPES.has(account.type) ? (holdingValueByAccount[account.id] ?? 0) : 0; const cashBal = Number(account.current_balance); const total = cashBal + holdingVal; return ( <>

{formatCurrency(holdingVal > 0 ? total : cashBal, account.currency)}

{holdingVal > 0 && cashBal !== 0 && (

{formatCurrency(cashBal, account.currency)} cash + {formatCurrency(holdingVal, account.currency)} holdings

)} {holdingVal > 0 && cashBal === 0 && (

holdings value

)} {account.credit_limit != null && (

limit {formatCurrency(account.credit_limit, account.currency)}

)} ); })()}
{/* Credit utilisation bar */} {utilPct !== null && (
Credit used {utilPct.toFixed(0)}%
80 ? "bg-destructive" : utilPct > 50 ? "bg-warning" : "bg-success")} style={{ width: `${utilPct}%` }} />
)}
); })}
); }