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 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 17:07:54 -04:00
parent c8cb583ec9
commit c94057c433
@@ -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<string | null>(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 <p className="text-sm text-muted-foreground">Select an association.</p>;
if (companyLoading) return <div className="flex justify-center py-12"><Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /></div>;
if (companyError || !companyId) return <p className="text-sm text-muted-foreground text-center py-12">{companyError || "Accounting setup is not ready."}</p>;
@@ -781,6 +813,21 @@ export default function AccountingReconcileDetailPage() {
}}>
<FileDown className="h-4 w-4 mr-1" />View Report
</Button>
{h.status === "completed" && h.id === latestCompletedId && (
<Button
size="sm"
variant="ghost"
className="text-destructive hover:text-destructive"
disabled={undoingId === h.id}
onClick={() => undoReconciliation(h)}
title="Reopen this period and remove the reconciliation"
>
{undoingId === h.id
? <Loader2 className="h-4 w-4 mr-1 animate-spin" />
: <RotateCcw className="h-4 w-4 mr-1" />}
Undo
</Button>
)}
</TableCell>
</TableRow>
))}