From 8f8f4598f4a320b8d49d9b42ff1fe7eb7c7181cc Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 01:57:57 +0000 Subject: [PATCH] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/components/cases/ledger-row.tsx | 156 ++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 src/components/cases/ledger-row.tsx diff --git a/src/components/cases/ledger-row.tsx b/src/components/cases/ledger-row.tsx new file mode 100644 index 0000000..e104fe3 --- /dev/null +++ b/src/components/cases/ledger-row.tsx @@ -0,0 +1,156 @@ +import { useEffect, useState } from "react"; +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { GripVertical, Trash2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { BUCKETS, num } from "@/lib/ledger"; +import { formatCurrency } from "@/lib/format"; + +export interface EditableEntry { + id: string; + entry_date: string; + description: string | null; + account: string | null; + assess: number; + late: number; + admin: number; + legal: number; + viol: number; + interest: number; + bank: number; + payment: number; + runningBalance?: number; +} + +interface Props { + entry: EditableEntry; + onPatch: (id: string, patch: Partial) => void | Promise; + onRemove: (id: string) => void; +} + +const NUM_COLS: { key: keyof EditableEntry; danger?: boolean; positive?: boolean }[] = [ + { key: "assess" }, + { key: "late" }, + { key: "admin" }, + { key: "legal" }, + { key: "viol" }, + { key: "interest" }, + { key: "bank", danger: true }, + { key: "payment", positive: true }, +]; + +export function SortableLedgerRow({ entry, onPatch, onRemove }: Props) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + id: entry.id, + }); + const style: React.CSSProperties = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.4 : 1, + backgroundColor: isDragging ? "hsl(var(--muted))" : undefined, + }; + + // Local field state (for typing without re-render thrashing) — committed on blur + const [draft, setDraft] = useState(entry); + useEffect(() => { + setDraft(entry); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [entry.id, entry.entry_date, entry.description, entry.account, + entry.assess, entry.late, entry.admin, entry.legal, + entry.viol, entry.interest, entry.bank, entry.payment]); + + const commit = (key: keyof EditableEntry, raw: string | number) => { + let val: any = raw; + if (NUM_COLS.some((c) => c.key === key)) val = raw === "" ? 0 : Number(raw) || 0; + if (val === (entry as any)[key]) return; + setDraft((d) => ({ ...d, [key]: val })); + onPatch(entry.id, { [key]: val } as any); + }; + + return ( + + + + + + setDraft((d) => ({ ...d, entry_date: e.target.value }))} + onBlur={(e) => commit("entry_date", e.target.value)} + className="w-full bg-transparent border border-transparent hover:border-border focus:border-ring rounded px-1.5 py-1 text-xs outline-none" + /> + + + setDraft((d) => ({ ...d, description: e.target.value }))} + onBlur={(e) => commit("description", e.target.value)} + placeholder="—" + className="w-full bg-transparent border border-transparent hover:border-border focus:border-ring rounded px-1.5 py-1 text-sm outline-none min-w-[180px]" + /> + + + setDraft((d) => ({ ...d, account: e.target.value }))} + onBlur={(e) => commit("account", e.target.value)} + placeholder="—" + className="w-full bg-transparent border border-transparent hover:border-border focus:border-ring rounded px-1.5 py-1 text-xs text-muted-foreground capitalize outline-none min-w-[80px]" + /> + + {NUM_COLS.map(({ key, danger, positive }) => { + const v = num((draft as any)[key]); + return ( + + setDraft((d) => ({ ...d, [key]: e.target.value === "" ? 0 : Number(e.target.value) || 0 }))} + onBlur={(e) => commit(key, e.target.value)} + placeholder="0.00" + className={`w-20 text-right font-mono text-sm bg-transparent border border-transparent hover:border-border focus:border-ring rounded px-1.5 py-1 outline-none ${ + v === 0 + ? "text-muted-foreground/40 placeholder:text-muted-foreground/40" + : danger + ? "text-destructive" + : positive + ? "text-emerald-600" + : "" + }`} + /> + + ); + })} + 0 ? "" : (entry.runningBalance ?? 0) < 0 ? "text-emerald-600" : "text-muted-foreground" + }`}> + {formatCurrency(entry.runningBalance ?? 0)} + + + + + + ); +} + +export const LEDGER_NUM_COLS = NUM_COLS; +export { BUCKETS };