mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
Reconciliation: only show the open period + sortable column headers
- Transactions on/before the last completed reconciliation's statement date no longer reappear in a new reconciliation (date floor). Imported GL registers were inserted cleared-but-unreconciled, flooding the list with prior-period rows; reconciling now works period-by-period - Sortable headers (Date / Description / Ref # / Deposit / Withdrawal) with asc/desc toggle indicators Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
||||
<TableHead className={cn("cursor-pointer select-none", align === "right" && "text-right")} onClick={() => onSort(col)}>
|
||||
<span className={cn("inline-flex items-center gap-1", align === "right" && "flex-row-reverse")}>
|
||||
{label}
|
||||
{activeCol ? (sort.dir === "asc" ? <ArrowUp className="h-3.5 w-3.5" /> : <ArrowDown className="h-3.5 w-3.5" />)
|
||||
: <ChevronsUpDown className="h-3.5 w-3.5 text-muted-foreground/50" />}
|
||||
</span>
|
||||
</TableHead>
|
||||
);
|
||||
}
|
||||
|
||||
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<Set<string>>(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<ReconReportData | null>(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)}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead>Description</TableHead>
|
||||
<TableHead>Ref #</TableHead>
|
||||
<TableHead className="text-right">Deposit</TableHead>
|
||||
<TableHead className="text-right">Withdrawal</TableHead>
|
||||
<SortHead col="date" label="Date" sort={sort} onSort={toggleSort} />
|
||||
<SortHead col="description" label="Description" sort={sort} onSort={toggleSort} />
|
||||
<SortHead col="reference" label="Ref #" sort={sort} onSort={toggleSort} />
|
||||
<SortHead col="deposit" label="Deposit" sort={sort} onSort={toggleSort} align="right" />
|
||||
<SortHead col="withdrawal" label="Withdrawal" sort={sort} onSort={toggleSort} align="right" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
||||
Reference in New Issue
Block a user