import { useMemo, useState, type FormEvent } from "react"; import { useCreateIncome } from "../hooks/useIncome"; import { useDashboard } from "../hooks/useDashboard"; import { Money, Field, Button } from "../components/ui"; import CurrencyInput from "../components/CurrencyInput"; import { previewAllocation } from "../utils/allocatorPreview"; import PercentGuard from "../components/PercentGuard"; import { useToast } from "../components/Toast"; import { useIncomePreview } from "../hooks/useIncomePreview"; function dollarsToCents(input: string): number { const n = Number.parseFloat(input || "0"); if (!Number.isFinite(n)) return 0; return Math.round(n * 100); } type Alloc = { id: number | string; amountCents: number; name: string }; export default function IncomePage() { const [amountStr, setAmountStr] = useState(""); const { push } = useToast(); const m = useCreateIncome(); const dash = useDashboard(); const cents = dollarsToCents(amountStr); const canSubmit = (dash.data?.percentTotal ?? 0) === 100; // Server preview (preferred) with client fallback const srvPreview = useIncomePreview(cents); const preview = useMemo(() => { if (!dash.data || cents <= 0) return null; if (srvPreview.data) return srvPreview.data; // fallback: local simulation return previewAllocation(cents, dash.data.fixedPlans, dash.data.variableCategories); }, [cents, dash.data, srvPreview.data]); const submit = (e: FormEvent) => { e.preventDefault(); if (cents <= 0 || !canSubmit) return; m.mutate( { amountCents: cents }, { onSuccess: (res) => { const fixed = (res.fixedAllocations ?? []).reduce( (s: number, a: any) => s + (a.amountCents ?? 0), 0 ); const variable = (res.variableAllocations ?? []).reduce( (s: number, a: any) => s + (a.amountCents ?? 0), 0 ); const unalloc = res.remainingUnallocatedCents ?? 0; push( "ok", `Allocated: Fixed ${(fixed / 100).toFixed(2)} + Variable ${(variable / 100).toFixed( 2 )}. Unallocated ${(unalloc / 100).toFixed(2)}.` ); setAmountStr(""); }, onError: (err: any) => push("err", err?.message ?? "Income failed"), } ); }; const variableAllocations: Alloc[] = useMemo(() => { if (!m.data) return []; const nameById = new Map( (dash.data?.variableCategories ?? []).map((c) => [c.id as string | number, c.name] as const) ); const grouped = new Map(); for (const a of m.data.variableAllocations ?? []) { const id = (a as any).variableCategoryId ?? (a as any).id ?? -1; grouped.set(id, (grouped.get(id) ?? 0) + (a as any).amountCents); } return [...grouped.entries()] .map(([id, amountCents]) => ({ id, amountCents, name: nameById.get(id) ?? `Category #${id}` })) .sort((a, b) => b.amountCents - a.amountCents); }, [m.data, dash.data]); const fixedAllocations: Alloc[] = useMemo(() => { if (!m.data) return []; const nameById = new Map( (dash.data?.fixedPlans ?? []).map((p) => [p.id as string | number, p.name] as const) ); const grouped = new Map(); for (const a of m.data.fixedAllocations ?? []) { const id = (a as any).fixedPlanId ?? (a as any).id ?? -1; grouped.set(id, (grouped.get(id) ?? 0) + (a as any).amountCents); } return [...grouped.entries()] .map(([id, amountCents]) => ({ id, amountCents, name: nameById.get(id) ?? `Plan #${id}` })) .sort((a, b) => b.amountCents - a.amountCents); }, [m.data, dash.data]); const hasResult = !!m.data; return (

Record Income

{/* Live Preview */} {!hasResult && preview && (

Preview (not yet applied)

Unallocated:

Fixed Plans

s + x.amountCents, 0)} />
{preview.fixed.length === 0 ? (
No fixed allocations.
) : (
    {preview.fixed.map((a) => (
  • {a.name}
  • ))}
)}

Variable Categories

s + x.amountCents, 0)} />
{preview.variable.length === 0 ? (
No variable allocations.
) : (
    {preview.variable.map((a) => (
  • {a.name}
  • ))}
)}
)} {/* Actual Result */} {m.error &&
⚠️ {(m.error as any).message}
} {hasResult && (
Unallocated

Fixed Plans (Applied)

s + x.amountCents, 0)} />
    {fixedAllocations.map((a) => (
  • {a.name}
  • ))}

Variable Categories (Applied)

s + x.amountCents, 0)} />
    {variableAllocations.map((a) => (
  • {a.name}
  • ))}
)}
); }