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 && ( + + )} ))}