diff --git a/frontend/src/pages/budgets/BudgetsPage.tsx b/frontend/src/pages/budgets/BudgetsPage.tsx index 6cb3b28..9ca68e4 100644 --- a/frontend/src/pages/budgets/BudgetsPage.tsx +++ b/frontend/src/pages/budgets/BudgetsPage.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { Title, Table, Group, Button, Stack, Text, NumberInput, Select, Loader, Center, Badge, Card, Alert, @@ -26,10 +26,6 @@ interface BudgetLine { const months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec_amt']; const monthLabels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; -/** - * 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) { @@ -41,8 +37,7 @@ function hydrateBudgetLine(row: any): BudgetLine { export function BudgetsPage() { const [year, setYear] = useState(new Date().getFullYear().toString()); - const [budgetData, setBudgetData] = useState([]); - const [isEditing, setIsEditing] = useState(false); + const [editData, setEditData] = useState(null); // null = not editing const queryClient = useQueryClient(); const navigate = useNavigate(); const isReadOnly = useIsReadOnly(); @@ -52,20 +47,36 @@ export function BudgetsPage() { const incomeSectionBg = isDark ? 'var(--mantine-color-green-9)' : '#e6f9e6'; const expenseSectionBg = isDark ? 'var(--mantine-color-red-9)' : '#fde8e8'; - const hasBudget = budgetData.length > 0; - const cellsEditable = !isReadOnly && isEditing; - - const { isLoading } = useQuery({ + // Query is the single source of truth for budget data + const { data: queryData, isLoading, isFetching } = useQuery({ queryKey: ['budgets', year], queryFn: async () => { const { data } = await api.get(`/budgets/${year}`); - const hydrated = (data as any[]).map(hydrateBudgetLine); - setBudgetData(hydrated); - setIsEditing(false); - return hydrated; + return (data as any[]).map(hydrateBudgetLine); }, }); + // Use edit data when editing, otherwise use query data + const isEditing = editData !== null; + const budgetData = isEditing ? editData : (queryData || []); + const hasBudget = budgetData.length > 0; + const cellsEditable = !isReadOnly && isEditing; + + const handleStartEdit = () => { + setEditData(queryData ? [...queryData] : []); + }; + + const handleCancelEdit = () => { + setEditData(null); + }; + + const handleYearChange = (v: string | null) => { + if (v) { + setYear(v); + setEditData(null); // Cancel any in-progress edit when switching years + } + }; + const saveMutation = useMutation({ mutationFn: async () => { const payload = budgetData @@ -81,7 +92,7 @@ export function BudgetsPage() { }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['budgets', year] }); - setIsEditing(false); + setEditData(null); notifications.show({ message: 'Budget saved', color: 'green' }); }, onError: (err: any) => { @@ -89,25 +100,22 @@ export function BudgetsPage() { }, }); - const handleCancelEdit = () => { - setIsEditing(false); - queryClient.invalidateQueries({ queryKey: ['budgets', year] }); - }; - const updateCell = (idx: number, month: string, value: number) => { - const updated = [...budgetData]; + if (!editData) return; + const updated = [...editData]; (updated[idx] as any)[month] = value || 0; updated[idx].annual_total = months.reduce((s, m) => s + ((updated[idx] as any)[m] || 0), 0); - setBudgetData(updated); + setEditData(updated); }; - const yearOptions = Array.from({ length: 5 }, (_, i) => { + const yearOptions = useMemo(() => Array.from({ length: 5 }, (_, i) => { const y = new Date().getFullYear() - 1 + i; return { value: String(y), label: String(y) }; - }); + }), []); const fmt = (v: number) => v.toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0 }); + // Show loader on initial load or when switching years with no cached data if (isLoading) return
; const incomeLines = budgetData.filter((b) => b.account_type === 'income'); @@ -123,14 +131,15 @@ export function BudgetsPage() { Budget Manager - + {isFetching && !isLoading && } {!isReadOnly && hasBudget && ( <> {!isEditing ? (