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
+
+
+
+
+