Skip to content

Commit 2ca8e18

Browse files
chitcommitclaude
andauthored
feat: admin Chart of Accounts editor (L4) (#89)
Adds /coa page for L4 (owner/admin) users to view and manage tenant-specific Chart of Accounts overrides alongside the 80 global defaults. New: client/src/pages/CoaAdmin.tsx - Searchable, filterable table (scope: all/global/tenant, type: 5 enum) - Stats header: total / global / tenant-specific counts - Create form with 8 fields (code, name, type, subtype, parent, schedule E, tax deductible, description) - Toggle activate/deactivate for tenant-specific accounts - Global defaults are displayed read-only (cannot be edited through this UI) - L4 authorization enforced server-side; UI shows friendly "need owner/admin role" error if PATCH/POST returns 403 validateNewAccountCode() enforces the ChittyFinance code-range convention: - 4-digit code required - Code must fall inside one of the ranges defined for its type (e.g. expense codes must be in 5000-5499, 5100-5199, 6000-6099, 7000-7099, or 9000-9999) - Code must not already exist for this tenant or as a global default - Parent code must exist in the COA if provided Hook additions (client/src/hooks/use-classification.ts): - useCreateCoaAccount() — POST /api/coa - useUpdateCoaAccount() — PATCH /api/coa/:id - Both invalidate the tenant-scoped /api/coa cache on success Wiring: - /coa route added to App.tsx - Sidebar link added (cfo, accountant roles; L4 check is server-side) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 515aad1 commit 2ca8e18

4 files changed

Lines changed: 493 additions & 1 deletion

File tree

client/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import Reports from "@/pages/Reports";
2020
import Integrations from "@/pages/Integrations";
2121
import Allocations from "@/pages/Allocations";
2222
import Classification from "@/pages/Classification";
23+
import CoaAdmin from "@/pages/CoaAdmin";
2324

2425
// Lazy-load the Orbital Console (57KB + physics sim + canvas rendering)
2526
const OrbitalConsole = lazy(() => import("@/pages/OrbitalConsole"));
@@ -61,6 +62,7 @@ function Router() {
6162
<Route path="/properties/:id" component={PropertyDetail} />
6263
<Route path="/allocations" component={Allocations} />
6364
<Route path="/classification" component={Classification} />
65+
<Route path="/coa" component={CoaAdmin} />
6466
<Route path="/connections" component={Connections} />
6567
<Route path="/admin" component={Admin} />
6668
<Route path="/orbital">

client/src/components/layout/Sidebar.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
Settings, Plug, Building2, Shield,
55
ChevronDown, ChevronRight, ChevronsUpDown,
66
Menu, X, Activity, LayoutDashboard,
7-
ArrowLeftRight, Wallet, BarChart3, Cable, Orbit, GitBranch, Tags
7+
ArrowLeftRight, Wallet, BarChart3, Cable, Orbit, GitBranch, Tags, BookOpen
88
} from "lucide-react";
99
import { useState, useMemo } from "react";
1010
import { useRole, type UserRole } from "@/contexts/RoleContext";
@@ -27,6 +27,7 @@ const NAV_ITEMS: NavItem[] = [
2727
{ href: "/orbital", label: "Orbital", icon: Orbit, roles: ["cfo", "accountant", "bookkeeper", "user"], badge: "NEW" },
2828
{ href: "/allocations", label: "Allocations", icon: GitBranch, roles: ["cfo", "accountant"] },
2929
{ href: "/classification", label: "Classification", icon: Tags, roles: ["cfo", "accountant", "bookkeeper"] },
30+
{ href: "/coa", label: "Chart of Accounts", icon: BookOpen, roles: ["cfo", "accountant"] },
3031
{ href: "/reports", label: "Reports", icon: BarChart3, roles: ["cfo", "accountant"] },
3132
{ href: "/integrations", label: "Integrations", icon: Cable, roles: ["cfo", "accountant"] },
3233
{ href: "/connections", label: "Connections", icon: Plug, roles: ["cfo", "accountant"] },

client/src/hooks/use-classification.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,40 @@ export function useBatchSuggest() {
142142
});
143143
}
144144

145+
/** L4 — create a tenant-specific COA account */
146+
export function useCreateCoaAccount() {
147+
const qc = useQueryClient();
148+
const tenantId = useTenantId();
149+
return useMutation({
150+
mutationFn: (data: {
151+
code: string;
152+
name: string;
153+
type: 'asset' | 'liability' | 'equity' | 'income' | 'expense';
154+
subtype?: string | null;
155+
description?: string | null;
156+
scheduleELine?: string | null;
157+
taxDeductible?: boolean;
158+
parentCode?: string | null;
159+
}) => apiRequest('POST', '/api/coa', data).then((r) => r.json()) as Promise<ChartOfAccount>,
160+
onSuccess: () => {
161+
qc.invalidateQueries({ queryKey: ['/api/coa', tenantId] });
162+
},
163+
});
164+
}
165+
166+
/** L4 — update a tenant-specific COA account */
167+
export function useUpdateCoaAccount() {
168+
const qc = useQueryClient();
169+
const tenantId = useTenantId();
170+
return useMutation({
171+
mutationFn: ({ id, ...data }: { id: string } & Partial<ChartOfAccount>) =>
172+
apiRequest('PATCH', `/api/coa/${id}`, data).then((r) => r.json()) as Promise<ChartOfAccount>,
173+
onSuccess: () => {
174+
qc.invalidateQueries({ queryKey: ['/api/coa', tenantId] });
175+
},
176+
});
177+
}
178+
145179
/** L1 — run GPT-4o-mini AI batch over unclassified queue */
146180
export function useAiSuggest() {
147181
const qc = useQueryClient();

0 commit comments

Comments
 (0)