mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
8ac0edfbd9
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>
417 lines
16 KiB
TypeScript
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>
|
|
);
|
|
}
|