From 422b828cdb9ac6986acb6408075455af64c30a26 Mon Sep 17 00:00:00 2001 From: renee-png Date: Thu, 18 Jun 2026 19:17:59 -0400 Subject: [PATCH] Reconcile screen: edit transactions inline Add an edit (pencil) action to each row in the reconciliation working list, reusing the add dialog in edit mode. On edit, category/vendor are optional (so imported GL register rows can be edited without forcing re-categorization); category/coa are only overwritten when a category is chosen. Co-Authored-By: Claude Opus 4.8 --- .../AccountingReconcileDetailPage.tsx | 72 +++++++++++++++---- 1 file changed, 59 insertions(+), 13 deletions(-) diff --git a/src/pages/accounting/AccountingReconcileDetailPage.tsx b/src/pages/accounting/AccountingReconcileDetailPage.tsx index 3d0dc31..063efb6 100644 --- a/src/pages/accounting/AccountingReconcileDetailPage.tsx +++ b/src/pages/accounting/AccountingReconcileDetailPage.tsx @@ -18,7 +18,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table"; -import { ArrowLeft, CheckCircle2, AlertTriangle, FileDown, Search, Loader2, ArrowUp, ArrowDown, ChevronsUpDown, Plus, Ban } from "lucide-react"; +import { ArrowLeft, CheckCircle2, AlertTriangle, FileDown, Search, Loader2, ArrowUp, ArrowDown, ChevronsUpDown, Plus, Ban, Pencil } from "lucide-react"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; import { money, fmtDate } from "./lib/format"; @@ -93,6 +93,7 @@ export default function AccountingReconcileDetailPage() { // Add a deposit/withdrawal directly from the reconciliation screen. const [addOpen, setAddOpen] = useState(false); + const [editId, setEditId] = useState(null); const [addSaving, setAddSaving] = useState(false); const [addTx, setAddTx] = useState<{ type: "credit" | "debit"; date: string; amount: string; description: string; coa_account_id: string; reference: string; vendor_id: string }>( { type: "credit", date: "", amount: "", description: "", coa_account_id: "", reference: "", vendor_id: "" }, @@ -156,7 +157,7 @@ export default function AccountingReconcileDetailPage() { // and not voided. const { data } = await accounting .from("transactions") - .select("id,date,description,reference,amount,type,cleared,reconciliation_id,voided,vendors(name),customers(name)") + .select("id,date,description,reference,amount,type,cleared,reconciliation_id,voided,coa_account_id,vendor_id,category,vendors(name),customers(name)") .eq("account_id", accountId) .is("reconciliation_id", null) .eq("voided", false) @@ -346,6 +347,7 @@ export default function AccountingReconcileDetailPage() { }; const openAdd = (type: "credit" | "debit") => { + setEditId(null); setAddTx({ type, date: active?.statement_end_date ?? new Date().toLocaleDateString("en-CA", { timeZone: "America/New_York" }), @@ -354,16 +356,53 @@ export default function AccountingReconcileDetailPage() { setAddOpen(true); }; + // Edit an existing transaction from the reconciliation working list. + const openEdit = (t: any) => { + setEditId(t.id); + setAddTx({ + type: t.type, + date: t.date, + amount: String(Math.abs(Number(t.amount))), + description: t.description ?? "", + coa_account_id: t.coa_account_id ?? "", + reference: t.reference ?? "", + vendor_id: t.vendor_id ?? "", + }); + setAddOpen(true); + }; + const addTransaction = async () => { if (!active) return; const amt = Number(addTx.amount); if (!amt || amt <= 0) return toast.error("Enter an amount"); if (!addTx.description.trim()) return toast.error("Enter a description"); - if (!addTx.coa_account_id) return toast.error("Pick a category account"); - if (addTx.type === "debit" && !addTx.vendor_id) return toast.error("Vendor is required for withdrawals"); + // Category/vendor are required when creating; on edit they're optional so + // imported (GL) register rows can be edited without re-categorizing. + if (!editId && !addTx.coa_account_id) return toast.error("Pick a category account"); + if (!editId && addTx.type === "debit" && !addTx.vendor_id) return toast.error("Vendor is required for withdrawals"); setAddSaving(true); try { const coaName = (allAccounts as any[]).find((a) => a.id === addTx.coa_account_id)?.name ?? ""; + if (editId) { + // Only overwrite category/coa when a category was chosen, so we don't + // wipe an imported row's existing category text. + const payload: any = { + date: addTx.date, + description: addTx.description.trim(), + amount: Math.abs(amt), + type: addTx.type, + reference: addTx.reference.trim() || null, + vendor_id: addTx.vendor_id || null, + }; + if (addTx.coa_account_id) { payload.coa_account_id = addTx.coa_account_id; payload.category = coaName; } + const { error } = await accounting.from("transactions").update(payload).eq("id", editId); + if (error) return toast.error(error.message); + setAddOpen(false); + toast.success("Transaction updated"); + qc.invalidateQueries({ queryKey: ["recon-txs", accountId] }); + qc.invalidateQueries({ queryKey: ["transactions", cid] }); + return; + } const { data, error } = await accounting .from("transactions") .insert({ @@ -553,7 +592,7 @@ export default function AccountingReconcileDetailPage() { - Void + Actions @@ -587,10 +626,16 @@ export default function AccountingReconcileDetailPage() { {t.type === "debit" ? money(t.amount, cur) : ""} - +
+ + +
); @@ -787,10 +832,11 @@ export default function AccountingReconcileDetailPage() { - Add {addTx.type === "credit" ? "Deposit" : "Withdrawal"} + {editId ? "Edit" : "Add"} {addTx.type === "credit" ? "Deposit" : "Withdrawal"} - Record a {addTx.type === "credit" ? "deposit (money in)" : "withdrawal (money out)"} on{" "} - {(account as any)?.name ?? "this account"}. It's added to this reconciliation automatically. + {editId + ? <>Edit this {addTx.type === "credit" ? "deposit" : "withdrawal"} on {(account as any)?.name ?? "this account"}. + : <>Record a {addTx.type === "credit" ? "deposit (money in)" : "withdrawal (money out)"} on {(account as any)?.name ?? "this account"}. It's added to this reconciliation automatically.}
@@ -845,7 +891,7 @@ export default function AccountingReconcileDetailPage() {