Files
acmcc/src/components/dashboard/AppSidebar.tsx
T
admin 8ac0edfbd9 Admin-only Accounting tab + platform COA consolidation
Phase 1: gate the Accounting sidebar item and /dashboard/accounting route
behind isAdmin via a RequireAdmin guard; Financial Reports stay visible.

Phase 2: platform associations now read the Chart of Accounts from
accounting.accounts (single source) instead of public.chart_of_accounts.
Shared fetchChartOfAccounts() normalizes both sources; central COA hooks
and ChartOfAccountsDropdown route through it (reads only, no migration).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 21:21:48 -04:00

417 lines
16 KiB
TypeScript

import {
LayoutDashboard, Users, Home, AlertTriangle, Gavel, FileText,
FolderKanban, CheckSquare, DollarSign, Scale, Receipt,
BarChart3, Calendar, FileBox, FilePlus, FileSignature,
ChevronDown, LogOut, Building2, Bell,
ClipboardList, Megaphone, MessageCircle, BookOpen, FileCode,
Phone, Send, Mail, MailOpen, AtSign, ServerCog,
Bot, CalendarOff, FileSearch, UserCog, Car, StickyNote,
ListChecks, CreditCard, Vote, UserCheck,
PieChart, Shield, BellRing, Database, ImageIcon, Clock, Upload, Landmark, PenLine, ClipboardCheck, Inbox, Calculator
} from "lucide-react";
import { NavLink } from "@/components/NavLink";
import sidebarLogo from "@/assets/favicon-logo.png";
import { useLocation, useNavigate } from "react-router-dom";
import { supabase } from "@/integrations/supabase/client";
import {
Sidebar, SidebarContent, SidebarGroup, SidebarGroupContent,
SidebarMenu, SidebarMenuButton, SidebarMenuItem,
SidebarFooter, useSidebar,
} from "@/components/ui/sidebar";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { useAuth } from "@/contexts/AuthContext";
import { useTickerCounts } from "@/contexts/TickerContext";
type NavItemDef = { title: string; url: string; icon: React.ElementType; tickerKey?: string };
type SectionDef = {
label: string;
items?: NavItemDef[];
subsections?: { label: string; items: NavItemDef[] }[];
defaultOpen?: boolean;
};
const coreItems: NavItemDef[] = [
{ title: "Dashboard", url: "/dashboard", icon: LayoutDashboard },
{ title: "Accounting", url: "/dashboard/accounting", icon: Calculator },
];
const sections: SectionDef[] = [
{
label: "Community Management",
items: [
{ title: "Associations", url: "/dashboard/associations", icon: Building2 },
{ title: "Directory", url: "/dashboard/directory", icon: Home },
{ title: "Committees", url: "/dashboard/directory?tab=committees", icon: Users },
],
subsections: [
{
label: "Board & Governance",
items: [
{ title: "Board Members", url: "/dashboard/board-members", icon: UserCheck },
{ title: "Board Resources", url: "/dashboard/board-resources", icon: BookOpen },
{ title: "Board Votes", url: "/dashboard/board-votes", icon: Vote, tickerKey: "board_votes" },
{ title: "Elections", url: "/dashboard/elections", icon: Vote },
],
},
{
label: "Architectural & Compliance",
items: [
{ title: "Violations", url: "/dashboard/violations", icon: AlertTriangle },
{ title: "ARC Applications", url: "/dashboard/arc-applications", icon: Gavel, tickerKey: "arc_applications" },
{ title: "ARC Inbound Emails", url: "/dashboard/arc-inbound", icon: Inbox },
{ title: "Parking", url: "/dashboard/parking", icon: Car },
{ title: "RV / Boat Lots", url: "/dashboard/rv-boat-lots", icon: Car },
],
},
{
label: "Requests & Cases",
items: [
{ title: "Homeowner Requests", url: "/dashboard/homeowner-requests", icon: MessageCircle, tickerKey: "homeowner_requests" },
{ title: "Client Requests", url: "/dashboard/client-requests", icon: ClipboardList },
{ title: "Legal Matters", url: "/dashboard/legal-matters", icon: Scale },
],
},
],
defaultOpen: true,
},
{
label: "Operations",
subsections: [
{
label: "Projects & Tasks",
items: [
{ title: "Projects", url: "/dashboard/projects", icon: FolderKanban, tickerKey: "projects" },
{ title: "Tasks", url: "/dashboard/tasks", icon: CheckSquare, tickerKey: "tasks" },
{ title: "Checklists", url: "/dashboard/checklists", icon: ListChecks },
],
},
{
label: "Scheduling & Activity",
items: [
{ title: "Calendar", url: "/dashboard/calendar", icon: Calendar },
{ title: "Reminders", url: "/dashboard/reminders", icon: Bell, tickerKey: "reminders" },
{ title: "Blocked Dates", url: "/dashboard/blocked-dates", icon: CalendarOff },
],
},
{
label: "Field & Service Work",
items: [
{ title: "Inspections", url: "/dashboard/inspections", icon: ClipboardList },
{ title: "Communication Log", url: "/dashboard/call-log", icon: Phone },
],
},
{
label: "Updates & Tracking",
items: [
{ title: "Owner Updates", url: "/dashboard/owner-updates", icon: StickyNote },
{ title: "Status Updates", url: "/dashboard/status-updates", icon: BarChart3 },
{ title: "Association Reports", url: "/dashboard/reports", icon: BarChart3 },
],
},
{
label: "Vendor & Approvals",
items: [
{ title: "Vendors", url: "/dashboard/vendors", icon: Users },
{ title: "Bids & Quotes", url: "/dashboard/bids-quotes", icon: Scale },
{ title: "Bill Approvals", url: "/dashboard/bill-approvals", icon: Shield, tickerKey: "bill_approvals" },
],
},
],
},
{
label: "Financial Management",
subsections: [
{
label: "Overview & Planning",
items: [
{ title: "Financial Overview", url: "/dashboard/financial-overview", icon: PieChart },
{ title: "Financial Reports", url: "/dashboard/financial-reports", icon: BarChart3 },
{ title: "Recent Ledger Updates", url: "/dashboard/recent-ledger-updates", icon: Clock },
{ title: "Budget Management", url: "/dashboard/budget-management", icon: BarChart3 },
],
},
{
label: "Receivables",
items: [
{ title: "Outstanding Balances", url: "/dashboard/outstanding-balances", icon: DollarSign },
{ title: "Bulk Charges", url: "/dashboard/bulk-charges", icon: DollarSign },
{ title: "Collections", url: "/dashboard/collections", icon: DollarSign },
{ title: "Payments", url: "/dashboard/payments", icon: CreditCard },
{ title: "Estoppels", url: "/dashboard/estoppels", icon: FileText },
{ title: "Payment Plans", url: "/dashboard/payment-plans", icon: DollarSign },
],
},
{
label: "Payables",
items: [
{ title: "Payables", url: "/dashboard/payables", icon: Receipt },
],
},
],
},
{
label: "Communication",
subsections: [
{
label: "Messaging",
items: [
{ title: "Messages", url: "/dashboard/messages", icon: MessageCircle },
{ title: "Compose Email", url: "/dashboard/compose-email", icon: Send },
],
},
{
label: "Notifications",
items: [
{ title: "Notify Board", url: "/dashboard/notify-board", icon: Megaphone },
{ title: "Notify Owners", url: "/dashboard/notify-owners", icon: BellRing },
],
},
{
label: "Email Management",
items: [
{ title: "Email History", url: "/dashboard/email-history", icon: Mail },
{ title: "Email Templates", url: "/dashboard/email-templates", icon: FileCode },
{ title: "Email Senders", url: "/dashboard/email-senders", icon: AtSign },
{ title: "Email Routing", url: "/dashboard/email-routing", icon: MailOpen },
],
},
],
},
{
label: "Documents & Content",
items: [
{ title: "Documents", url: "/dashboard/documents", icon: FileBox },
{ title: "Forms & Letters", url: "/dashboard/forms-letters", icon: FilePlus },
{ title: "Avria Sign", url: "/dashboard/avria-sign", icon: FileSignature },
{ title: "Announcements", url: "/dashboard/announcements", icon: Megaphone },
{ title: "Media Library", url: "/dashboard/media", icon: ImageIcon },
],
},
{
label: "Admin & System",
subsections: [
{
label: "Users & Access",
items: [
{ title: "User Management", url: "/dashboard/user-management", icon: UserCog },
{ title: "Sign-up Codes", url: "/dashboard/signup-codes", icon: UserCog },
{ title: "Bulk Updates", url: "/dashboard/bulk-updates", icon: Upload },
{ title: "Compliance Checklists", url: "/dashboard/compliance-checklists", icon: ClipboardCheck },
],
},
{
label: "Accounting Setup",
items: [
{ title: "Bank Accounts", url: "/dashboard/bank-accounts", icon: Landmark },
{ title: "Chart of Accounts", url: "/dashboard/chart-of-accounts", icon: BookOpen },
],
},
{
label: "Company Accounting",
items: [
{ title: "Company Bank Accounts", url: "/dashboard/company-bank-accounts", icon: Landmark },
{ title: "Company Bank Register", url: "/dashboard/company-bank-register", icon: BookOpen },
{ title: "Company Checks", url: "/dashboard/company-checks", icon: PenLine },
],
},
{
label: "Billing & Invoicing",
items: [
{ title: "Billable Expenses", url: "/dashboard/billable-expenses", icon: Receipt },
{ title: "Invoice Clients", url: "/dashboard/invoice-clients", icon: FileText },
{ title: "Client Invoices", url: "/dashboard/client-invoices", icon: FileText },
{ title: "Invoice Tracking", url: "/dashboard/invoice-tracking", icon: FileSearch },
],
},
{
label: "Data Management",
items: [
{ title: "Time Tracking", url: "/dashboard/time-tracking", icon: Clock },
{ title: "Data Migration", url: "/dashboard/data-migration", icon: Database },
{ title: "Migration Fields", url: "/dashboard/migration-fields", icon: Database },
],
},
],
},
];
/* ── Components ────────────────────── */
function NavItem({ item, collapsed }: { item: NavItemDef; collapsed: boolean }) {
const location = useLocation();
const isActive = location.pathname === item.url;
const counts = useTickerCounts();
const count = item.tickerKey ? counts[item.tickerKey as keyof typeof counts] : 0;
return (
<SidebarMenuItem>
<SidebarMenuButton asChild className="h-8">
<NavLink
to={item.url}
end
className={cn(
"relative flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-[13px] font-normal transition-colors",
"text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-foreground",
isActive && "bg-sidebar-primary/15 text-sidebar-primary-foreground font-medium"
)}
activeClassName=""
>
{isActive && (
<span className="absolute left-0 top-1/2 -translate-y-1/2 h-4 w-[2px] rounded-r-full bg-sidebar-primary" />
)}
<item.icon className={cn(
"h-4 w-4 shrink-0 transition-colors",
isActive ? "text-sidebar-primary" : "text-sidebar-foreground/50"
)} />
{!collapsed && (
<>
<span className="truncate flex-1">{item.title}</span>
{count > 0 && (
<Badge variant="secondary" className="h-5 min-w-5 px-1.5 text-[10px] font-semibold bg-primary/15 text-primary border-0 ml-auto">
{count > 99 ? "99+" : count}
</Badge>
)}
</>
)}
{collapsed && count > 0 && (
<span className="absolute -top-0.5 -right-0.5 h-2 w-2 rounded-full bg-primary" />
)}
</NavLink>
</SidebarMenuButton>
</SidebarMenuItem>
);
}
function SubsectionGroup({ label, items, collapsed }: { label: string; items: NavItemDef[]; collapsed: boolean }) {
return (
<div className="mt-1">
{!collapsed && (
<div className="px-3 py-1 text-[10px] font-semibold uppercase tracking-wider text-sidebar-foreground/30">
{label}
</div>
)}
<SidebarMenu className="gap-0.5 px-1">
{items.map((item) => (
<NavItem key={item.url} item={item} collapsed={collapsed} />
))}
</SidebarMenu>
</div>
);
}
function SidebarSection({ section }: { section: SectionDef }) {
const { state } = useSidebar();
const collapsed = state === "collapsed";
const location = useLocation();
const allItems = [
...(section.items ?? []),
...(section.subsections ?? []).flatMap((s) => s.items),
];
const hasActive = allItems.some((i) => location.pathname === i.url);
return (
<Collapsible defaultOpen={section.defaultOpen || hasActive}>
<SidebarGroup className="py-0">
<CollapsibleTrigger className="flex w-full items-center justify-between px-3 py-2 text-[11px] font-semibold uppercase tracking-wider text-sidebar-foreground/40 hover:text-sidebar-foreground/60 transition-colors">
{!collapsed && <span>{section.label}</span>}
{!collapsed && <ChevronDown className="h-3 w-3 transition-transform [[data-state=open]>&]:rotate-180" />}
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarGroupContent>
{section.items && (
<SidebarMenu className="gap-0.5 px-1">
{section.items.map((item) => (
<NavItem key={item.url} item={item} collapsed={collapsed} />
))}
</SidebarMenu>
)}
{section.subsections?.map((sub) => (
<SubsectionGroup key={sub.label} label={sub.label} items={sub.items} collapsed={collapsed} />
))}
</SidebarGroupContent>
</CollapsibleContent>
</SidebarGroup>
</Collapsible>
);
}
export function AppSidebar() {
const { state } = useSidebar();
const collapsed = state === "collapsed";
const navigate = useNavigate();
const { isAdmin } = useAuth();
// Filter Admin & System section for non-admins
const visibleSections = sections.filter((s) => {
if (s.label === "Admin & System" && !isAdmin) return false;
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("/");
};
return (
<Sidebar collapsible="icon" className="border-r-0">
<div className="shrink-0 px-4 py-4 border-b border-sidebar-border">
<div className="flex items-center gap-2.5">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-sidebar-primary overflow-hidden">
<img src={sidebarLogo} alt="Logo" className="h-5 w-5 object-contain" />
</div>
{!collapsed && (
<div className="flex flex-col">
<span className="text-[13px] font-semibold text-sidebar-foreground tracking-tight">Community Cloud</span>
<span className="text-[10px] text-sidebar-foreground/40">Management Platform</span>
</div>
)}
</div>
</div>
<SidebarContent className="flex-1 overflow-hidden">
<ScrollArea className="h-full">
<div className="py-2 space-y-0.5">
<SidebarGroup className="py-0">
<SidebarGroupContent>
<SidebarMenu className="gap-0.5 px-1">
{visibleCoreItems.map((item) => (
<NavItem key={item.title} item={item} collapsed={collapsed} />
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
{visibleSections.map((section) => (
<SidebarSection key={section.label} section={section} />
))}
</div>
</ScrollArea>
</SidebarContent>
<SidebarFooter className="border-t border-sidebar-border p-2">
<Button
variant="ghost"
size="sm"
className="w-full justify-start gap-2 text-sidebar-foreground/50 hover:text-red-400 hover:bg-red-500/10 h-8 text-[13px]"
onClick={handleSignOut}
>
<LogOut className="h-4 w-4" />
{!collapsed && <span>Sign Out</span>}
</Button>
</SidebarFooter>
</Sidebar>
);
}