import { Link, useNavigate, useParams } from "react-router-dom"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useEffect, useMemo, useState } from "react"; import { accounting } from "@/lib/accountingClient"; import { useCompanyId } from "./lib/useCompanyId"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; import { ArrowLeft, ChevronDown, ChevronRight, MoveRight, Wand2, Save, CheckCircle2 } from "lucide-react"; import { toast } from "sonner"; import { money } from "./lib/format"; const MONTHS = ["JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"]; const QUARTERS = ["Q1","Q2","Q3","Q4"]; function periodLabels(p: string) { if (p === "monthly") return MONTHS; if (p === "quarterly") return QUARTERS; return ["Year"]; } type Grid = Record; export default function AccountingBudgetDetailPage({ basePath = "/dashboard/accounting/budgets" }: { basePath?: string } = {}) { const { id = "" } = useParams(); const { companyId } = useCompanyId(); const cid = companyId ?? ""; const cur = "USD"; const navigate = useNavigate(); const qc = useQueryClient(); const [grid, setGrid] = useState({}); const [expanded, setExpanded] = useState>({ income: true, expense: true }); const [hideZero, setHideZero] = useState(false); const [saving, setSaving] = useState(false); const [tab, setTab] = useState<"summary" | "edit">("summary"); const [summarySection, setSummarySection] = useState<"income" | "expense">("income"); const { data: budget, isLoading: bLoading } = useQuery({ queryKey: ["budget", id], enabled: !!id, queryFn: async () => { const { data, error } = await accounting.from("budgets").select("*").eq("id", id).single(); if (error) console.error("Budget query error:", error); return data; }, }); const { data: accounts = [] } = useQuery({ queryKey: ["accounts", cid], enabled: !!cid, queryFn: async () => (await accounting.from("accounts").select("*").eq("company_id", cid).order("code")).data ?? [], }); const { data: existing = [] } = useQuery({ queryKey: ["budget-entries", id], enabled: !!id, queryFn: async () => (await accounting.from("budget_entries").select("*").eq("budget_id", id)).data ?? [], }); const periods = useMemo(() => periodLabels(budget?.period_type ?? "monthly"), [budget?.period_type]); const periodCount = periods.length; useEffect(() => { if (!(accounts as any[]).length || !budget) return; const next: Grid = {}; for (const a of accounts as any[]) { next[a.id] = Array.from({ length: periodCount }, () => ""); } for (const e of existing as any[]) { if (next[e.account_id] !== undefined && e.period_index < periodCount) { next[e.account_id][e.period_index] = String(e.amount); } } setGrid(next); }, [(accounts as any[]).length, (existing as any[]).length, budget?.id, periodCount]); const grouped = useMemo(() => { const out: Record = {}; for (const a of accounts as any[]) { if (!out[a.type]) out[a.type] = []; out[a.type].push(a); } return out; }, [accounts]); const rowSum = (aid: string) => (grid[aid] ?? []).reduce((s, v) => s + (parseFloat(v) || 0), 0); const rowPeriod = (aid: string, i: number) => parseFloat(grid[aid]?.[i] ?? "") || 0; const groupPeriod = (type: string, i: number) => (grouped[type] ?? []).reduce((s: number, a: any) => s + rowPeriod(a.id, i), 0); const groupTotal = (type: string) => (grouped[type] ?? []).reduce((s: number, a: any) => s + rowSum(a.id), 0); const updateCell = (aid: string, idx: number, val: string) => { setGrid(prev => { const row = [...(prev[aid] ?? Array.from({ length: periodCount }, () => ""))]; row[idx] = val; return { ...prev, [aid]: row }; }); }; // Distribute a FY total evenly across all periods const distributeFYTotal = (aid: string, total: number) => { if (!total || periodCount === 0) return; const perPeriod = (total / periodCount).toFixed(2); setGrid(prev => ({ ...prev, [aid]: Array.from({ length: periodCount }, () => perPeriod) })); }; const fillRow = (aid: string) => { const first = (grid[aid] ?? [])[0]; if (!first) return toast.error("Enter Jan first"); setGrid(prev => ({ ...prev, [aid]: Array.from({ length: periodCount }, () => first) })); }; const autoFill = async () => { if (!budget) return; const year = budget.fiscal_year - 1; const [{ data: inv }, { data: exp }] = await Promise.all([ accounting.from("invoices").select("total,status,issue_date").eq("company_id", cid) .gte("issue_date", `${year}-01-01`).lte("issue_date", `${year}-12-31`), accounting.from("expenses").select("amount,category,date").eq("company_id", cid) .gte("date", `${year}-01-01`).lte("date", `${year}-12-31`), ]); const next: Grid = {}; for (const a of accounts as any[]) { next[a.id] = Array.from({ length: periodCount }, () => ""); } const incomeAccs = grouped.income ?? []; const expAccs = grouped.expense ?? []; for (const i of inv ?? []) { if (i.status !== "paid") continue; const m = new Date(i.issue_date).getMonth(); const b = budget.period_type === "monthly" ? m : budget.period_type === "quarterly" ? Math.floor(m / 3) : 0; const share = Number(i.total) / Math.max(incomeAccs.length, 1); for (const a of incomeAccs) { next[a.id][b] = String((parseFloat(next[a.id][b]) || 0) + share); } } for (const e of exp ?? []) { const m = new Date(e.date).getMonth(); const b = budget.period_type === "monthly" ? m : budget.period_type === "quarterly" ? Math.floor(m / 3) : 0; const match = expAccs.find((a: any) => a.name.toLowerCase() === String(e.category).toLowerCase()); if (match) next[match.id][b] = String((parseFloat(next[match.id][b]) || 0) + Number(e.amount)); } const formatted: Grid = {}; for (const [k, v] of Object.entries(next)) { formatted[k] = v.map(n => parseFloat(n) ? parseFloat(n).toFixed(2) : ""); } setGrid(formatted); toast.success(`Pre-filled from ${year} actuals`); }; const save = async (activate: boolean) => { setSaving(true); await accounting.from("budget_entries").delete().eq("budget_id", id); const rows: any[] = []; for (const [aid, vals] of Object.entries(grid)) { vals.forEach((v, idx) => { const n = parseFloat(v); if (!isNaN(n) && n !== 0) rows.push({ budget_id: id, account_id: aid, period_index: idx, amount: n }); }); } if (rows.length) { const { error } = await accounting.from("budget_entries").insert(rows); if (error) { setSaving(false); return toast.error(error.message); } } await accounting.from("budgets").update({ status: activate ? "active" : "draft" }).eq("id", id); setSaving(false); toast.success(activate ? "Budget activated" : "Saved"); qc.invalidateQueries({ queryKey: ["budget-entries", id] }); qc.invalidateQueries({ queryKey: ["budgets", cid] }); if (activate) navigate(basePath); }; if (bLoading) { return
Loading budget…
; } if (!budget) { return (
Budget not found (ID: {id})
); } const incomeTotal = groupTotal("income"); const expenseTotal = groupTotal("expense"); const netIncome = incomeTotal - expenseTotal; const fyLabel = `FY${budget.fiscal_year}`; return (
{/* Header */}

{budget.name}

{budget.status === "active" ? "Active" : "Draft"}

FY {budget.fiscal_year} · {budget.period_type}

{/* Tab buttons (plain, no Tabs component) */}
{(["summary", "edit"] as const).map(t => ( ))}
{/* Summary */} {tab === "summary" && (
{periods.map(p => )} {periods.map((_, i) => )} {periods.map((_, i) => )} {periods.map((_, i) => { const net = groupPeriod("income", i) - groupPeriod("expense", i); return ; })}
TOTALS{p}{fyLabel}
Expense{groupPeriod("expense", i).toFixed(2)}{money(expenseTotal, cur)}
Income{groupPeriod("income", i).toFixed(2)}{money(incomeTotal, cur)}
Net Income{net.toFixed(2)}{money(netIncome, cur)}
{(["income", "expense"] as const).map(s => ( ))}
{periods.map(p => )} {(grouped[summarySection] ?? []).map((a: any) => { const total = rowSum(a.id); return ( {periods.map((_, i) => ( ))} ); })} {(grouped[summarySection] ?? []).length === 0 && ( )}
{summarySection.toUpperCase()}{p}{fyLabel}
{a.code ? `${a.code} ${a.name}` : a.name} {rowPeriod(a.id, i) > 0 ? rowPeriod(a.id, i).toFixed(2) : "—"} {money(total, cur)}
No {summarySection} accounts yet. Add them in Chart of Accounts.
)} {/* Edit */} {tab === "edit" && (
{(["income", "expense"] as const).map(type => { const accs = grouped[type] ?? []; const isOpen = expanded[type]; return (
setExpanded(p => ({ ...p, [type]: !p[type] }))}>
{isOpen ? : } {type === "income" ? "Income" : "Expenses"} ({accs.length})
{money(groupTotal(type), cur)}
{isOpen && ( {periods.map(p => )} {accs.map((a: any) => { const total = rowSum(a.id); if (hideZero && total === 0) return null; return ( {periods.map((_, idx) => ( ))} ); })} {accs.length === 0 && ( )} {accs.length > 0 && ( {periods.map((_, i) => ( ))} )}
Account{p}{fyLabel}
{a.code ? `${a.code} ${a.name}` : a.name} updateCell(a.id, idx, e.target.value)} className="h-7 text-right tabular-nums px-2 text-xs" placeholder="0.00" /> { const v = parseFloat(e.target.value); if (v > 0) { distributeFYTotal(a.id, v); e.target.value = ""; } }} onKeyDown={(e) => { if (e.key === "Enter") { const v = parseFloat((e.target as HTMLInputElement).value); if (v > 0) { distributeFYTotal(a.id, v); (e.target as HTMLInputElement).value = ""; } } }} />
No accounts. Add them in Chart of Accounts first.
Total{money(groupPeriod(type, i), cur)}{money(groupTotal(type), cur)}
)}
); })}
Net Income {money(netIncome, cur)}
)}
); }