From c94057c433b51a6826e695a11df0183144a61a64 Mon Sep 17 00:00:00 2001 From: renee-png Date: Fri, 19 Jun 2026 17:07:54 -0400 Subject: [PATCH] Reconciliation: allow undoing the most recent reconciliation Adds an Undo action in the reconciliation History tab. Undo reopens every item the reconciliation cleared (clears the cleared flag and reconciliation_id) and deletes the reconciliation record, so the period can be reconciled again. No transactions are deleted. Only the latest completed reconciliation is undoable, since reopening an earlier one would invalidate the opening balance all later periods were built on. Co-Authored-By: Claude Opus 4.8 --- .../AccountingReconcileDetailPage.tsx | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/pages/accounting/AccountingReconcileDetailPage.tsx b/src/pages/accounting/AccountingReconcileDetailPage.tsx index 82da21d..b43b384 100644 --- a/src/pages/accounting/AccountingReconcileDetailPage.tsx +++ b/src/pages/accounting/AccountingReconcileDetailPage.tsx @@ -18,7 +18,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, SelectGroup, SelectLabel, } 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, Pencil } from "lucide-react"; +import { ArrowLeft, CheckCircle2, AlertTriangle, FileDown, Search, Loader2, ArrowUp, ArrowDown, ChevronsUpDown, Plus, Ban, Pencil, RotateCcw } from "lucide-react"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; import { money, fmtDate } from "./lib/format"; @@ -512,6 +512,38 @@ export default function AccountingReconcileDetailPage() { qc.invalidateQueries({ queryKey: ["transactions", cid] }); }; + // Undo a completed reconciliation: reopen its items (clear flag + link removed) + // and delete the reconciliation record so the period can be reconciled again. + // Only the most recent reconciliation may be undone — undoing an earlier one + // would invalidate the opening balance every later period was built on. + const [undoingId, setUndoingId] = useState(null); + const undoReconciliation = async (h: any) => { + if (!window.confirm( + `Undo the reconciliation for ${fmtDate(h.statement_end_date)}?\n\n` + + `Its ${money(h.statement_balance, cur)} statement will be removed and all items it cleared will be reopened so you can reconcile the period again. This does not delete any transactions.` + )) return; + setUndoingId(h.id); + try { + const { error: txErr } = await accounting + .from("transactions") + .update({ cleared: false, reconciliation_id: null }) + .eq("reconciliation_id", h.id); + if (txErr) { toast.error(txErr.message); return; } + const { error: recErr } = await accounting.from("reconciliations").delete().eq("id", h.id); + if (recErr) { toast.error(recErr.message); return; } + toast.success("Reconciliation undone — the period is open again."); + qc.invalidateQueries({ queryKey: ["recon-history", accountId] }); + qc.invalidateQueries({ queryKey: ["recon-txs", accountId] }); + qc.invalidateQueries({ queryKey: ["recon-last", cid] }); + qc.invalidateQueries({ queryKey: ["recon-accounts", cid] }); + qc.invalidateQueries({ queryKey: ["account", accountId] }); + } finally { + setUndoingId(null); + } + }; + // The single most-recent completed reconciliation (history is sorted desc). + const latestCompletedId = (history as any[]).find((h: any) => h.status === "completed")?.id ?? null; + if (!associationId) return

Select an association.

; if (companyLoading) return
; if (companyError || !companyId) return

{companyError || "Accounting setup is not ready."}

; @@ -781,6 +813,21 @@ export default function AccountingReconcileDetailPage() { }}> View Report + {h.status === "completed" && h.id === latestCompletedId && ( + + )} ))}