import { useState, useRef, useEffect, Fragment } from "react"; import { useTranslation } from "react-i18next"; import { AlertTriangle } from "lucide-react"; import type { BudgetYearRow } from "../../shared/types"; const fmt = new Intl.NumberFormat("en-CA", { style: "currency", currency: "CAD", minimumFractionDigits: 0, maximumFractionDigits: 0, }); const MONTH_KEYS = [ "months.jan", "months.feb", "months.mar", "months.apr", "months.may", "months.jun", "months.jul", "months.aug", "months.sep", "months.oct", "months.nov", "months.dec", ] as const; interface BudgetTableProps { rows: BudgetYearRow[]; onUpdatePlanned: (categoryId: number, month: number, amount: number) => void; onSplitEvenly: (categoryId: number, annualAmount: number) => void; } export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: BudgetTableProps) { const { t } = useTranslation(); const [editingCell, setEditingCell] = useState<{ categoryId: number; monthIdx: number } | null>(null); const [editingAnnual, setEditingAnnual] = useState<{ categoryId: number } | null>(null); const [editingValue, setEditingValue] = useState(""); const inputRef = useRef(null); const annualInputRef = useRef(null); useEffect(() => { if (editingCell && inputRef.current) { inputRef.current.focus(); inputRef.current.select(); } }, [editingCell]); useEffect(() => { if (editingAnnual && annualInputRef.current) { annualInputRef.current.focus(); annualInputRef.current.select(); } }, [editingAnnual]); const handleStartEdit = (categoryId: number, monthIdx: number, currentValue: number) => { setEditingAnnual(null); setEditingCell({ categoryId, monthIdx }); setEditingValue(currentValue === 0 ? "" : String(currentValue)); }; const handleStartEditAnnual = (categoryId: number, currentValue: number) => { setEditingCell(null); setEditingAnnual({ categoryId }); setEditingValue(currentValue === 0 ? "" : String(currentValue)); }; const handleSave = () => { if (!editingCell) return; const amount = parseFloat(editingValue) || 0; onUpdatePlanned(editingCell.categoryId, editingCell.monthIdx + 1, amount); setEditingCell(null); }; const handleSaveAnnual = () => { if (!editingAnnual) return; const amount = parseFloat(editingValue) || 0; onSplitEvenly(editingAnnual.categoryId, amount); setEditingAnnual(null); }; const handleCancel = () => { setEditingCell(null); setEditingAnnual(null); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") handleSave(); if (e.key === "Escape") handleCancel(); if (e.key === "Tab") { e.preventDefault(); if (!editingCell) return; const amount = parseFloat(editingValue) || 0; onUpdatePlanned(editingCell.categoryId, editingCell.monthIdx + 1, amount); // Move to next month cell const nextMonth = editingCell.monthIdx + (e.shiftKey ? -1 : 1); if (nextMonth >= 0 && nextMonth < 12) { const row = rows.find((r) => r.category_id === editingCell.categoryId); if (row) { handleStartEdit(editingCell.categoryId, nextMonth, row.months[nextMonth]); } } else { setEditingCell(null); } } }; const handleAnnualKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") handleSaveAnnual(); if (e.key === "Escape") handleCancel(); }; // Group rows by type const grouped: Record = {}; for (const row of rows) { const key = row.category_type; if (!grouped[key]) grouped[key] = []; grouped[key].push(row); } const typeOrder = ["expense", "income", "transfer"] as const; const typeLabelKeys: Record = { expense: "budget.expenses", income: "budget.income", transfer: "budget.transfers", }; // Column totals const monthTotals: number[] = Array(12).fill(0); let annualTotal = 0; for (const row of rows) { for (let m = 0; m < 12; m++) { monthTotals[m] += row.months[m]; } annualTotal += row.annual; } const totalCols = 14; // category + annual + 12 months if (rows.length === 0) { return (

{t("budget.noCategories")}

); } return (
{MONTH_KEYS.map((key) => ( ))} {typeOrder.map((type) => { const group = grouped[type]; if (!group || group.length === 0) return null; return ( {group.map((row) => ( {/* Category name - sticky */} {/* Annual total — editable */} {/* 12 month cells */} {row.months.map((val, mIdx) => ( ))} ))} ); })} {/* Totals row */} {monthTotals.map((total, mIdx) => ( ))}
{t("budget.category")} {t("budget.annual")} {t(key)}
{t(typeLabelKeys[type])}
{row.category_name}
{editingAnnual?.categoryId === row.category_id ? ( setEditingValue(e.target.value)} onBlur={handleSaveAnnual} onKeyDown={handleAnnualKeyDown} className="w-full text-right bg-[var(--background)] border border-[var(--border)] rounded px-1 py-0.5 text-xs focus:outline-none focus:ring-1 focus:ring-[var(--primary)]" /> ) : (
{(() => { const monthSum = row.months.reduce((s, v) => s + v, 0); return row.annual !== 0 && Math.abs(row.annual - monthSum) > 0.01 ? ( ) : null; })()}
)}
{editingCell?.categoryId === row.category_id && editingCell.monthIdx === mIdx ? ( setEditingValue(e.target.value)} onBlur={handleSave} onKeyDown={handleKeyDown} className="w-full text-right bg-[var(--background)] border border-[var(--border)] rounded px-1 py-0.5 text-xs focus:outline-none focus:ring-1 focus:ring-[var(--primary)]" /> ) : ( )}
{t("common.total")} {fmt.format(annualTotal)} {fmt.format(total)}
); }