diff --git a/src/App.tsx b/src/App.tsx
index 99216d9..df8fcf8 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -5,6 +5,7 @@ import { Toaster } from "@/components/ui/toaster";
import { TooltipProvider } from "@/components/ui/tooltip";
import { AuthProvider } from "@/contexts/AuthContext";
import { ViewAsBanner } from "@/components/ViewAsBanner";
+import { RequireAdmin } from "@/components/RequireAdmin";
import Index from "./pages/Index";
import Auth from "./pages/Auth";
import ResetPasswordPage from "./pages/ResetPasswordPage";
@@ -374,7 +375,7 @@ const App = () => (
} />
} />
} />
- }>
+ }>
} />
} />
} />
diff --git a/src/components/ChartOfAccountsDropdown.jsx b/src/components/ChartOfAccountsDropdown.jsx
index 7ad7846..014afaa 100644
--- a/src/components/ChartOfAccountsDropdown.jsx
+++ b/src/components/ChartOfAccountsDropdown.jsx
@@ -2,9 +2,9 @@ import React, { useState, useEffect, useMemo } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { cn } from '@/lib/utils';
+import { fetchChartOfAccounts } from '@/lib/chartOfAccountsSource';
const NONE_VALUE = '__none__';
-const PAGE_SIZE = 1000;
const normalizeType = (type) => String(type || '').trim().toLowerCase();
@@ -33,32 +33,24 @@ export default function ChartOfAccountsDropdown({ value, onChange, className, pl
useEffect(() => {
const fetch = async () => {
- // Resolve which accounting system this association uses
+ // Resolve which accounting system this association uses (platform / zoho / buildium)
let system = 'buildium';
if (associationId) {
const { data: assoc } = await supabase
.from('associations')
- .select('zoho_organization_id')
+ .select('accounting_system, zoho_organization_id')
.eq('id', associationId)
.maybeSingle();
- if (assoc?.zoho_organization_id && String(assoc.zoho_organization_id).trim() !== '') {
+ const explicit = assoc?.accounting_system;
+ if (explicit === 'platform' || explicit === 'zoho' || explicit === 'buildium') {
+ system = explicit;
+ } else if (assoc?.zoho_organization_id && String(assoc.zoho_organization_id).trim() !== '') {
system = 'zoho';
}
}
- const allAccounts = [];
- for (let from = 0; ; from += PAGE_SIZE) {
- const { data, error } = await supabase
- .from('chart_of_accounts')
- .select('id, account_name, account_number, account_type, parent_account_id, association_id, association_ids, accounting_system')
- .eq('is_active', true)
- .order('account_number', { ascending: true })
- .range(from, from + PAGE_SIZE - 1);
-
- if (error || !data) break;
- allAccounts.push(...data);
- if (data.length < PAGE_SIZE) break;
- }
+ // Single normalized source: accounting.accounts for platform, else public COA.
+ const allAccounts = await fetchChartOfAccounts(associationId, system);
const filtered = allAccounts.filter((account) => {
const matchesType = accountTypeMatches(account.account_type, accountType);
diff --git a/src/components/RequireAdmin.tsx b/src/components/RequireAdmin.tsx
new file mode 100644
index 0000000..564c017
--- /dev/null
+++ b/src/components/RequireAdmin.tsx
@@ -0,0 +1,23 @@
+import { Navigate } from "react-router-dom";
+import { useAuth } from "@/contexts/AuthContext";
+
+/**
+ * Route guard for admin-only areas (e.g. the Accounting platform).
+ * Shows a spinner while auth resolves, then redirects non-admins to the
+ * dashboard. Admin status respects "view as" (uses effective `isAdmin`).
+ */
+export function RequireAdmin({ children }: { children: React.ReactNode }) {
+ const { loading, isAdmin } = useAuth();
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (!isAdmin) return ;
+
+ return <>{children}>;
+}
diff --git a/src/components/dashboard/AppSidebar.tsx b/src/components/dashboard/AppSidebar.tsx
index bd41748..b3370a1 100644
--- a/src/components/dashboard/AppSidebar.tsx
+++ b/src/components/dashboard/AppSidebar.tsx
@@ -353,6 +353,12 @@ export function AppSidebar() {
return true;
});
+ // The Accounting platform is admin-only (Financial Reports/Overview stay visible to all)
+ const visibleCoreItems = coreItems.filter((item) => {
+ if (item.url === "/dashboard/accounting" && !isAdmin) return false;
+ return true;
+ });
+
const handleSignOut = async () => {
await supabase.auth.signOut();
navigate("/");
@@ -380,7 +386,7 @@ export function AppSidebar() {
- {coreItems.map((item) => (
+ {visibleCoreItems.map((item) => (
))}
diff --git a/src/hooks/useCentralChartOfAccounts.js b/src/hooks/useCentralChartOfAccounts.js
index 4e2b0d3..f0122f9 100644
--- a/src/hooks/useCentralChartOfAccounts.js
+++ b/src/hooks/useCentralChartOfAccounts.js
@@ -1,9 +1,11 @@
import { useState, useEffect } from 'react';
-import { supabase } from '@/integrations/supabase/client';
import { useAssociationAccountingSystem } from './useAssociationAccountingSystem';
+import { fetchChartOfAccounts } from '@/lib/chartOfAccountsSource';
/**
- * JS variant. Pass `associationId` to scope to that association's COA system (zoho vs buildium).
+ * JS variant. Pass `associationId` to scope to that association's COA system.
+ * - `platform` associations read from the Accounting module (`accounting.accounts`).
+ * - `zoho` / `buildium` read the public `chart_of_accounts`.
* When omitted, returns the Buildium (default) set.
*/
export function useCentralChartOfAccounts(associationId) {
@@ -20,24 +22,9 @@ export function useCentralChartOfAccounts(associationId) {
setLoading(true);
setError(null);
try {
- const { data, error: fetchError } = await supabase
- .from('chart_of_accounts')
- .select('id, account_number, account_name, account_type, is_active, accounting_system')
- .eq('is_active', true)
- .eq('accounting_system', system)
- .order('account_number', { ascending: true });
-
- if (fetchError) throw fetchError;
-
- if (isMounted) {
- const processedData = (data || []).map(acc => ({
- ...acc,
- account_number: acc.account_number || '',
- account_name: acc.account_name || 'Unnamed Account',
- account_type: acc.account_type || 'Uncategorized'
- }));
- setAccounts(processedData);
- }
+ const rows = await fetchChartOfAccounts(associationId, system);
+ // Match prior JS behavior: only active accounts surface here.
+ if (isMounted) setAccounts(rows.filter((a) => a.is_active));
} catch (err) {
console.error('[useCentralChartOfAccounts] Fetch Error:', err);
if (isMounted) setError(err.message);
@@ -48,7 +35,7 @@ export function useCentralChartOfAccounts(associationId) {
fetchAccounts();
return () => { isMounted = false; };
- }, [system, systemLoading]);
+ }, [associationId, system, systemLoading]);
return { accounts, loading, error, system };
}
diff --git a/src/hooks/useCentralChartOfAccounts.ts b/src/hooks/useCentralChartOfAccounts.ts
index 46fd8b4..cfd6b57 100644
--- a/src/hooks/useCentralChartOfAccounts.ts
+++ b/src/hooks/useCentralChartOfAccounts.ts
@@ -1,17 +1,15 @@
import { useState, useEffect } from "react";
-import { supabase } from "@/integrations/supabase/client";
-import type { Tables } from "@/integrations/supabase/types";
import { useAssociationAccountingSystem } from "./useAssociationAccountingSystem";
-
-type ChartOfAccount = Tables<"chart_of_accounts">;
+import { fetchChartOfAccounts, type NormalizedAccount } from "@/lib/chartOfAccountsSource";
/**
* Loads the Chart of Accounts scoped to the association's accounting system.
- * - If associationId is provided, only accounts matching that system (zoho vs buildium) are returned.
- * - If associationId is omitted/null, returns the Buildium set (default global system).
+ * - `platform` associations read from the Accounting module (`accounting.accounts`).
+ * - `zoho` / `buildium` read the public `chart_of_accounts`, scoped by system.
+ * - If associationId is omitted/null, returns the Buildium (default) set.
*/
export function useCentralChartOfAccounts(associationId?: string | null) {
- const [accounts, setAccounts] = useState([]);
+ const [accounts, setAccounts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const { system, loading: systemLoading } = useAssociationAccountingSystem(associationId);
@@ -21,28 +19,25 @@ export function useCentralChartOfAccounts(associationId?: string | null) {
let cancelled = false;
const fetchAccounts = async () => {
setLoading(true);
- const { data, error: fetchError } = await supabase
- .from("chart_of_accounts")
- .select("*")
- .eq("accounting_system", system)
- .order("account_number");
-
- if (cancelled) return;
- if (fetchError) {
- console.error("[useCentralChartOfAccounts] Error:", fetchError);
- setError(fetchError.message);
- } else {
- setAccounts(data ?? []);
+ try {
+ const rows = await fetchChartOfAccounts(associationId, system);
+ if (cancelled) return;
+ setAccounts(rows);
setError(null);
+ } catch (fetchError: any) {
+ if (cancelled) return;
+ console.error("[useCentralChartOfAccounts] Error:", fetchError);
+ setError(fetchError?.message ?? "Failed to load accounts");
+ } finally {
+ if (!cancelled) setLoading(false);
}
- setLoading(false);
};
fetchAccounts();
return () => {
cancelled = true;
};
- }, [system, systemLoading]);
+ }, [associationId, system, systemLoading]);
return { accounts, loading, error, system };
}
diff --git a/src/lib/chartOfAccountsSource.ts b/src/lib/chartOfAccountsSource.ts
new file mode 100644
index 0000000..bfc31ef
--- /dev/null
+++ b/src/lib/chartOfAccountsSource.ts
@@ -0,0 +1,91 @@
+import { supabase } from "@/integrations/supabase/client";
+import { accounting } from "@/lib/accountingClient";
+
+/**
+ * The Chart of Accounts shape every consumer in the app expects, regardless of
+ * whether the underlying source is the public `chart_of_accounts` table or the
+ * Accounting platform's `accounting.accounts` table.
+ */
+export type NormalizedAccount = {
+ id: string;
+ account_number: string;
+ account_name: string;
+ account_type: string;
+ parent_account_id: string | null;
+ is_active: boolean;
+ association_id: string | null;
+ association_ids: string[] | null;
+ accounting_system: string;
+ description: string | null;
+};
+
+function fromPublic(row: any): NormalizedAccount {
+ return {
+ id: String(row.id),
+ account_number: row.account_number ?? "",
+ account_name: row.account_name ?? "Unnamed Account",
+ account_type: row.account_type ?? "Uncategorized",
+ parent_account_id: row.parent_account_id ?? null,
+ is_active: row.is_active ?? true,
+ association_id: row.association_id ?? null,
+ association_ids: Array.isArray(row.association_ids) ? row.association_ids : null,
+ accounting_system: row.accounting_system ?? "buildium",
+ description: row.description ?? null,
+ };
+}
+
+function fromPlatform(row: any, associationId: string): NormalizedAccount {
+ return {
+ id: String(row.id),
+ account_number: row.code ?? "",
+ account_name: row.name ?? "Unnamed Account",
+ account_type: row.type ?? "Uncategorized",
+ parent_account_id: row.parent_account_id ?? null,
+ is_active: true, // accounting.accounts has no active flag; all are active
+ association_id: associationId,
+ association_ids: [associationId],
+ accounting_system: "platform",
+ description: row.subtype ?? row.description ?? null,
+ };
+}
+
+/**
+ * Loads the Chart of Accounts for an association's accounting system.
+ * - `platform` → the Accounting module's `accounting.accounts` (single source of
+ * truth once an association is on the platform). Returns [] if the association
+ * has no `accounting.companies` row yet.
+ * - `zoho` / `buildium` → the public `chart_of_accounts`, scoped by system.
+ *
+ * Returned rows are normalized to {@link NormalizedAccount} so callers never
+ * branch on the source.
+ */
+export async function fetchChartOfAccounts(
+ associationId: string | null | undefined,
+ system: string,
+): Promise {
+ if (system === "platform" && associationId) {
+ const { data: company, error: companyError } = await accounting
+ .from("companies")
+ .select("id")
+ .eq("association_id", associationId)
+ .maybeSingle();
+ if (companyError) throw companyError;
+ if (!company?.id) return [];
+
+ const { data, error } = await accounting
+ .from("accounts")
+ .select("id, code, name, type, subtype, parent_account_id, description")
+ .eq("company_id", company.id)
+ .order("code", { ascending: true });
+ if (error) throw error;
+ return (data ?? []).map((row) => fromPlatform(row, associationId));
+ }
+
+ const { data, error } = await supabase
+ .from("chart_of_accounts")
+ .select("*")
+ .eq("accounting_system", system)
+ .order("account_number", { ascending: true });
+ if (error) throw error;
+ return (data ?? []).map(fromPublic);
+}