import React, { useState, useEffect } from 'react';
import { Link, useLocation } from 'react-router-dom';
import {
LayoutDashboard,
Building2,
BarChart3,
Users,
Shield,
FileText,
Settings,
ChevronDown,
X,
Plug,
TrendingUp,
Target,
LineChart,
Map,
Lightbulb,
FolderOpen,
Mail,
Handshake,
Crown,
Key,
CreditCard,
AlertTriangle,
Zap,
Award,
ClipboardList,
DollarSign,
Search,
Hash,
Phone,
Plus,
Palette,
SlidersHorizontal,
ShieldCheck,
} from 'lucide-react';
import { useAuth } from '../../contexts/AuthContext';
import type { CompanyPermission, CompanyRole } from '../../types';
// --- Types ---
interface NavItem {
path: string;
label: string;
icon: React.ElementType;
permission?: CompanyPermission;
}
interface NavSection {
title: string;
items: NavItem[];
}
// --- Partner Navigation ---
const partnerNavSections: NavSection[] = [
{
title: 'Dashboard',
items: [
{ path: '/app', label: 'Dashboard', icon: LayoutDashboard },
],
},
{
title: 'Pipeline',
items: [
{ path: '/app/estimates', label: 'Estimates', icon: FileText },
{ path: '/app/angi-leads', label: 'Angi Leads', icon: ClipboardList },
],
},
{
title: 'Ad Channels',
items: [
{ path: '/app/lead-gen', label: 'Lead Gen Dashboard', icon: Target },
{ path: '/app/lead-gen/campaigns/new', label: 'New Campaign', icon: Plus },
{ path: '/app/lead-gen/creatives', label: 'Creatives', icon: Palette },
{ path: '/app/lead-gen/optimization', label: 'Optimization', icon: SlidersHorizontal },
{ path: '/app/lead-gen/attribution', label: 'Attribution', icon: TrendingUp },
{ path: '/app/google-ads', label: 'Google Ads', icon: Search },
{ path: '/app/facebook-ads', label: 'Facebook Ads', icon: Search },
],
},
{
title: 'Communications',
items: [
{ path: '/app/slack', label: 'Slack', icon: Hash },
{ path: '/app/sms-call', label: 'SMS / Calls', icon: Phone },
],
},
{
title: 'Revenue',
items: [
{ path: '/app/revenue-leaks', label: 'Revenue Leaks', icon: AlertTriangle },
{ path: '/app/optimization-moves', label: 'Optimizations', icon: Zap },
{ path: '/app/forecasts', label: 'Forecasts', icon: TrendingUp },
{ path: '/app/commissions', label: 'Commissions', icon: BarChart3, permission: 'view_reports' },
],
},
{
title: 'Coaching',
items: [
{ path: '/app/coaching-assignments', label: 'Assignments', icon: ClipboardList },
{ path: '/app/coaching-scorecards', label: 'Scorecards', icon: Award },
{ path: '/app/coaching', label: 'Smart Coaching', icon: Lightbulb },
],
},
{
title: 'Team & Org',
items: [
{ path: '/app/organizations', label: 'Organizations', icon: Building2 },
{ path: '/app/team', label: 'Team', icon: Users, permission: 'manage_team' },
],
},
{
title: 'Settings',
items: [
{ path: '/app/integrations', label: 'Integrations', icon: Plug, permission: 'edit_integrations' },
{ path: '/app/billing', label: 'Billing', icon: CreditCard },
{ path: '/app/settings', label: 'Settings', icon: Settings, permission: 'edit_settings' },
],
},
];
// --- Admin Navigation ---
const adminNavSections: NavSection[] = [
{
title: 'Overview',
items: [
{ path: '/admin', label: 'Dashboard', icon: LayoutDashboard },
],
},
{
title: 'People',
items: [
{ path: '/admin/organizations', label: 'Organizations', icon: Building2 },
{ path: '/admin/partners', label: 'Partners', icon: Handshake },
{ path: '/admin/commissions', label: 'Commissions', icon: CreditCard },
{ path: '/admin/super-admins', label: 'Super Admins', icon: Crown },
{ path: '/admin/users', label: 'Users', icon: Users },
{ path: '/admin/leads', label: 'Leads', icon: FileText },
],
},
{
title: 'Insights',
items: [
{ path: '/admin/analytics', label: 'Analytics', icon: BarChart3 },
{ path: '/admin/crm-analytics', label: 'CRM Analytics', icon: LineChart },
{ path: '/admin/forecasting', label: 'Forecasting', icon: TrendingUp },
{ path: '/admin/goals', label: 'Goals', icon: Target },
{ path: '/admin/strategic-intelligence', label: 'Strategic Intel', icon: Shield },
],
},
{
title: 'Growth',
items: [
{ path: '/admin/scale-optimization', label: 'Scale Optimization', icon: TrendingUp },
{ path: '/admin/multi-market', label: 'Multi-Market', icon: Map },
{ path: '/admin/coaching', label: 'Smart Coaching', icon: Lightbulb },
{ path: '/admin/portfolios', label: 'Portfolios', icon: FolderOpen },
],
},
{
title: 'Operations',
items: [
{ path: '/admin/email-campaigns', label: 'Email Campaigns', icon: Mail },
{ path: '/admin/integrations', label: 'Integrations', icon: Plug },
{ path: '/admin/enterprise', label: 'Enterprise', icon: Shield },
{ path: '/admin/audit', label: 'Audit Log', icon: FileText },
{ path: '/admin/access-levels', label: 'Access Levels', icon: Key },
],
},
{
title: 'Settings',
items: [
{ path: '/admin/settings', label: 'Settings', icon: Settings },
],
},
];
// --- View Mode ---
type ViewMode = 'admin' | 'partner';
const STORAGE_KEY = 'cs-sidebar-sections';
const MODE_KEY = 'cs-view-mode';
function loadMode(): ViewMode {
try {
return (localStorage.getItem(MODE_KEY) as ViewMode) || 'partner';
} catch {
return 'partner';
}
}
// --- Role Badges ---
const roleLabels: Record<string, string> = {
super_admin: 'Super Admin',
partner: 'Partner',
user: 'User',
};
const roleBadgeColors: Record<string, string> = {
super_admin: 'bg-red-100 text-red-700',
partner: 'bg-blue-100 text-blue-700',
user: 'bg-gray-100 text-gray-600',
};
const companyRoleLabels: Record<CompanyRole, string> = {
owner: 'Owner',
admin: 'Admin',
manager: 'Manager',
rep: 'Rep',
viewer: 'Viewer',
};
const companyRoleBadgeColors: Record<CompanyRole, string> = {
owner: 'bg-purple-100 text-purple-700',
admin: 'bg-indigo-100 text-indigo-700',
manager: 'bg-amber-100 text-amber-700',
rep: 'bg-emerald-100 text-emerald-700',
viewer: 'bg-slate-100 text-slate-600',
};
// --- Helpers ---
function defaultExpanded(mode: ViewMode): Record<string, boolean> {
const sections = mode === 'partner' ? partnerNavSections : adminNavSections;
return sections.reduce((acc, s) => ({ ...acc, [s.title]: s.title === sections[0].title }), {} as Record<string, boolean>);
}
function loadExpanded(mode: ViewMode): Record<string, boolean> {
try {
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
return { ...defaultExpanded(mode), ...stored };
} catch {
return defaultExpanded(mode);
}
}
function isActivePath(pathname: string, itemPath: string): boolean {
return pathname === itemPath || (itemPath !== '/app' && itemPath !== '/admin' && pathname.startsWith(itemPath));
}
// --- Component ---
export function Sidebar({ open, onClose }: { open?: boolean; onClose?: () => void }) {
const location = useLocation();
const { user, isRole, hasCompanyPermission } = useAuth();
const isSuperAdmin = isRole('super_admin');
// View mode toggle (super admins only)
const [mode, setMode] = useState<ViewMode>(() => isSuperAdmin ? loadMode() : 'partner');
useEffect(() => {
localStorage.setItem(MODE_KEY, mode);
}, [mode]);
// Pick sections based on mode
const sections = mode === 'admin' ? adminNavSections : partnerNavSections;
// Filter items by company permission (partner view)
const filteredSections = sections.map(section => ({
...section,
items: isSuperAdmin
? section.items
: section.items.filter(item => !item.permission || hasCompanyPermission(item.permission)),
})).filter(section => section.items.length > 0);
// Expanded state with localStorage persistence
const [expanded, setExpanded] = useState<Record<string, boolean>>(() => loadExpanded(mode));
// Reset expanded when mode changes
useEffect(() => {
setExpanded(defaultExpanded(mode));
}, [mode]);
// Auto-expand section containing the active item
useEffect(() => {
const activeSection = filteredSections.find(s => s.items.some(item => isActivePath(location.pathname, item.path)));
if (activeSection && !expanded[activeSection.title]) {
setExpanded(prev => ({ ...prev, [activeSection.title]: true }));
}
}, [location.pathname]); // eslint-disable-line react-hooks/exhaustive-deps
// Persist expanded state
useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(expanded));
}, [expanded]);
// Close sidebar on route change (mobile only)
useEffect(() => {
if (open && window.innerWidth < 768) {
onClose?.();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname]);
// Close on Escape key
useEffect(() => {
const handleKey = (e: KeyboardEvent) => {
if (e.key === 'Escape' && open) onClose?.();
};
window.addEventListener('keydown', handleKey);
return () => window.removeEventListener('keydown', handleKey);
}, [open, onClose]);
const toggleSection = (title: string) => {
setExpanded(prev => ({ ...prev, [title]: !prev[title] }));
};
return (
<>
{/* Mobile overlay */}
{open && (
<div
className="fixed inset-0 z-40 bg-black/50 lg:hidden"
onClick={onClose}
aria-hidden="true"
/>
)}
<aside
className={`fixed left-0 top-0 z-50 flex h-screen w-64 flex-col border-r border-surface-200 bg-white transition-transform duration-200 ease-in-out ${
open ? 'translate-x-0' : '-translate-x-full'
} lg:translate-x-0`}
>
{/* Mobile close button */}
{onClose && (
<button
type="button"
onClick={onClose}
className="absolute right-3 top-3 flex min-h-[44px] min-w-[44px] items-center justify-center rounded-lg text-surface-500 hover:bg-surface-100 hover:text-surface-600 lg:hidden"
aria-label="Close sidebar"
>
<X className="h-5 w-5" />
</button>
)}
{/* Logo */}
<div className="flex items-center gap-3 border-b border-surface-200 px-4 py-4 sm:px-6">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-brand-600 text-white">
<span className="text-sm font-bold">CS</span>
</div>
<div className="min-w-0">
<h1 className="truncate text-sm font-semibold text-surface-900">Command Sovereignty</h1>
<p className="truncate text-xs text-surface-500">{mode === 'admin' ? 'Admin Panel' : 'Command Center'}</p>
</div>
</div>
{/* View mode toggle — super admins only */}
{isSuperAdmin && (
<div className="px-3 pb-2">
<div className="flex rounded-lg border border-surface-200 p-1 bg-surface-50">
<button
type="button"
onClick={() => setMode('partner')}
className={`flex flex-1 items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-semibold transition-colors min-h-[36px] ${
mode === 'partner'
? 'bg-white text-surface-900 shadow-sm'
: 'text-surface-400 hover:text-surface-600'
}`}
>
<LayoutDashboard className="h-3.5 w-3.5" />
Partner
</button>
<button
type="button"
onClick={() => setMode('admin')}
className={`flex flex-1 items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-semibold transition-colors min-h-[36px] ${
mode === 'admin'
? 'bg-white text-surface-900 shadow-sm'
: 'text-surface-400 hover:text-surface-600'
}`}
>
<ShieldCheck className="h-3.5 w-3.5" />
Admin
</button>
</div>
</div>
)}
{/* Navigation */}
<nav className="flex-1 overflow-y-auto px-3 py-4">
{filteredSections.map((section) => {
const isExpanded = expanded[section.title];
const hasActiveItem = section.items.some(item => isActivePath(location.pathname, item.path));
return (
<div key={section.title} className="mb-1">
{/* Section header */}
<button
type="button"
onClick={() => toggleSection(section.title)}
className="flex w-full items-center gap-1 rounded-md px-2 py-1.5 text-xs font-semibold uppercase tracking-wider text-surface-400 hover:text-surface-600 transition-colors"
aria-expanded={isExpanded}
>
<ChevronDown
className={`h-3 w-3 shrink-0 transition-transform duration-200 ${
isExpanded ? 'rotate-180' : ''
}`}
/>
<span>{section.title}</span>
{hasActiveItem && !isExpanded && (
<span className="ml-auto h-1.5 w-1.5 rounded-full bg-brand-500" />
)}
</button>
{/* Section items */}
<div
className={`grid transition-all duration-200 ${
isExpanded ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'
}`}
>
<div className="overflow-hidden">
<ul className="space-y-0.5 pt-1 pb-1">
{section.items.map((item) => {
const Icon = item.icon;
const isActive = isActivePath(location.pathname, item.path);
return (
<li key={item.path}>
<Link
to={item.path}
className={`group flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors min-h-[40px] ${
isActive
? 'bg-brand-50 text-brand-700'
: 'text-surface-600 hover:bg-surface-100 hover:text-surface-900'
}`}
>
<Icon className="h-4 w-4 shrink-0" />
<span className="truncate">{item.label}</span>
</Link>
</li>
);
})}
</ul>
</div>
</div>
</div>
);
})}
</nav>
{/* User */}
<div className="border-t border-surface-200 px-4 py-3">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-surface-200 text-xs font-semibold text-surface-700">
{user?.full_name?.charAt(0)?.toUpperCase() || '?'}
</div>
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-1.5">
<p className="truncate text-sm font-medium text-surface-900">{user?.full_name || 'User'}</p>
{user?.role && (
<span className={`shrink-0 rounded-full px-1.5 py-0.5 text-[10px] font-semibold ${roleBadgeColors[user.role] || roleBadgeColors.user}`}>
{roleLabels[user.role] || user.role}
</span>
)}
</div>
<div className="flex flex-wrap items-center gap-1.5">
<p className="truncate text-xs text-surface-500">{user?.email}</p>
{!isSuperAdmin && user?.companyRole && (
<span className={`shrink-0 rounded-full px-1.5 py-0.5 text-[10px] font-semibold ${companyRoleBadgeColors[user.companyRole] || companyRoleBadgeColors.viewer}`}>
{companyRoleLabels[user.companyRole]}
</span>
)}
</div>
</div>
</div>
</div>
</aside>
</>
);
}