import { useEffect, useState, useMemo } from 'react'; import { Link } from 'react-router-dom'; import { ArrowRight, TrendingUp, TrendingDown, RefreshCw, CreditCard, Pencil, Check, X, BellRing, Landmark, } from 'lucide-react'; import api, { type Account, type Transaction } from '../api'; import { useSettings } from '@/hooks/useSettings'; import { formatAmount, formatDate } from '@/lib/format'; import DashboardSection from '@/components/DashboardSection'; import SectionConfigDialog from '@/components/SectionConfigDialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { cn } from '@/lib/utils'; // --- Section definitions --- interface SectionDef { filterFn: (a: Account) => boolean; totalCurrency: string; // empty string = no total } const SECTION_DEFS: Record = { crc_accounts: { filterFn: (a) => a.account_type === 'BANK' && a.currency === 'CRC', totalCurrency: 'CRC' }, usd_accounts: { filterFn: (a) => a.account_type === 'BANK' && a.currency === 'USD', totalCurrency: 'USD' }, pension: { filterFn: (a) => a.account_type === 'PENSION', totalCurrency: 'CRC' }, savings: { filterFn: (a) => a.account_type === 'SAVINGS', totalCurrency: 'CRC' }, liabilities: { filterFn: (a) => a.account_type === 'LIABILITY', totalCurrency: '' }, crypto: { filterFn: (a) => a.account_type === 'CRYPTO', totalCurrency: '' }, }; const BANK_ORDER = ['BAC', 'BCR', 'DAVIVIENDA']; // --- AccountRow --- interface AccountRowProps { account: Account; editingId: number | null; editValue: string; setEditValue: (v: string) => void; startEditing: (a: Account) => void; saveBalance: (id: number) => void; cancelEditing: () => void; } function AccountRow({ account, editingId, editValue, setEditValue, startEditing, saveBalance, cancelEditing, }: AccountRowProps) { const isLiability = account.account_type === 'LIABILITY'; const isCrypto = account.account_type === 'CRYPTO'; const label = isCrypto ? account.label : (account.bank === 'DAVIVIENDA' ? 'DAV' : account.bank); const isEditing = editingId === account.id; return (
{label} {isEditing ? (
setEditValue(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') saveBalance(account.id); if (e.key === 'Escape') cancelEditing(); }} autoFocus className="text-sm font-bold font-mono tracking-tight h-auto py-1 w-40" />
) : (
{formatAmount(account.balance, account.currency)} {isLiability && account.next_payment != null && ( Next: {formatAmount(account.next_payment, account.currency)} )}
)}
); } // --- Dashboard --- export default function Dashboard() { const [accounts, setAccounts] = useState([]); const [recent, setRecent] = useState([]); const [loading, setLoading] = useState(true); const [editingId, setEditingId] = useState(null); const [editValue, setEditValue] = useState(''); const [exchangeRate, setExchangeRate] = useState<{ buy_rate: number; sell_rate: number } | null>(null); const [configSection, setConfigSection] = useState(null); const [testingPush, setTestingPush] = useState(false); const { settings, patchSection } = useSettings(); const fetchData = async () => { setLoading(true); try { const [accRes, txRes] = await Promise.all([ api.get('/accounts/'), api.get('/transactions/recent?limit=5'), ]); setAccounts(accRes.data); setRecent(txRes.data); } catch (e) { console.error(e); } finally { setLoading(false); } api.get('/exchange-rate/').then((r) => setExchangeRate(r.data)).catch(() => {}); }; useEffect(() => { fetchData(); }, []); const startEditing = (account: Account) => { setEditingId(account.id); setEditValue(String(account.balance)); }; const cancelEditing = () => { setEditingId(null); setEditValue(''); }; const saveBalance = async (accountId: number) => { const parsed = parseFloat(editValue); if (isNaN(parsed)) return cancelEditing(); try { await api.patch(`/accounts/${accountId}`, { balance: parsed }); setEditingId(null); setEditValue(''); fetchData(); } catch (e) { console.error(e); } }; const rowProps = { editingId, editValue, setEditValue, startEditing, saveBalance, cancelEditing }; // Sort sections by order, filter by visible const sortedSections = useMemo(() => { const sections = settings.dashboard.sections; return Object.entries(sections) .filter(([, s]) => s.visible) .sort(([, a], [, b]) => a.order - b.order); }, [settings]); // Net worth calculation const netWorthBreakdown = useMemo(() => { if (accounts.length === 0) return null; let assets = 0; let liabilities = 0; for (const a of accounts) { const isLiability = a.account_type === 'LIABILITY'; let crcValue = 0; if (a.currency === 'USD') { crcValue = Math.abs(a.balance) * (exchangeRate?.sell_rate ?? 0); } else if (a.currency === 'CRC') { crcValue = Math.abs(a.balance); } if (isLiability) { liabilities += crcValue; } else { assets += crcValue; } } return { assets, liabilities, net: assets - liabilities }; }, [accounts, exchangeRate]); return (
{/* Header */}

Dashboard

Financial overview

{/* Net Worth */} {netWorthBreakdown != null && (
Net {netWorthBreakdown.net < 0 ? '-' : ''}{formatAmount(netWorthBreakdown.net, 'CRC')}
Assets {formatAmount(netWorthBreakdown.assets, 'CRC')} Liabilities {formatAmount(netWorthBreakdown.liabilities, 'CRC')}
)} {/* Account sections */} {sortedSections.map(([sectionId, sectionSettings]) => { const def = SECTION_DEFS[sectionId]; if (!def) return null; let accts = accounts.filter(def.filterFn); if (accts.length === 0) return null; // Sort bank accounts by bank order if (sectionId === 'crc_accounts' || sectionId === 'usd_accounts') { accts = accts.sort((a, b) => BANK_ORDER.indexOf(a.bank) - BANK_ORDER.indexOf(b.bank)); } const total = accts.reduce((s, a) => s + a.balance, 0); return ( patchSection(sectionId, { expanded })} onOpenConfig={() => setConfigSection(sectionId)} > {accts.map((a) => ( ))} ); })} {/* Exchange rate */} {exchangeRate && ( USD/CRC Exchange Rate
Buy: ₡{exchangeRate.buy_rate.toFixed(2)} Sell: ₡{exchangeRate.sell_rate.toFixed(2)}
)} {/* Recent transactions */}
Recent Charges
View all
{recent.length === 0 && !loading ? (
No transactions yet. Add your first one!
) : (
{recent.map((tx) => (
{tx.transaction_type === 'DEPOSITO' ? : tx.transaction_type === 'DEVOLUCION' ? : }

{tx.merchant}

{formatDate(tx.date)} {tx.category && {tx.category.name}}

{tx.transaction_type === 'COMPRA' ? '-' : '+'}{formatAmount(tx.amount, tx.currency)}
))}
)}
{/* Test push notification */}

Test Push Notification

Creates a mock transaction to trigger a push notification

{/* Section config dialog */} {configSection && settings.dashboard.sections[configSection] && ( { if (!open) setConfigSection(null); }} onSave={(id, partial) => patchSection(id, partial)} /> )}
); }