import { useEffect, useState } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { CategoryPicker } from "@/components/elements/CategoryPicker"; import { useMoney } from "@/hooks/useMoney"; import { UNCATEGORIZED } from "@/lib/categories"; import type { SplitDto, SplitInput } from "@/lib/api"; import { Plus, X } from "lucide-react"; import { cn } from "@/lib/utils"; interface Row { category: string; amount: string; } const emptyRow = (): Row => ({ category: UNCATEGORIZED, amount: "" }); interface Props { open: boolean; onOpenChange: (open: boolean) => void; /** The parent transaction's signed amount; splits target its magnitude. */ transactionAmount: number; initialSplits: SplitDto[]; onSave: (splits: SplitInput[]) => Promise; saving?: boolean; } /** * Divides one transaction's amount across several categories. Sends the * whole set on save (set_transaction_splits replaces it wholesale), so this * always starts from the current splits (or two blank rows for a first-time * split) rather than diffing individual add/remove operations. */ export function SplitEditor({ open, onOpenChange, transactionAmount, initialSplits, onSave, saving }: Props) { const { moneyAbs } = useMoney(); const [rows, setRows] = useState([emptyRow(), emptyRow()]); const target = Math.abs(transactionAmount); useEffect(() => { if (!open) return; setRows( initialSplits.length > 0 ? initialSplits.map((s) => ({ category: s.category || UNCATEGORIZED, amount: String(s.amount) })) : [emptyRow(), emptyRow()] ); }, [open, initialSplits]); const sum = rows.reduce((n, r) => n + (parseFloat(r.amount) || 0), 0); const remaining = Math.round((target - sum) * 100) / 100; const balanced = Math.abs(remaining) < 0.01; const rowsValid = rows.every((r) => r.category && (parseFloat(r.amount) || 0) > 0); const canSave = rows.length >= 2 && rowsValid && balanced; const update = (i: number, patch: Partial) => setRows((prev) => prev.map((r, idx) => (idx === i ? { ...r, ...patch } : r))); const addRow = () => setRows((prev) => [...prev, emptyRow()]); const removeRow = (i: number) => setRows((prev) => prev.filter((_, idx) => idx !== i)); const save = async () => { if (!canSave) return; await onSave(rows.map((r) => ({ category: r.category, amount: Math.round(parseFloat(r.amount) * 100) / 100 }))); onOpenChange(false); }; const removeSplit = async () => { await onSave([]); onOpenChange(false); }; return ( Split transaction
{rows.map((row, i) => (
update(i, { category: c })} triggerClassName="h-9 flex-1" /> update(i, { amount: e.target.value })} className="w-24 shrink-0" />
))}
Total: {moneyAbs(target)} {balanced ? "Balanced" : remaining > 0 ? `${moneyAbs(remaining)} left` : `${moneyAbs(Math.abs(remaining))} over`}
{initialSplits.length > 0 ? ( ) : }
); }