From 017421a85a5bac1b9692e7fd1ecd4e36397b8eb9 Mon Sep 17 00:00:00 2001 From: olsch01 Date: Sat, 21 Feb 2026 18:44:19 -0500 Subject: [PATCH] Fix budget page UI: annual totals, summary cards, sticky headers, column layout - Compute annual_total on data load via hydrateBudgetLine() (was only computed on edit) - Ensure monthly values are cast to numbers from PostgreSQL response - Fix summary cards to use pre-computed annual_total instead of re-summing months - Make Income/Expense section headers sticky so labels stay visible on horizontal scroll - Split account number and name into separate columns for better readability - Add section total in annual column for each income/expense group - Add subtle border between frozen columns and scrollable month data Co-Authored-By: Claude Opus 4.6 --- frontend/src/pages/budgets/BudgetsPage.tsx | 82 ++++++++++++++++++---- 1 file changed, 68 insertions(+), 14 deletions(-) diff --git a/frontend/src/pages/budgets/BudgetsPage.tsx b/frontend/src/pages/budgets/BudgetsPage.tsx index be1e847..6a02b93 100644 --- a/frontend/src/pages/budgets/BudgetsPage.tsx +++ b/frontend/src/pages/budgets/BudgetsPage.tsx @@ -40,6 +40,19 @@ function parseCurrencyValue(val: string): number { return isNegative ? -num : num; } +/** + * Ensure all monthly values are numbers (PostgreSQL can return strings for NUMERIC columns) + * and compute annual_total as the sum of all monthly values. + */ +function hydrateBudgetLine(row: any): BudgetLine { + const line: any = { ...row }; + for (const m of months) { + line[m] = Number(line[m]) || 0; + } + line.annual_total = months.reduce((sum, m) => sum + (line[m] || 0), 0); + return line as BudgetLine; +} + function parseCSV(text: string): Record[] { const lines = text.trim().split('\n'); if (lines.length < 2) return []; @@ -88,8 +101,10 @@ export function BudgetsPage() { queryKey: ['budgets', year], queryFn: async () => { const { data } = await api.get(`/budgets/${year}`); - setBudgetData(data); - return data; + // Hydrate each line: ensure numbers and compute annual_total + const hydrated = (data as any[]).map(hydrateBudgetLine); + setBudgetData(hydrated); + return hydrated; }, }); @@ -222,8 +237,8 @@ export function BudgetsPage() { const incomeLines = budgetData.filter((b) => b.account_type === 'income'); const expenseLines = budgetData.filter((b) => b.account_type === 'expense'); - const totalIncome = months.reduce((s, m) => s + incomeLines.reduce((a, b) => a + ((b as any)[m] || 0), 0), 0); - const totalExpense = months.reduce((s, m) => s + expenseLines.reduce((a, b) => a + ((b as any)[m] || 0), 0), 0); + const totalIncome = incomeLines.reduce((sum, line) => sum + (line.annual_total || 0), 0); + const totalExpense = expenseLines.reduce((sum, line) => sum + (line.annual_total || 0), 0); return ( @@ -285,20 +300,21 @@ export function BudgetsPage() {
- +
- Account + Acct # + Account Name {monthLabels.map((m) => ( {m} ))} - Annual + Annual {budgetData.length === 0 && ( - + No budget data. Import a CSV or add income/expense accounts to get started. @@ -306,18 +322,56 @@ export function BudgetsPage() { {['income', 'expense'].map((type) => { const lines = budgetData.filter((b) => b.account_type === type); if (lines.length === 0) return null; + + const sectionBg = type === 'income' ? '#e6f9e6' : '#fde8e8'; + const sectionTotal = lines.reduce((sum, line) => sum + (line.annual_total || 0), 0); + return [ - - {type} + + + {type} + + {monthLabels.map((m) => ( + + ))} + {fmt(sectionTotal)} , ...lines.map((line) => { const idx = budgetData.indexOf(line); return ( - - - {line.account_number} - {line.account_name} + + {line.account_number} + + + + {line.account_name} {line.fund_type === 'reserve' && R}