import { useState } from 'react'; import { Title, Table, Group, Stack, Text, Card, Loader, Center, Select, Badge, Progress, SimpleGrid, } from '@mantine/core'; import { useQuery } from '@tanstack/react-query'; import api from '../../services/api'; import { usePreferencesStore } from '../../stores/preferencesStore'; interface BudgetVsActualLine { account_id: string; account_number: string; account_name: string; account_type: string; fund_type: string; budget_amount: number; actual_amount: number; variance: number; variance_pct: number; } interface BudgetVsActualData { year: number; lines: BudgetVsActualLine[]; total_income_budget: number; total_income_actual: number; total_expense_budget: number; total_expense_actual: number; } const monthFilterOptions = [ { value: '', label: 'Full Year' }, { value: '1', label: 'January' }, { value: '2', label: 'February' }, { value: '3', label: 'March' }, { value: '4', label: 'April' }, { value: '5', label: 'May' }, { value: '6', label: 'June' }, { value: '7', label: 'July' }, { value: '8', label: 'August' }, { value: '9', label: 'September' }, { value: '10', label: 'October' }, { value: '11', label: 'November' }, { value: '12', label: 'December' }, ]; export function BudgetVsActualPage() { const [year, setYear] = useState(new Date().getFullYear().toString()); const [month, setMonth] = useState(''); const isDark = usePreferencesStore((s) => s.colorScheme) === 'dark'; const incomeBg = isDark ? 'var(--mantine-color-green-9)' : '#e6f9e6'; const expenseBg = isDark ? 'var(--mantine-color-red-9)' : '#fde8e8'; const yearOptions = Array.from({ length: 5 }, (_, i) => { const y = new Date().getFullYear() - 2 + i; return { value: String(y), label: String(y) }; }); const { data, isLoading } = useQuery({ queryKey: ['budget-vs-actual', year, month], queryFn: async () => { const params = month ? `?month=${month}` : ''; const { data } = await api.get(`/budgets/${year}/vs-actual${params}`); return data; }, }); const fmt = (v: number) => (v || 0).toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0 }); const pctFmt = (v: number) => `${(v || 0).toFixed(1)}%`; if (isLoading) return
; const lines = data?.lines || []; const incomeLines = lines.filter((l) => l.account_type === 'income'); const expenseLines = lines.filter((l) => l.account_type === 'expense'); const totalIncomeBudget = data?.total_income_budget || incomeLines.reduce((s, l) => s + l.budget_amount, 0); const totalIncomeActual = data?.total_income_actual || incomeLines.reduce((s, l) => s + l.actual_amount, 0); const totalExpenseBudget = data?.total_expense_budget || expenseLines.reduce((s, l) => s + l.budget_amount, 0); const totalExpenseActual = data?.total_expense_actual || expenseLines.reduce((s, l) => s + l.actual_amount, 0); const incomeVariance = totalIncomeActual - totalIncomeBudget; const expenseVariance = totalExpenseActual - totalExpenseBudget; const netBudget = totalIncomeBudget - totalExpenseBudget; const netActual = totalIncomeActual - totalExpenseActual; const varianceColor = (variance: number, isExpense: boolean) => { if (variance === 0) return 'gray'; // For income: positive variance (actual > budget) is good // For expenses: negative variance (actual < budget) is good if (isExpense) return variance < 0 ? 'green' : 'red'; return variance > 0 ? 'green' : 'red'; }; const renderSection = (title: string, sectionLines: BudgetVsActualLine[], isExpense: boolean, totalBudget: number, totalActual: number) => ( <> {title} {sectionLines.map((line) => { const usagePct = line.budget_amount > 0 ? (line.actual_amount / line.budget_amount) * 100 : 0; return ( {line.account_number} {line.account_name} {line.fund_type === 'reserve' && R} {fmt(line.budget_amount)} {fmt(line.actual_amount)} {line.variance > 0 ? '+' : ''}{fmt(line.variance)} {line.budget_amount > 0 ? pctFmt(line.variance_pct) : '—'} {line.budget_amount > 0 && ( 100 ? (isExpense ? 'red' : 'green') : (isExpense ? 'green' : 'yellow')} /> )} ); })} Total {title} {fmt(totalBudget)} {fmt(totalActual)} {(totalActual - totalBudget) > 0 ? '+' : ''}{fmt(totalActual - totalBudget)} {totalBudget > 0 ? pctFmt(((totalActual - totalBudget) / totalBudget) * 100) : '—'} ); return ( Budget vs. Actual setMonth(v || '')} w={150} placeholder="Month" clearable={false} /> Income Variance = 0 ? 'green' : 'red'}> {incomeVariance >= 0 ? '+' : ''}{fmt(incomeVariance)} {fmt(totalIncomeActual)} of {fmt(totalIncomeBudget)} budgeted Expense Variance {expenseVariance >= 0 ? '+' : ''}{fmt(expenseVariance)} {fmt(totalExpenseActual)} of {fmt(totalExpenseBudget)} budgeted Net Budget = 0 ? 'green' : 'red'}>{fmt(netBudget)} Net Actual = 0 ? 'green' : 'red'}>{fmt(netActual)} Account Budget Actual Variance ($) Variance % Progress {lines.length === 0 && ( No budget vs actual data available. Create a budget first. )} {incomeLines.length > 0 && renderSection('Income', incomeLines, false, totalIncomeBudget, totalIncomeActual)} {expenseLines.length > 0 && renderSection('Expenses', expenseLines, true, totalExpenseBudget, totalExpenseActual)}
); }