From 3d8cdc9068dd158e11b70fe5a19bc4668f9c5791 Mon Sep 17 00:00:00 2001 From: Christian Hood Date: Thu, 19 Mar 2026 19:15:08 -0400 Subject: [PATCH] Add annual overview page Year-at-a-glance table with monthly income, bills, variable spending, and surplus/deficit. Fetches all 12 months in parallel. Summary cards show annual totals. Co-Authored-By: Claude Sonnet 4.6 --- client/src/pages/AnnualOverview.jsx | 215 +++++++++++++++++++++++++++- 1 file changed, 212 insertions(+), 3 deletions(-) diff --git a/client/src/pages/AnnualOverview.jsx b/client/src/pages/AnnualOverview.jsx index f28d012..fdf4920 100644 --- a/client/src/pages/AnnualOverview.jsx +++ b/client/src/pages/AnnualOverview.jsx @@ -1,5 +1,214 @@ -function AnnualOverview() { - return

Annual Overview

Placeholder — coming soon.

; +import { useState, useEffect } from 'react'; + +const MONTH_NAMES = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December', +]; + +function fmt(value) { + if (value == null) return '—'; + const num = Number(value); + if (isNaN(num)) return '—'; + const abs = Math.abs(num).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + return num < 0 ? `-$${abs}` : `$${abs}`; } -export default AnnualOverview; +function surplusStyle(value) { + if (value == null || isNaN(Number(value))) return {}; + return { color: Number(value) >= 0 ? '#2a9d2a' : '#cc2222', fontWeight: 500 }; +} + +function sumField(rows, field) { + return rows.reduce((acc, row) => { + const v = row && row[field] != null ? Number(row[field]) : 0; + return acc + (isNaN(v) ? 0 : v); + }, 0); +} + +export default function AnnualOverview() { + const currentYear = new Date().getFullYear(); + const [year, setYear] = useState(currentYear); + const [monthData, setMonthData] = useState(Array(12).fill(null)); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(null); + + Promise.all( + Array.from({ length: 12 }, (_, i) => + fetch(`/api/summary/monthly?year=${year}&month=${i + 1}`) + .then(r => { + if (!r.ok) return null; + return r.json(); + }) + .catch(() => null) + ) + ).then(results => { + if (!cancelled) { + setMonthData(results); + setLoading(false); + } + }).catch(err => { + if (!cancelled) { + setError(err.message || 'Failed to load data'); + setLoading(false); + } + }); + + return () => { cancelled = true; }; + }, [year]); + + const hasData = monthData.some(row => row != null); + + const annualIncome = sumField(monthData, 'total_income'); + const annualBills = sumField(monthData, 'total_bills'); + const annualVariable = sumField(monthData, 'total_variable'); + const annualOneTime = sumField(monthData, 'total_one_time'); + const annualSpending = sumField(monthData, 'total_spending'); + const annualSurplus = sumField(monthData, 'surplus_deficit'); + + const cardStyle = { + background: '#f5f5f5', + border: '1px solid #ddd', + borderRadius: 6, + padding: '1rem 1.25rem', + minWidth: 160, + flex: '1 1 160px', + }; + + const cardLabelStyle = { + fontSize: '0.75rem', + color: '#666', + textTransform: 'uppercase', + letterSpacing: '0.05em', + marginBottom: 4, + }; + + const cardValueStyle = { + fontSize: '1.4rem', + fontWeight: 700, + }; + + return ( +
+ {/* Year navigation */} +
+ +

{year} Annual Overview

+ +
+ + {/* Summary cards */} +
+
+
Annual Income (net)
+
{hasData ? fmt(annualIncome) : '—'}
+
+
+
Annual Bills
+
{hasData ? fmt(annualBills) : '—'}
+
+
+
Annual Variable Spending
+
{hasData ? fmt(annualVariable) : '—'}
+
+
+
Annual Surplus / Deficit
+
+ {hasData ? fmt(annualSurplus) : '—'} +
+
+
+ + {/* Status messages */} + {loading &&

Loading…

} + {error &&

Error: {error}

} + + {/* Monthly table */} + + + + + + + + + + + + + + {MONTH_NAMES.map((name, i) => { + const row = monthData[i]; + const hasRow = row != null; + const surplus = hasRow ? row.surplus_deficit : null; + return ( + + + + + + + + + + ); + })} + + + + + + + + + + + + +
MonthIncome (net)BillsVariableOne-timeTotal SpendingSurplus / Deficit
{name} + {hasRow ? fmt(row.total_income) : '—'} + + {hasRow ? fmt(row.total_bills) : '—'} + + {hasRow ? fmt(row.total_variable) : '—'} + + {hasRow ? fmt(row.total_one_time) : '—'} + + {hasRow ? fmt(row.total_spending) : '—'} + + {hasRow ? fmt(surplus) : '—'} +
Total + {hasData ? fmt(annualIncome) : '—'} + + {hasData ? fmt(annualBills) : '—'} + + {hasData ? fmt(annualVariable) : '—'} + + {hasData ? fmt(annualOneTime) : '—'} + + {hasData ? fmt(annualSpending) : '—'} + + {hasData ? fmt(annualSurplus) : '—'} +
+
+ ); +}