import { useState, useMemo } 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 { useCanEdit, CAPABILITIES } from '../../permissions'; 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']; 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 [editData, setEditData] = useState(null); // null = not editing const queryClient = useQueryClient(); const navigate = useNavigate(); const isReadOnly = !useCanEdit(CAPABILITIES.FINANCIALS_BUDGETS_EDIT); 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'; // 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}`); 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 .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] }); setEditData(null); notifications.show({ message: 'Budget saved', color: 'green' }); }, onError: (err: any) => { notifications.show({ message: err.response?.data?.message || 'Save failed', color: 'red' }); }, }); const updateCell = (idx: number, month: string, value: number) => { 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); setEditData(updated); }; 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'); 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