import { Link, useParams } from "react-router-dom"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useMemo, useState, useEffect } from "react"; import { accounting } from "@/lib/accountingClient"; import { useCompanyId } from "./lib/useCompanyId"; import { useAuth } from "@/contexts/AuthContext"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Badge } from "@/components/ui/badge"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, } from "@/components/ui/dialog"; 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 } from "lucide-react"; import { toast } from "sonner"; import { money, fmtDate } from "./lib/format"; import { renderReconciliationPdf, type ReconReportData } from "./lib/reconciliationPdf"; type Tx = { id: string; date: string; description: string; reference: string | null; amount: number; type: "debit" | "credit"; cleared: boolean; reconciliation_id: string | null; }; export default function AccountingReconcileDetailPage() { const { accountId = "" } = useParams(); const { companyId, loading: companyLoading, error: companyError, associationId, associationName } = useCompanyId(); const { user } = useAuth(); const cid = companyId ?? ""; const cur = "USD"; const qc = useQueryClient(); const [setupOpen, setSetupOpen] = useState(false); const [setup, setSetup] = useState({ statement_end_date: new Date().toLocaleDateString("en-CA", { timeZone: "America/New_York" }), statement_balance: 0, }); const [active, setActive] = useState<{ statement_end_date: string; statement_balance: number; opening_balance: number; } | null>(null); const [search, setSearch] = useState(""); const [filter, setFilter] = useState<"all" | "deposits" | "withdrawals">("all"); const [checked, setChecked] = useState>(new Set()); const [successOpen, setSuccessOpen] = useState(false); const [successData, setSuccessData] = useState(null); const [adjOpen, setAdjOpen] = useState(false); const [adjAccount, setAdjAccount] = useState(""); const [adjNote, setAdjNote] = useState("Bank reconciliation adjustment"); const { data: account } = useQuery({ queryKey: ["account", accountId], enabled: !!accountId, queryFn: async () => (await accounting.from("accounts").select("*").eq("id", accountId).single()).data, }); const { data: history = [] } = useQuery({ queryKey: ["recon-history", accountId], enabled: !!accountId, queryFn: async () => ( await accounting .from("reconciliations") .select("*") .eq("account_id", accountId) .order("statement_end_date", { ascending: false }) ).data ?? [], }); const lastCompleted = (history as any[]).find((h: any) => h.status === "completed"); const openingBalance = active?.opening_balance ?? Number(lastCompleted?.statement_balance ?? 0); const { data: allAccounts = [] } = useQuery({ queryKey: ["accounts", cid], enabled: !!cid, queryFn: async () => (await accounting.from("accounts").select("id,name,type").eq("company_id", cid).order("name")).data ?? [], }); const { data: txs = [] } = useQuery({ queryKey: ["recon-txs", accountId, active?.statement_end_date], enabled: !!accountId && !!active, queryFn: async () => { const { data } = await accounting .from("transactions") .select("id,date,description,reference,amount,type,cleared,reconciliation_id") .eq("account_id", accountId) .is("reconciliation_id", null) .lte("date", active!.statement_end_date) .order("date"); return (data ?? []) as Tx[]; }, }); // Preload checked from already-cleared rows useEffect(() => { if (active) { setChecked(new Set((txs as Tx[]).filter((t) => t.cleared).map((t) => t.id))); } }, [active, txs]); const filtered = useMemo(() => { const s = search.toLowerCase().trim(); return (txs as Tx[]).filter((t) => { if (filter === "deposits" && t.type !== "credit") return false; if (filter === "withdrawals" && t.type !== "debit") return false; if (s && !`${t.description} ${t.reference ?? ""}`.toLowerCase().includes(s)) return false; return true; }); }, [txs, search, filter]); const clearedDeposits = useMemo( () => (txs as Tx[]).filter((t) => checked.has(t.id) && t.type === "credit") .reduce((s, t) => s + Number(t.amount), 0), [txs, checked], ); const clearedWithdrawals = useMemo( () => (txs as Tx[]).filter((t) => checked.has(t.id) && t.type === "debit") .reduce((s, t) => s + Number(t.amount), 0), [txs, checked], ); const clearedBalance = openingBalance + clearedDeposits - clearedWithdrawals; const difference = Number(active?.statement_balance ?? 0) - clearedBalance; const balanced = Math.abs(difference) < 0.005; const startReconciling = () => { if (!setup.statement_end_date) return toast.error("Statement date required"); setActive({ statement_end_date: setup.statement_end_date, statement_balance: Number(setup.statement_balance) || 0, opening_balance: Number(lastCompleted?.statement_balance ?? 0), }); setSetupOpen(false); }; const toggleAll = (on: boolean) => { if (on) setChecked(new Set(filtered.map((t) => t.id))); else setChecked(new Set()); }; const saveForLater = async () => { if (!active) return; const ids = Array.from(checked); if (ids.length === 0) return toast.error("No transactions selected"); const { error } = await accounting.from("transactions").update({ cleared: true }).in("id", ids); if (error) return toast.error(error.message); toast.success("Progress saved"); qc.invalidateQueries({ queryKey: ["recon-txs", accountId] }); }; const finish = async () => { if (!active || !balanced) return; if (!cid) return toast.error("No company selected"); const ids = Array.from(checked); const { data: rec, error: recErr } = await accounting .from("reconciliations") .insert({ company_id: cid, account_id: accountId, statement_end_date: active.statement_end_date, statement_balance: active.statement_balance, opening_balance: active.opening_balance, cleared_deposits: clearedDeposits, cleared_withdrawals: clearedWithdrawals, status: "completed", completed_at: new Date().toISOString(), completed_by: user?.id ?? null, }) .select() .single(); if (recErr || !rec) return toast.error(recErr?.message ?? "Failed to save reconciliation"); if (ids.length > 0) { const { error: txErr } = await accounting .from("transactions") .update({ cleared: true, reconciliation_id: rec.id }) .in("id", ids); if (txErr) { toast.error(`Reconciliation saved but transaction update failed: ${txErr.message}`); } } const mapRow = (t: Tx) => ({ name: t.description ?? "", date: t.date, number: t.reference ?? "", memo: "", amount: Number(t.amount) }); const clearedDepRows = (txs as Tx[]).filter((t) => checked.has(t.id) && t.type === "credit").map(mapRow); const clearedWdRows = (txs as Tx[]).filter((t) => checked.has(t.id) && t.type === "debit").map(mapRow); const unclearedDepRows = (txs as Tx[]).filter((t) => !checked.has(t.id) && t.type === "credit").map(mapRow); const unclearedWdRows = (txs as Tx[]).filter((t) => !checked.has(t.id) && t.type === "debit").map(mapRow); const sumAmt = (rs: { amount: number }[]) => rs.reduce((s, r) => s + r.amount, 0); const begin = Number(active.opening_balance); const ending = begin + sumAmt(clearedDepRows) - sumAmt(clearedWdRows); const book = ending + sumAmt(unclearedDepRows) - sumAmt(unclearedWdRows); const acctLabel = (account as any)?.code ? `${(account as any).code} ${(account as any).name}` : ((account as any)?.name ?? "Account"); setSuccessData({ companyName: associationName ?? "Company", accountName: acctLabel, statementEndDate: active.statement_end_date, beginningBalance: begin, endingBalance: ending, bookBalance: book, clearedDeposits: clearedDepRows, clearedWithdrawals: clearedWdRows, unclearedDeposits: unclearedDepRows, unclearedWithdrawals: unclearedWdRows, preparedBy: user?.email ?? "—", currency: cur, completedAt: new Date().toISOString(), }); setSuccessOpen(true); setActive(null); setChecked(new Set()); qc.invalidateQueries({ queryKey: ["recon-history", accountId] }); qc.invalidateQueries({ queryKey: ["recon-last", cid] }); qc.invalidateQueries({ queryKey: ["recon-txs", accountId] }); }; const postAdjustment = async () => { if (!active || !adjAccount) return toast.error("Pick an account"); const adjAmount = difference; // sign: positive if statement > cleared const type: "credit" | "debit" = adjAmount >= 0 ? "credit" : "debit"; const { data, error } = await accounting .from("transactions") .insert({ company_id: cid, account_id: accountId, date: active.statement_end_date, description: adjNote || "Reconciliation adjustment", amount: Math.abs(adjAmount), type, cleared: true, reference: "ADJ", category: "Adjustment", }) .select() .single(); if (error || !data) return toast.error(error?.message ?? "Failed"); // Offset entry to chosen account await accounting.from("transactions").insert({ company_id: cid, account_id: adjAccount, date: active.statement_end_date, description: adjNote || "Reconciliation adjustment", amount: Math.abs(adjAmount), type: type === "credit" ? "debit" : "credit", reference: "ADJ", category: "Adjustment", }); setAdjOpen(false); toast.success("Adjustment posted"); qc.invalidateQueries({ queryKey: ["recon-txs", accountId] }); // auto-check the new adjustment setChecked((prev) => new Set(prev).add(data.id)); }; if (!associationId) return

Select an association.

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

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

; return (

{(account as any)?.name ?? "Account"}

Current balance {money((account as any)?.balance, cur)}

{!active && ( )}
{active ? (
{/* Left panel */}
Statement Transactions
setSearch(e.target.value)} />
0 && filtered.every((t) => checked.has(t.id))} onCheckedChange={(v) => toggleAll(!!v)} /> Date Description Ref # Deposit Withdrawal {filtered.map((t) => { const on = checked.has(t.id); return ( { setChecked((prev) => { const next = new Set(prev); if (v) next.add(t.id); else next.delete(t.id); return next; }); }} /> {fmtDate(t.date)} {t.description} {t.reference ?? "—"} {t.type === "credit" ? money(t.amount, cur) : ""} {t.type === "debit" ? money(t.amount, cur) : ""} ); })} {filtered.length === 0 && ( No unreconciled transactions in this period. )}
{/* Right panel */}
Summary
setActive({ ...active, statement_balance: Number(e.target.value) })} />
Opening balance{money(openingBalance, cur)}
+ Cleared deposits ({(txs as Tx[]).filter(t => checked.has(t.id) && t.type === "credit").length})+{money(clearedDeposits, cur)}
− Cleared withdrawals ({(txs as Tx[]).filter(t => checked.has(t.id) && t.type === "debit").length})−{money(clearedWithdrawals, cur)}
Cleared balance{money(clearedBalance, cur)}
Statement balance{money(active.statement_balance, cur)}
Difference
{balanced ? : } {money(difference, cur)}
{!balanced && ( )}
) : ( Overview History

{lastCompleted ? <>Last reconciled on {fmtDate(lastCompleted.statement_end_date)} at {money(lastCompleted.statement_balance, cur)}. : "This account has never been reconciled."}

Period (Statement End) Statement Balance Reconciled Date Status {(history as any[]).map((h: any) => ( {fmtDate(h.statement_end_date)} {money(h.statement_balance, cur)} {h.completed_at ? fmtDate(h.completed_at) : "—"} {h.status === "completed" ? Completed : In progress} ))} {(history as any[]).length === 0 && ( No reconciliations yet. )}
)} {/* Setup modal */} Start Reconciliation Enter the details from your bank statement.
setSetup({ ...setup, statement_end_date: e.target.value })} />
setSetup({ ...setup, statement_balance: Number(e.target.value) })} />

Pulled from last reconciliation.

{/* Adjustment modal */} Create Adjustment Entry Post a journal entry of {money(difference, cur)} to balance the reconciliation.
setAdjNote(e.target.value)} />
{/* Success modal */} Reconciliation Complete {successData && (
Period reconciled{fmtDate(successData.statementEndDate)}
Ending balance{money(successData.endingBalance, successData.currency)}
Cleared transactions{successData.clearedDeposits.length + successData.clearedWithdrawals.length}
Date completed{fmtDate(successData.completedAt)}
)}
); }