'use client'; import { useState, useEffect, useCallback } from 'react'; import { AuthService } from '../lib/authService'; import { DataService } from '../lib/dataService'; import DataInput from '../components/DataInput'; import TransactionEdit from '../components/TransactionEdit'; import EnvelopeCreate from '../components/EnvelopeCreate'; import EnvelopeEdit from '../components/EnvelopeEdit'; import AuthModal from '../components/AuthModal'; import SetupWizard from '../components/SetupWizard'; import AccountManagement from '../components/AccountManagement'; import DashboardCharts from '../components/DashboardCharts'; import Settings from '../components/Settings'; import GetPaid from '../components/GetPaid'; import GoalsManager from '../components/GoalsManager'; import TransactionRulesManager from '../components/TransactionRulesManager'; interface User { userId: string; email: string } interface Account { id: string; name: string; type: 'checking' | 'savings' | 'credit_card' | 'mortgage' | 'investment' | 'loan'; balance: number; startingBalance?: number; institution?: string; accountNumber?: string; color: string; isActive: boolean; defaultPaycheckAmount?: number; } interface Envelope { id: string; name: string; allocated: number; spent: number; color: string; accountId: string; incomeAllocation?: number; incomeAllocationType?: 'percentage' | 'fixed'; rollover?: boolean; } interface Transaction { id: string; envelopeId?: string; amount: number; description: string; date: Date; accountId: string; isRecurring?: boolean; recurringFrequency?: 'weekly' | 'biweekly' | 'monthly' | 'yearly'; lastAppliedDate?: string; } export interface Goal { id: string; name: string; targetAmount: number; currentAmount: number; accountId: string; targetDate?: string; color: string; } export interface TransactionRule { id: string; keyword: string; envelopeId: string; matchCase: boolean; } type ViewMode = 'dashboard' | 'accounts' | 'transactions' | 'envelopes' | 'goals' | 'rules' | 'settings'; type SaveStatus = 'idle' | 'saving' | 'saved' | 'error'; const liabilityTypes: Array = ['credit_card', 'mortgage', 'loan']; function normalizeBalanceByType(type: Account['type'], balance: number): number { return liabilityTypes.includes(type) ? -Math.abs(balance || 0) : (balance || 0); } function applyRulesToTransaction( tx: Transaction, rules: TransactionRule[], envelopes: Envelope[], ): Transaction { if (tx.envelopeId || tx.amount >= 0) return tx; for (const rule of rules) { const haystack = rule.matchCase ? tx.description : tx.description.toLowerCase(); const needle = rule.matchCase ? rule.keyword : rule.keyword.toLowerCase(); if (haystack.includes(needle) && envelopes.some(e => e.id === rule.envelopeId)) { return { ...tx, envelopeId: rule.envelopeId }; } } return tx; } export default function BudgetDashboard() { const [currentView, setCurrentView] = useState('dashboard'); const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [showAuthModal, setShowAuthModal] = useState(false); const [showSetupWizard, setShowSetupWizard] = useState(false); const [showGetPaidModal, setShowGetPaidModal] = useState(false); const [saveStatus, setSaveStatus] = useState('idle'); const [accounts, setAccounts] = useState([]); const [envelopes, setEnvelopes] = useState([]); const [transactions, setTransactions] = useState([]); const [goals, setGoals] = useState([]); const [transactionRules, setTransactionRules] = useState([]); const [setupCompleted, setSetupCompleted] = useState(false); // Transaction filters const [txSearch, setTxSearch] = useState(''); const [txDateFrom, setTxDateFrom] = useState(''); const [txDateTo, setTxDateTo] = useState(''); const [txAccountFilter, setTxAccountFilter] = useState(''); const [txEnvelopeFilter, setTxEnvelopeFilter] = useState(''); // Dashboard customization const [dashboardSections, setDashboardSections] = useState([ { id: 'summaryCards', label: 'Summary Cards', visible: true }, { id: 'charts', label: 'Analytics Charts', visible: true }, { id: 'insights', label: 'Spending Insights', visible: true }, { id: 'budgetProgress', label: 'Budget Progress', visible: true }, { id: 'goals', label: 'Goals', visible: true }, { id: 'quickActions', label: 'Quick Actions', visible: true }, { id: 'recentTransactions', label: 'Recent Transactions', visible: true }, ]); const [showDashboardSettings, setShowDashboardSettings] = useState(false); const [draggedIndex, setDraggedIndex] = useState(null); // Modal states const [editingTransaction, setEditingTransaction] = useState(null); const [showCreateEnvelope, setShowCreateEnvelope] = useState(false); const [editingEnvelope, setEditingEnvelope] = useState(null); const [showGoalsManager, setShowGoalsManager] = useState(false); // ── Derived helpers ────────────────────────────────────────────────────────── const calculateEnvelopeSpending = useCallback((envs: Envelope[], txns: Transaction[]): Envelope[] => envs.map(envelope => { const spent = txns .filter(t => t.envelopeId === envelope.id && t.amount < 0) .reduce((sum, t) => sum + Math.abs(t.amount), 0); return { ...envelope, spent }; }), []); const calculateAccountBalances = useCallback((accs: Account[], txns: Transaction[]): Account[] => { const map = new Map(); txns.forEach(t => map.set(t.accountId, (map.get(t.accountId) ?? 0) + t.amount)); return accs.map(acc => { const base = normalizeBalanceByType(acc.type, acc.startingBalance ?? acc.balance ?? 0); return { ...acc, startingBalance: base, balance: base + (map.get(acc.id) ?? 0) }; }); }, []); // ── Auth & data load ───────────────────────────────────────────────────────── useEffect(() => { const unsub = AuthService.onAuthStateChanged(async (u) => { setUser(u); if (u) { DataService.setUserId(u.userId); try { const userData = await DataService.loadUserData(); if (userData) { const txnsWithDates = (userData.transactions || []).map((t: any) => ({ ...t, date: typeof t.date === 'string' ? new Date(t.date) : t.date, })); const accsWithBase = (userData.accounts || []).map((acc: any) => { const txTotal = txnsWithDates .filter((t: Transaction) => t.accountId === acc.id) .reduce((s: number, t: Transaction) => s + t.amount, 0); const base = normalizeBalanceByType(acc.type, acc.startingBalance ?? acc.balance - txTotal); return { ...acc, startingBalance: base, balance: base + txTotal }; }); setAccounts(accsWithBase); setEnvelopes(userData.envelopes || []); setTransactions(txnsWithDates); setGoals(userData.goals || []); setTransactionRules(userData.transactionRules || []); setSetupCompleted(userData.setupCompleted || false); setShowSetupWizard(!(userData.setupCompleted) || accsWithBase.length === 0); // Auto-apply due recurring transactions autoApplyRecurring(txnsWithDates); } else { setShowSetupWizard(true); } } catch { setShowSetupWizard(true); } } else { DataService.setUserId(null); setAccounts([]); setEnvelopes([]); setTransactions([]); setGoals([]); setTransactionRules([]); setSetupCompleted(false); setShowSetupWizard(false); } setLoading(false); }); return () => unsub(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // ── Auto-save ──────────────────────────────────────────────────────────────── useEffect(() => { if (!user) return; if (!accounts.length && !envelopes.length && !transactions.length && !setupCompleted) return; setSaveStatus('saving'); const timer = setTimeout(async () => { try { const updatedEnvelopes = calculateEnvelopeSpending(envelopes, transactions); const updatedAccounts = calculateAccountBalances(accounts, transactions); await DataService.saveUserData({ accounts: updatedAccounts, envelopes: updatedEnvelopes, transactions, goals, transactionRules, setupCompleted, } as any); setSaveStatus('saved'); setTimeout(() => setSaveStatus('idle'), 2000); } catch { setSaveStatus('error'); } }, 1000); return () => clearTimeout(timer); }, [user, accounts, envelopes, transactions, goals, transactionRules, setupCompleted, calculateEnvelopeSpending, calculateAccountBalances]); // ── Recurring transactions ─────────────────────────────────────────────────── const autoApplyRecurring = (txns: Transaction[]) => { const now = new Date(); const due = txns.filter(t => { if (!t.isRecurring || !t.recurringFrequency) return false; const last = t.lastAppliedDate ? new Date(t.lastAppliedDate) : t.date; const next = new Date(last); if (t.recurringFrequency === 'weekly') next.setDate(next.getDate() + 7); else if (t.recurringFrequency === 'biweekly') next.setDate(next.getDate() + 14); else if (t.recurringFrequency === 'monthly') next.setMonth(next.getMonth() + 1); else if (t.recurringFrequency === 'yearly') next.setFullYear(next.getFullYear() + 1); return next <= now; }); if (due.length === 0) return; const newTxns: Transaction[] = due.map(t => ({ ...t, id: crypto.randomUUID(), date: new Date(), lastAppliedDate: new Date().toISOString(), })); setTransactions(prev => { const updated = prev.map(t => due.find(d => d.id === t.id) ? { ...t, lastAppliedDate: new Date().toISOString() } : t); return [...updated, ...newTxns]; }); }; // ── Event handlers ─────────────────────────────────────────────────────────── const handleTransactionsAdded = (newTxns: Transaction[]) => { // Apply auto-categorization rules const ruled = newTxns.map(t => applyRulesToTransaction(t, transactionRules, envelopes)); const all = [...ruled]; ruled.forEach(tx => { if (tx.amount > 0 && !tx.envelopeId) { const accEnvs = envelopes.filter(e => e.accountId === tx.accountId); const withAlloc = accEnvs.filter(e => e.incomeAllocation && e.incomeAllocationType); const hasFixed = withAlloc.some(e => e.incomeAllocationType === 'fixed'); const hasPerc = withAlloc.some(e => e.incomeAllocationType === 'percentage'); const source = withAlloc.length > 0 && !(hasFixed && hasPerc) ? withAlloc : []; if (source.length > 0) { source.forEach(env => { const amt = env.incomeAllocationType === 'percentage' ? tx.amount * (env.incomeAllocation! / 100) : Math.min(env.incomeAllocation!, tx.amount); if (amt > 0) all.push({ id: crypto.randomUUID(), envelopeId: env.id, amount: amt, description: `Income → ${env.name}`, date: tx.date, accountId: tx.accountId, }); }); } else { const totalAlloc = accEnvs.reduce((s, e) => s + e.allocated, 0); if (accEnvs.length > 0 && totalAlloc > 0) { accEnvs.forEach(env => { const amt = tx.amount * (env.allocated / totalAlloc); if (amt > 0) all.push({ id: crypto.randomUUID(), envelopeId: env.id, amount: amt, description: `Income → ${env.name}`, date: tx.date, accountId: tx.accountId, }); }); } } } }); setTransactions(prev => { const next = [...prev, ...all]; setEnvelopes(calculateEnvelopeSpending(envelopes, next)); setAccounts(calculateAccountBalances(accounts, next)); return next; }); }; const handleSignOut = async () => { setLoading(true); await AuthService.signOut().catch(() => {}); setLoading(false); }; const handleSetupComplete = async (accs: Account[], envs: Envelope[]) => { // Re-load from server so imported CSV transactions are included try { const userData = await DataService.loadUserData(); if (userData) { const accsWithBase = (userData.accounts ?? accs).map(a => { const base = normalizeBalanceByType(a.type, a.startingBalance ?? a.balance); return { ...a, startingBalance: base, balance: base }; }); setAccounts(accsWithBase); setEnvelopes(userData.envelopes ?? envs); setTransactions(userData.transactions ?? []); setGoals(userData.goals ?? []); setTransactionRules(userData.transactionRules ?? []); } else { const accsWithBase = accs.map(a => { const base = normalizeBalanceByType(a.type, a.startingBalance ?? a.balance); return { ...a, startingBalance: base, balance: base }; }); setAccounts(accsWithBase); setEnvelopes(envs); setTransactions([]); } } catch { const accsWithBase = accs.map(a => { const base = normalizeBalanceByType(a.type, a.startingBalance ?? a.balance); return { ...a, startingBalance: base, balance: base }; }); setAccounts(accsWithBase); setEnvelopes(envs); setTransactions([]); } setSetupCompleted(true); setShowSetupWizard(false); }; const handleAccountUpdate = (updated: Account) => { setAccounts(prev => { const base = normalizeBalanceByType(updated.type, updated.startingBalance ?? updated.balance); const next = prev.map(a => a.id === updated.id ? { ...updated, startingBalance: base } : a); return calculateAccountBalances(next, transactions); }); }; const handleAccountAdd = (newAcc: Account) => { setAccounts(prev => { const base = normalizeBalanceByType(newAcc.type, newAcc.startingBalance ?? newAcc.balance); return calculateAccountBalances([...prev, { ...newAcc, startingBalance: base }], transactions); }); }; const handleTransactionEdit = (updated: Transaction) => { setTransactions(prev => { const next = prev.map(t => t.id === updated.id ? updated : t); setEnvelopes(calculateEnvelopeSpending(envelopes, next)); setAccounts(calculateAccountBalances(accounts, next)); return next; }); }; const handleTransactionDelete = (id: string) => { setTransactions(prev => { const next = prev.filter(t => t.id !== id); setEnvelopes(calculateEnvelopeSpending(envelopes, next)); setAccounts(calculateAccountBalances(accounts, next)); return next; }); }; // ── Filtered transactions ──────────────────────────────────────────────────── const filteredTransactions = transactions.filter(t => { if (txSearch && !t.description.toLowerCase().includes(txSearch.toLowerCase())) return false; if (txAccountFilter && t.accountId !== txAccountFilter) return false; if (txEnvelopeFilter && t.envelopeId !== txEnvelopeFilter) return false; const d = t.date instanceof Date ? t.date : new Date(t.date); if (txDateFrom && d < new Date(txDateFrom)) return false; if (txDateTo && d > new Date(txDateTo + 'T23:59:59')) return false; return true; }).slice().sort((a, b) => { const da = a.date instanceof Date ? a.date : new Date(a.date); const db = b.date instanceof Date ? b.date : new Date(b.date); return db.getTime() - da.getTime(); }); // Running balance (newest-first list — show balance before each transaction) const runningBalances = (() => { const sorted = [...transactions].sort((a, b) => { const da = a.date instanceof Date ? a.date : new Date(a.date); const db = b.date instanceof Date ? b.date : new Date(b.date); return da.getTime() - db.getTime(); }); const balMap = new Map(); sorted.forEach(t => balMap.set(t.id, (balMap.get(t.id) ?? 0))); let running = 0; const result = new Map(); sorted.forEach(t => { running += t.amount; result.set(t.id, running); }); return result; })(); // ── Spending insights ──────────────────────────────────────────────────────── const spendingInsights = (() => { const now = new Date(); const insights: string[] = []; for (let i = 0; i < 2; i++) { const mo = new Date(now.getFullYear(), now.getMonth() - i, 1); const me = new Date(now.getFullYear(), now.getMonth() - i + 1, 0); const spent = transactions .filter(t => { const d = t.date instanceof Date ? t.date : new Date(t.date); return t.amount < 0 && d >= mo && d <= me; }) .reduce((s, t) => s + Math.abs(t.amount), 0); if (i === 0 && spent > 0) insights.push(`This month you've spent $${spent.toFixed(2)} so far.`); if (i === 1 && spent > 0) { const thisMo = transactions .filter(t => { const d = t.date instanceof Date ? t.date : new Date(t.date); const m0 = new Date(now.getFullYear(), now.getMonth(), 1); const m1 = new Date(now.getFullYear(), now.getMonth() + 1, 0); return t.amount < 0 && d >= m0 && d <= m1; }) .reduce((s, t) => s + Math.abs(t.amount), 0); const diff = thisMo - spent; if (Math.abs(diff) > 1) { insights.push(diff > 0 ? `You're spending $${diff.toFixed(2)} more than last month.` : `You're spending $${Math.abs(diff).toFixed(2)} less than last month. Great job!`); } } } const overspent = envelopes.filter(e => e.allocated > 0 && e.spent > e.allocated); if (overspent.length > 0) { insights.push(`${overspent.length} envelope${overspent.length > 1 ? 's are' : ' is'} overspent: ${overspent.map(e => e.name).join(', ')}.`); } const nearLimit = envelopes.filter(e => e.allocated > 0 && e.spent / e.allocated >= 0.8 && e.spent <= e.allocated); if (nearLimit.length > 0) { insights.push(`${nearLimit.map(e => e.name).join(', ')} ${nearLimit.length > 1 ? 'are' : 'is'} near the budget limit (≥80%).`); } return insights; })(); // ── Render ─────────────────────────────────────────────────────────────────── const navBtn = (view: ViewMode, label: string) => ( ); const renderLanding = () => (
Capsule Capsule

Take control of your money with smart envelopes

Create envelopes, track spending, and auto-allocate income across accounts. Get set up in minutes with our guided wizard.

Auto-save & offline-ready data Guided setup wizard
Capsule

Capsule

Smart Envelope Budgeting

{[ { label: 'Total Balance', body: '$0.00', sub: 'Link accounts to see your starting balance', c: 'var(--color-primary-blue)', bg: 'rgba(30,115,190,0.05)', border: 'var(--color-cloud-blue)' }, { label: 'Envelopes', body: 'Organize spending', sub: 'Create categories with color-coded envelopes', c: 'var(--color-finance-green)', bg: 'rgba(40,167,69,0.05)', border: 'var(--color-finance-green)' }, { label: 'Income Auto-Allocation', body: 'Smart distribution', sub: 'Allocate paychecks by percentage or fixed amounts', c: 'var(--color-primary-blue)', bg: 'rgba(167,216,248,0.1)', border: 'var(--color-cloud-blue)' }, ].map(({ label, body, sub, c, bg, border }) => (

{label}

{body}

{sub}

))}
); const renderDashboard = () => { const assetTypes: Account['type'][] = ['checking', 'savings', 'investment']; const debtTypes: Account['type'][] = ['credit_card', 'mortgage', 'loan']; const totalAssets = accounts.filter(a => assetTypes.includes(a.type)).reduce((s, a) => s + a.balance, 0); const totalDebt = Math.abs(accounts.filter(a => debtTypes.includes(a.type)).reduce((s, a) => s + a.balance, 0)); const totalBalance = accounts.reduce((s, a) => s + a.balance, 0); const now = new Date(); const monthStart = new Date(now.getFullYear(), now.getMonth(), 1); const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0); const monthTxns = transactions.filter(t => { const d = t.date instanceof Date ? t.date : new Date(t.date); return d >= monthStart && d <= monthEnd; }); const totalBudget = envelopes.reduce((s, e) => s + e.allocated, 0); const totalSpent = monthTxns.filter(t => t.amount < 0).reduce((s, t) => s + Math.abs(t.amount), 0); const budgetPct = totalBudget > 0 ? Math.round((totalSpent / totalBudget) * 100) : 0; return dashboardSections.filter(s => s.visible).map(section => { switch (section.id) { case 'summaryCards': return (
{[ { label: 'Total Assets', value: `$${totalAssets.toFixed(2)}`, color: 'bg-green-500', textColor: 'text-green-600', icon: '+', view: 'accounts' as ViewMode }, { label: 'Total Debt', value: `$${totalDebt.toFixed(2)}`, color: 'bg-red-500', textColor: 'text-red-600', icon: '−', view: 'accounts' as ViewMode }, { label: 'Net Worth', value: `$${totalBalance.toFixed(2)}`, color: 'bg-blue-500', textColor: totalBalance >= 0 ? 'text-blue-600' : 'text-red-600', icon: '$', view: 'accounts' as ViewMode }, { label: 'Envelopes', value: envelopes.length.toString(), color: 'bg-purple-500', textColor: 'text-purple-600', icon: 'E', view: 'envelopes' as ViewMode }, { label: 'Transactions', value: transactions.length.toString(), color: 'bg-orange-500', textColor: 'text-gray-900', icon: 'T', view: 'transactions' as ViewMode }, ].map(({ label, value, color, textColor, icon, view }) => ( ))}
); case 'charts': return (
); case 'insights': return spendingInsights.length > 0 ? (

Spending Insights

    {spendingInsights.map((insight, i) => (
  • • {insight}
  • ))}
) : null; case 'budgetProgress': return (

Budget Progress This Month

Total Spending ${totalSpent.toFixed(2)} / ${totalBudget.toFixed(2)}
= 100 ? 'bg-red-500' : budgetPct >= 80 ? 'bg-yellow-500' : 'bg-green-500'}`} style={{ width: `${Math.min(budgetPct, 100)}%` }} />

{budgetPct}% of monthly budget used

{/* Per-envelope progress */}
{envelopes.filter(e => e.allocated > 0).slice(0, 5).map(e => { const pct = Math.round((e.spent / e.allocated) * 100); return (
{e.name} {e.rollover && ↻} ${e.spent.toFixed(0)} / ${e.allocated.toFixed(0)}
= 100 ? 'bg-red-500' : pct >= 80 ? 'bg-yellow-400' : 'bg-green-400'}`} style={{ width: `${Math.min(pct, 100)}%`, backgroundColor: pct < 80 ? e.color : undefined }} />
); })}
); case 'goals': return goals.length > 0 ? (

Goals

{goals.map(g => { const pct = g.targetAmount > 0 ? Math.min(100, Math.round((g.currentAmount / g.targetAmount) * 100)) : 0; return (
{g.name} {pct}%
${g.currentAmount.toFixed(2)} ${g.targetAmount.toFixed(2)}
{g.targetDate && (

Target: {new Date(g.targetDate).toLocaleDateString()}

)}
); })}
) : null; case 'quickActions': return (

Quick Actions

{[ { icon: '➕', label: 'Add Transaction', view: 'transactions' as ViewMode, bg: 'bg-blue-50 border-blue-200 hover:bg-blue-100' }, { icon: '📦', label: 'Envelopes', view: 'envelopes' as ViewMode, bg: 'bg-purple-50 border-purple-200 hover:bg-purple-100' }, { icon: '🏦', label: 'Accounts', view: 'accounts' as ViewMode, bg: 'bg-green-50 border-green-200 hover:bg-green-100' }, { icon: '🎯', label: 'Goals', view: 'goals' as ViewMode, bg: 'bg-yellow-50 border-yellow-200 hover:bg-yellow-100' }, ].map(({ icon, label, view, bg }) => ( ))}
); case 'recentTransactions': return (

Recent Transactions

    {transactions.slice().sort((a, b) => { const da = a.date instanceof Date ? a.date : new Date(a.date); const db = b.date instanceof Date ? b.date : new Date(b.date); return db.getTime() - da.getTime(); }).slice(0, 5).map(t => { const env = envelopes.find(e => e.id === t.envelopeId); return (
  • {env && }

    {t.description}

    {(t.date instanceof Date ? t.date : new Date(t.date)).toLocaleDateString()} {env && ` · ${env.name}`}

    = 0 ? 'text-green-600' : 'text-red-600'}`}> {t.amount >= 0 ? '+' : '-'}${Math.abs(t.amount).toFixed(2)}
  • ); })}
); default: return null; } }); }; const renderTransactions = () => (

Transactions

({ id: a.id, name: a.name }))} transactions={transactions} /> {/* Filter bar */}
setTxSearch(e.target.value)} className="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
setTxDateFrom(e.target.value)} className="flex-1 px-2 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" /> setTxDateTo(e.target.value)} className="flex-1 px-2 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
{(txSearch || txAccountFilter || txEnvelopeFilter || txDateFrom || txDateTo) && ( )} {/* Transaction list */}

All Transactions {filteredTransactions.length !== transactions.length && `(${filteredTransactions.length} of ${transactions.length})`}

{filteredTransactions.length === 0 ? (

No transactions match your filters.

) : (
{filteredTransactions.map(t => { const env = envelopes.find(e => e.id === t.envelopeId); const acc = accounts.find(a => a.id === t.accountId); const runBal = runningBalances.get(t.id) ?? 0; return ( ); })}
Date Description Envelope Account Amount Balance
{(t.date instanceof Date ? t.date : new Date(t.date)).toLocaleDateString()} {t.description} {t.isRecurring && ↻} {env ? ( {env.name} ) : —} {acc?.name ?? '—'} = 0 ? 'text-green-600' : 'text-red-600'}`}> {t.amount >= 0 ? '+' : '-'}${Math.abs(t.amount).toFixed(2)}
)}
); const renderEnvelopes = () => (

Envelopes

{envelopes.map(env => { const incomeAlloc = transactions.filter(t => t.envelopeId === env.id && t.amount > 0).reduce((s, t) => s + t.amount, 0); const totalAlloc = env.allocated + incomeAlloc; const remaining = totalAlloc - env.spent; const pct = totalAlloc > 0 ? Math.min(100, Math.round((env.spent / totalAlloc) * 100)) : 0; return (

{env.name}

{env.rollover && ↻}
= 100 ? 'bg-red-500' : pct >= 80 ? 'bg-yellow-400' : ''}`} style={{ width: `${pct}%`, backgroundColor: pct < 80 ? env.color : undefined }} />
Allocated${totalAlloc.toFixed(2)}
Spent${env.spent.toFixed(2)}
Remaining${remaining.toFixed(2)}
); })}
); return (
{/* Navigation */} {/* Main Content */}
{loading ? (
Loading…
) : !user ? ( renderLanding() ) : ( <> {currentView === 'dashboard' && (

Dashboard

{showDashboardSettings && (

Arrange sections — drag to reorder

{dashboardSections.map((sec, i) => (
setDraggedIndex(i)} onDragOver={e => e.preventDefault()} onDrop={() => { if (draggedIndex !== null && draggedIndex !== i) { const s = [...dashboardSections]; const [r] = s.splice(draggedIndex, 1); s.splice(i, 0, r); setDashboardSections(s); setDraggedIndex(null); } }} className={`flex items-center gap-3 p-3 bg-white border-2 rounded-lg cursor-move ${draggedIndex === i ? 'opacity-50 border-blue-400' : 'border-gray-200'}`}> ⋮⋮
))}
)} {renderDashboard()}
)} {currentView === 'accounts' && (
setAccounts(prev => prev.filter(a => a.id !== id))} />
)} {currentView === 'transactions' && renderTransactions()} {currentView === 'envelopes' && renderEnvelopes()} {currentView === 'goals' && (
)} {currentView === 'rules' && (
)} {currentView === 'settings' && user && ( { setCurrentView('dashboard'); setUser(null); }} /> )} )}
{/* Modals */} {showAuthModal && ( setShowAuthModal(false)} onAuthSuccess={() => setShowAuthModal(false)} /> )} {showSetupWizard && ( setShowSetupWizard(false)} userId={user?.userId || ''} /> )} {showGetPaidModal && (
e.stopPropagation()} className="w-full max-w-2xl"> { handleTransactionsAdded(txns); setShowGetPaidModal(false); }} onAccountUpdate={handleAccountUpdate} autoOpen={true} />
)} {editingTransaction && ( ({ id: e.id, name: e.name }))} accounts={accounts.map(a => ({ id: a.id, name: a.name }))} onSave={t => { handleTransactionEdit(t); setEditingTransaction(null); }} onCancel={() => setEditingTransaction(null)} /> )} {showCreateEnvelope && ( { setEnvelopes(prev => [...prev, env]); setShowCreateEnvelope(false); }} onCancel={() => setShowCreateEnvelope(false)} accounts={accounts.map(a => ({ id: a.id, name: a.name }))} /> )} {editingEnvelope && ( { setEnvelopes(prev => prev.map(e => e.id === env.id ? env : e)); setEditingEnvelope(null); }} onEnvelopeDeleted={id => { setEnvelopes(prev => prev.filter(e => e.id !== id)); setTransactions(prev => prev.map(t => t.envelopeId === id ? { ...t, envelopeId: undefined } : t)); setEditingEnvelope(null); }} onCancel={() => setEditingEnvelope(null)} accounts={accounts.map(a => ({ id: a.id, name: a.name }))} /> )}
); }