From 36787b193dfde471c28b5f2a09bf847973a863f4 Mon Sep 17 00:00:00 2001 From: renee-png Date: Sun, 7 Jun 2026 14:54:19 -0400 Subject: [PATCH] Accounting COA: bulk edit + bulk delete of accounts Add row checkboxes (with per-category select-all) to the accounting Chart of Accounts. A selection bar exposes Edit/Delete/Clear. Bulk edit applies any of type, parent account, bank flag, reserve flag to all selected (each field has a "No change" option); bulk delete removes selected accounts. Mirrors the existing bulk-edit UX on the per-association chart_of_accounts page. Co-Authored-By: Claude Opus 4.8 --- .../AccountingChartOfAccountsPage.tsx | 201 +++++++++++++++++- 1 file changed, 199 insertions(+), 2 deletions(-) diff --git a/src/pages/accounting/AccountingChartOfAccountsPage.tsx b/src/pages/accounting/AccountingChartOfAccountsPage.tsx index 4c82a8a..b5f188c 100644 --- a/src/pages/accounting/AccountingChartOfAccountsPage.tsx +++ b/src/pages/accounting/AccountingChartOfAccountsPage.tsx @@ -9,6 +9,7 @@ import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } 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"; @@ -19,7 +20,7 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; -import { Plus, Trash2, CalendarIcon, Lock, Pencil, RotateCcw, Save, Info, Loader2, Upload } from "lucide-react"; +import { Plus, Trash2, CalendarIcon, Lock, Pencil, RotateCcw, Save, Info, Loader2, Upload, X } from "lucide-react"; import { toast } from "sonner"; import { money } from "./lib/format"; import { cn } from "@/lib/utils"; @@ -51,6 +52,19 @@ export default function AccountingChartOfAccountsPage() { const [description, setDescription] = useState(""); const [parentId, setParentId] = useState(""); + // ── Bulk selection / edit state ── + const [selectedIds, setSelectedIds] = useState>(new Set()); + const [bulkEditOpen, setBulkEditOpen] = useState(false); + const [bulkEditing, setBulkEditing] = useState(false); + const [bulkDeleteOpen, setBulkDeleteOpen] = useState(false); + const [bulkDeleting, setBulkDeleting] = useState(false); + const [bulkEdit, setBulkEdit] = useState({ + type: "no_change", + parent_account_id: "no_change", + is_bank: "no_change", + is_reserve: "no_change", + }); + // ── Opening balances state ── const [obRows, setObRows] = useState>({}); const [asOfDate, setAsOfDate] = useState(new Date()); @@ -153,6 +167,82 @@ export default function AccountingChartOfAccountsPage() { qc.invalidateQueries({ queryKey: ["accounts", cid] }); }; + // ── Bulk selection / edit actions ── + const toggleSelect = (id: string) => + setSelectedIds((prev) => { + const next = new Set(prev); + next.has(id) ? next.delete(id) : next.add(id); + return next; + }); + + const toggleSelectMany = (ids: string[], checked: boolean) => + setSelectedIds((prev) => { + const next = new Set(prev); + ids.forEach((id) => (checked ? next.add(id) : next.delete(id))); + return next; + }); + + const clearSelection = () => setSelectedIds(new Set()); + + const openBulkEdit = () => { + setBulkEdit({ type: "no_change", parent_account_id: "no_change", is_bank: "no_change", is_reserve: "no_change" }); + setBulkEditOpen(true); + }; + + const handleBulkEdit = async () => { + const ids = [...selectedIds]; + if (!ids.length) return; + + const patch: Record = {}; + if (bulkEdit.type !== "no_change") patch.type = bulkEdit.type; + if (bulkEdit.parent_account_id !== "no_change") + patch.parent_account_id = bulkEdit.parent_account_id === "none" ? null : bulkEdit.parent_account_id; + if (bulkEdit.is_bank !== "no_change") patch.is_bank = bulkEdit.is_bank === "true"; + if (bulkEdit.is_reserve !== "no_change") patch.is_reserve = bulkEdit.is_reserve === "true"; + + if (Object.keys(patch).length === 0) return toast.error("No changes selected"); + if (patch.parent_account_id && ids.includes(patch.parent_account_id)) + return toast.error("A selected account can't be set as its own parent"); + + setBulkEditing(true); + try { + for (let i = 0; i < ids.length; i += 50) { + const batch = ids.slice(i, i + 50); + const { error } = await accounting.from("accounts").update(patch).in("id", batch); + if (error) throw error; + } + toast.success(`Updated ${ids.length} account${ids.length === 1 ? "" : "s"}`); + setBulkEditOpen(false); + clearSelection(); + qc.invalidateQueries({ queryKey: ["accounts", cid] }); + } catch (e: any) { + toast.error(e.message || "Bulk update failed"); + } finally { + setBulkEditing(false); + } + }; + + const handleBulkDelete = async () => { + const ids = [...selectedIds]; + if (!ids.length) return; + setBulkDeleting(true); + try { + for (let i = 0; i < ids.length; i += 50) { + const batch = ids.slice(i, i + 50); + const { error } = await accounting.from("accounts").delete().in("id", batch); + if (error) throw error; + } + toast.success(`Deleted ${ids.length} account${ids.length === 1 ? "" : "s"}`); + setBulkDeleteOpen(false); + clearSelection(); + qc.invalidateQueries({ queryKey: ["accounts", cid] }); + } catch (e: any) { + toast.error(e.message || "Bulk delete failed"); + } finally { + setBulkDeleting(false); + } + }; + // ── Opening balances actions ── const updateOb = (id: string, side: "debit" | "credit", v: string) => { setObRows((r) => ({ ...r, [id]: { debit: side === "debit" ? v : "", credit: side === "credit" ? v : "" } })); @@ -322,6 +412,83 @@ export default function AccountingChartOfAccountsPage() { onSuccess={() => qc.invalidateQueries({ queryKey: ["accounts", cid] })} /> + {/* ── Bulk edit dialog ── */} + + + Edit {selectedIds.size} account{selectedIds.size === 1 ? "" : "s"} +
+

Only fields you change are applied. Leave a field on “No change” to keep each account’s existing value.

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + +
+
+ + {/* ── Bulk delete confirm ── */} + + + + Delete {selectedIds.size} account{selectedIds.size === 1 ? "" : "s"}? + This permanently removes the selected accounts. Accounts that have posted transactions may fail to delete. + + + Cancel + + {bulkDeleting && }Delete + + + + + Accounts @@ -330,6 +497,22 @@ export default function AccountingChartOfAccountsPage() { {/* ── Accounts tab ── */} + {selectedIds.size > 0 && ( +
+ {selectedIds.size} selected +
+ + + +
+
+ )} {TYPES.map((t) => { const rows = grouped[t.value] ?? []; const subtotal = rows.reduce((s: number, a: any) => s + Number(a.balance), 0); @@ -346,6 +529,13 @@ export default function AccountingChartOfAccountsPage() { + + 0 && rows.every((a: any) => selectedIds.has(a.id))} + onCheckedChange={(c) => toggleSelectMany(rows.map((a: any) => a.id), !!c)} + /> + Code Account name Parent @@ -359,6 +549,13 @@ export default function AccountingChartOfAccountsPage() { const isChild = !!a.parent_account_id; return ( + + toggleSelect(a.id)} + /> + {a.code ?? "—"}
@@ -389,7 +586,7 @@ export default function AccountingChartOfAccountsPage() { })} {rows.length === 0 && ( - + No accounts in this category yet.