From 4f0ac97e83d69c1e7b89e3c23841b65ac04b7fe3 Mon Sep 17 00:00:00 2001 From: renee-png Date: Wed, 10 Jun 2026 22:34:58 -0400 Subject: [PATCH] Income Statement: Buildium-style category subgroups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Group the multi-period Income Statement by account category (Operating Income, Administration, Utilities, Reserves Budget, …) with "Total for " subtotals, matching the Buildium layout, in the on-screen table, PDF, and CSV. - New accounting.accounts.category column (nullable; null = ungrouped), seeded from the local chart_of_accounts parent hierarchy. - Editable in Chart of Accounts: single-edit (with datalist autocomplete) and bulk-edit (blank = no change, __clear__ to unset). - buildium-account-categories edge function pulls each account's parent-GL category from Buildium (matched by code, fallback name) and backfills accounting.accounts.category; idempotent and re-runnable. Co-Authored-By: Claude Opus 4.8 --- .../AccountingChartOfAccountsPage.tsx | 26 ++- .../accounting/AccountingReportsPage.tsx | 159 ++++++++++++------ .../buildium-account-categories/index.ts | 122 ++++++++++++++ ...610130000_accounting_accounts_category.sql | 22 +++ 4 files changed, 272 insertions(+), 57 deletions(-) create mode 100644 supabase/functions/buildium-account-categories/index.ts create mode 100644 supabase/migrations/20260610130000_accounting_accounts_category.sql diff --git a/src/pages/accounting/AccountingChartOfAccountsPage.tsx b/src/pages/accounting/AccountingChartOfAccountsPage.tsx index 7b8bcef..191b1d6 100644 --- a/src/pages/accounting/AccountingChartOfAccountsPage.tsx +++ b/src/pages/accounting/AccountingChartOfAccountsPage.tsx @@ -49,6 +49,7 @@ export default function AccountingChartOfAccountsPage() { const [type, setType] = useState("asset"); const [isBank, setIsBank] = useState(false); const [isReserve, setIsReserve] = useState(false); + const [category, setCategory] = useState(""); const [description, setDescription] = useState(""); const [parentId, setParentId] = useState(""); @@ -63,6 +64,7 @@ export default function AccountingChartOfAccountsPage() { parent_account_id: "no_change", is_bank: "no_change", is_reserve: "no_change", + category: "", }); // ── Opening balances state ── @@ -139,10 +141,16 @@ export default function AccountingChartOfAccountsPage() { return out; }, [accounts]); + // Existing category names (for the Income Statement subgroups) — used as autocomplete suggestions. + const existingCategories = useMemo( + () => [...new Set((accounts as any[]).map((a) => a.category).filter(Boolean) as string[])].sort((a, b) => a.localeCompare(b)), + [accounts], + ); + // ── Accounts actions ── const resetForm = () => { setEditId(null); setName(""); setCode(""); setType("asset"); - setIsBank(false); setIsReserve(false); setDescription(""); setParentId(""); + setIsBank(false); setIsReserve(false); setCategory(""); setDescription(""); setParentId(""); }; const openEdit = (a: any) => { @@ -152,6 +160,7 @@ export default function AccountingChartOfAccountsPage() { setType(a.type ?? "asset"); setIsBank(a.is_bank ?? false); setIsReserve(a.is_reserve ?? false); + setCategory(a.category ?? ""); setDescription(a.description ?? ""); setParentId(a.parent_account_id ?? ""); setOpen(true); @@ -165,6 +174,7 @@ export default function AccountingChartOfAccountsPage() { type: type as any, is_bank: isBank, is_reserve: isReserve, + category: category.trim() || null, description: description || null, parent_account_id: parentId || null, }; @@ -207,7 +217,7 @@ export default function AccountingChartOfAccountsPage() { const clearSelection = () => setSelectedIds(new Set()); const openBulkEdit = () => { - setBulkEdit({ type: "no_change", parent_account_id: "no_change", is_bank: "no_change", is_reserve: "no_change" }); + setBulkEdit({ type: "no_change", parent_account_id: "no_change", is_bank: "no_change", is_reserve: "no_change", category: "" }); setBulkEditOpen(true); }; @@ -221,6 +231,7 @@ export default function AccountingChartOfAccountsPage() { 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 (bulkEdit.category.trim()) patch.category = bulkEdit.category.trim() === "__clear__" ? null : bulkEdit.category.trim(); if (Object.keys(patch).length === 0) return toast.error("No changes selected"); if (patch.parent_account_id && ids.includes(patch.parent_account_id)) @@ -407,6 +418,13 @@ export default function AccountingChartOfAccountsPage() { +
+ + setCategory(e.target.value)} placeholder="Ungrouped" /> + + {existingCategories.map((c) => +