diff --git a/src/pages/accounting/AccountingReconcileDetailPage.tsx b/src/pages/accounting/AccountingReconcileDetailPage.tsx index be31a28..cca3945 100644 --- a/src/pages/accounting/AccountingReconcileDetailPage.tsx +++ b/src/pages/accounting/AccountingReconcileDetailPage.tsx @@ -18,11 +18,31 @@ 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 { ArrowLeft, CheckCircle2, AlertTriangle, FileDown, Search, Loader2, ArrowUp, ArrowDown, ChevronsUpDown } from "lucide-react"; +import { cn } from "@/lib/utils"; import { toast } from "sonner"; import { money, fmtDate } from "./lib/format"; import { renderReconciliationPdf, type ReconReportData } from "./lib/reconciliationPdf"; +/** Clickable, sortable column header for the reconciliation table. */ +function SortHead({ col, label, sort, onSort, align }: { + col: string; label: string; + sort: { col: string; dir: "asc" | "desc" }; + onSort: (col: any) => void; + align?: "right"; +}) { + const activeCol = sort.col === col; + return ( + onSort(col)}> + + {label} + {activeCol ? (sort.dir === "asc" ? : ) + : } + + + ); +} + type Tx = { id: string; date: string; @@ -56,6 +76,10 @@ export default function AccountingReconcileDetailPage() { const [search, setSearch] = useState(""); const [filter, setFilter] = useState<"all" | "deposits" | "withdrawals">("all"); const [checked, setChecked] = useState>(new Set()); + type SortCol = "date" | "description" | "reference" | "deposit" | "withdrawal"; + const [sort, setSort] = useState<{ col: SortCol; dir: "asc" | "desc" }>({ col: "date", dir: "asc" }); + const toggleSort = (col: SortCol) => + setSort((s) => (s.col === col ? { col, dir: s.dir === "asc" ? "desc" : "asc" } : { col, dir: "asc" })); const [successOpen, setSuccessOpen] = useState(false); const [successData, setSuccessData] = useState(null); @@ -85,6 +109,10 @@ export default function AccountingReconcileDetailPage() { const lastCompleted = (history as any[]).find((h: any) => h.status === "completed"); const openingBalance = active?.opening_balance ?? Number(lastCompleted?.statement_balance ?? 0); + // The period being reconciled opens the day AFTER the last completed + // reconciliation. Transactions on/before that date belong to a closed period + // and must not reappear in a new reconciliation. + const priorReconDate: string | null = lastCompleted?.statement_end_date ?? null; const { data: allAccounts = [] } = useQuery({ queryKey: ["accounts", cid], @@ -94,16 +122,18 @@ export default function AccountingReconcileDetailPage() { }); const { data: txs = [] } = useQuery({ - queryKey: ["recon-txs", accountId, active?.statement_end_date], + queryKey: ["recon-txs", accountId, active?.statement_end_date, priorReconDate], enabled: !!accountId && !!active, queryFn: async () => { - const { data } = await accounting + let q = 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"); + .lte("date", active!.statement_end_date); + // Only this period: skip anything already covered by the last reconciliation. + if (priorReconDate) q = q.gt("date", priorReconDate); + const { data } = await q.order("date"); return (data ?? []) as Tx[]; }, }); @@ -117,13 +147,29 @@ export default function AccountingReconcileDetailPage() { const filtered = useMemo(() => { const s = search.toLowerCase().trim(); - return (txs as Tx[]).filter((t) => { + const rows = (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 dir = sort.dir === "asc" ? 1 : -1; + const key = (t: Tx): string | number => { + switch (sort.col) { + case "date": return t.date ?? ""; + case "description": return (t.description ?? "").toLowerCase(); + case "reference": return (t.reference ?? "").toLowerCase(); + case "deposit": return t.type === "credit" ? Number(t.amount) : -Infinity; + case "withdrawal": return t.type === "debit" ? Number(t.amount) : -Infinity; + } + }; + return [...rows].sort((a, b) => { + const ka = key(a), kb = key(b); + if (ka < kb) return -1 * dir; + if (ka > kb) return 1 * dir; + return 0; + }); + }, [txs, search, filter, sort]); const clearedDeposits = useMemo( () => (txs as Tx[]).filter((t) => checked.has(t.id) && t.type === "credit") @@ -336,11 +382,11 @@ export default function AccountingReconcileDetailPage() { onCheckedChange={(v) => toggleAll(!!v)} /> - Date - Description - Ref # - Deposit - Withdrawal + + + + +