import { useState } from 'react'; import { Title, Table, Group, Button, Stack, Text, NumberInput, Select, Loader, Center, Badge, Card, Alert, } from '@mantine/core'; import { notifications } from '@mantine/notifications'; import { IconDeviceFloppy, IconInfoCircle, IconPencil, IconX, IconArrowRight } from '@tabler/icons-react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import api from '../../services/api'; import { useIsReadOnly } from '../../stores/authStore'; import { usePreferencesStore } from '../../stores/preferencesStore'; interface BudgetLine { account_id: string; account_number: string; account_name: string; account_type: string; fund_type: string; jan: number; feb: number; mar: number; apr: number; may: number; jun: number; jul: number; aug: number; sep: number; oct: number; nov: number; dec_amt: number; annual_total: number; } 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) { line[m] = Number(line[m]) || 0; } line.annual_total = months.reduce((sum, m) => sum + (line[m] || 0), 0); return line as BudgetLine; } export function BudgetsPage() { const [year, setYear] = useState(new Date().getFullYear().toString()); const [budgetData, setBudgetData] = useState([]); const [isEditing, setIsEditing] = useState(false); const queryClient = useQueryClient(); const navigate = useNavigate(); const isReadOnly = useIsReadOnly(); const isDark = usePreferencesStore((s) => s.colorScheme) === 'dark'; const stickyBg = isDark ? 'var(--mantine-color-dark-7)' : 'white'; const stickyBorder = isDark ? 'var(--mantine-color-dark-4)' : '#e9ecef'; 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({ 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; }, }); const saveMutation = useMutation({ mutationFn: async () => { const payload = budgetData .filter((b) => months.some((m) => (b as any)[m] > 0)) .map((b) => ({ accountId: b.account_id, fundType: b.fund_type, jan: b.jan, feb: b.feb, mar: b.mar, apr: b.apr, may: b.may, jun: b.jun, jul: b.jul, aug: b.aug, sep: b.sep, oct: b.oct, nov: b.nov, dec: b.dec_amt, })); return api.put(`/budgets/${year}`, payload); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['budgets', year] }); setIsEditing(false); notifications.show({ message: 'Budget saved', color: 'green' }); }, onError: (err: any) => { notifications.show({ message: err.response?.data?.message || 'Save failed', color: 'red' }); }, }); const handleCancelEdit = () => { setIsEditing(false); queryClient.invalidateQueries({ queryKey: ['budgets', year] }); }; const updateCell = (idx: number, month: string, value: number) => { const updated = [...budgetData]; (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); }; const yearOptions = 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 }); if (isLoading) return
; const incomeLines = budgetData.filter((b) => b.account_type === 'income'); const operatingIncomeLines = incomeLines.filter((b) => b.fund_type === 'operating'); const reserveIncomeLines = incomeLines.filter((b) => b.fund_type === 'reserve'); const expenseLines = budgetData.filter((b) => b.account_type === 'expense'); const totalOperatingIncome = operatingIncomeLines.reduce((sum, line) => sum + (line.annual_total || 0), 0); const totalReserveIncome = reserveIncomeLines.reduce((sum, line) => sum + (line.annual_total || 0), 0); const totalExpense = expenseLines.reduce((sum, line) => sum + (line.annual_total || 0), 0); return ( Budget Manager