import React, { useState, useCallback, forwardRef, useMemo } from 'react'; import { clsx } from 'clsx'; import { useLocation, useNavigate } from 'react-router-dom'; import { useAuthUser, useIsAuthenticated } from '../../../stores'; import { getNavigationRoutes, canAccessRoute, ROUTES } from '../../../router/routes.config'; import { Button } from '../../ui'; import { Badge } from '../../ui'; import { Tooltip } from '../../ui'; import { LayoutDashboard, Package, Factory, BarChart3, Brain, ShoppingCart, Truck, Zap, Database, GraduationCap, Bell, Settings, User, CreditCard, ChevronLeft, ChevronRight, ChevronDown, Dot, Menu } from 'lucide-react'; export interface SidebarProps { className?: string; /** * Whether the sidebar is open (mobile drawer state) */ isOpen?: boolean; /** * Whether the sidebar is collapsed (desktop state) */ isCollapsed?: boolean; /** * Callback when sidebar is closed (mobile) */ onClose?: () => void; /** * Callback when sidebar collapse state is toggled (desktop) */ onToggleCollapse?: () => void; /** * Custom navigation items */ customItems?: NavigationItem[]; /** * Show/hide collapse button */ showCollapseButton?: boolean; /** * Show/hide footer */ showFooter?: boolean; } export interface NavigationItem { id: string; label: string; path: string; icon?: React.ComponentType<{ className?: string }>; badge?: { text: string; variant?: 'default' | 'primary' | 'success' | 'warning' | 'error'; }; children?: NavigationItem[]; requiredPermissions?: string[]; requiredRoles?: string[]; disabled?: boolean; external?: boolean; } export interface SidebarRef { scrollToItem: (path: string) => void; expandItem: (path: string) => void; collapseItem: (path: string) => void; } const iconMap: Record> = { dashboard: LayoutDashboard, inventory: Package, production: Factory, sales: BarChart3, forecasting: Brain, orders: ShoppingCart, procurement: Truck, pos: Zap, data: Database, training: GraduationCap, notifications: Bell, settings: Settings, user: User, 'credit-card': CreditCard, }; /** * Sidebar - Navigation sidebar with collapsible menu items and role-based access * * Features: * - Hierarchical navigation menu with icons * - Icons for menu items with fallback support * - Collapsible sections with smooth animations * - Active state highlighting with proper contrast * - Role-based menu filtering and permissions * - Responsive design with mobile drawer mode * - Keyboard navigation support * - Smooth scrolling to items * - Badge support for notifications/counts */ export const Sidebar = forwardRef(({ className, isOpen = false, isCollapsed = false, onClose, onToggleCollapse, customItems, showCollapseButton = true, showFooter = true, }, ref) => { const location = useLocation(); const navigate = useNavigate(); const user = useAuthUser(); const isAuthenticated = useIsAuthenticated(); const [expandedItems, setExpandedItems] = useState>(new Set()); const [hoveredItem, setHoveredItem] = useState(null); const sidebarRef = React.useRef(null); // Get navigation routes from config and convert to navigation items - memoized const navigationItems = useMemo(() => { const navigationRoutes = getNavigationRoutes(); const convertRoutesToItems = (routes: typeof navigationRoutes): NavigationItem[] => { return routes.map(route => ({ id: route.path, label: route.title, path: route.path, icon: route.icon ? iconMap[route.icon] : undefined, requiredPermissions: route.requiredPermissions, requiredRoles: route.requiredRoles, children: route.children ? convertRoutesToItems(route.children) : undefined, })); }; return customItems || convertRoutesToItems(navigationRoutes); }, [customItems]); // Filter items based on user permissions - memoized to prevent infinite re-renders const visibleItems = useMemo(() => { const filterItemsByPermissions = (items: NavigationItem[]): NavigationItem[] => { if (!isAuthenticated || !user) return []; return items.map(item => ({ ...item, // Create a shallow copy to avoid mutation children: item.children ? filterItemsByPermissions(item.children) : item.children })).filter(item => { const userRoles = user.role ? [user.role] : []; const userPermissions: string[] = user?.permissions || []; const hasAccess = !item.requiredPermissions && !item.requiredRoles || canAccessRoute( { path: item.path, requiredRoles: item.requiredRoles, requiredPermissions: item.requiredPermissions } as any, isAuthenticated, userRoles, userPermissions ); return hasAccess; }); }; return filterItemsByPermissions(navigationItems); }, [navigationItems, isAuthenticated, user]); // Handle item click const handleItemClick = useCallback((item: NavigationItem) => { if (item.disabled) return; if (item.children && item.children.length > 0) { // Toggle expansion for parent items setExpandedItems(prev => { const newSet = new Set(prev); if (newSet.has(item.id)) { newSet.delete(item.id); } else { newSet.add(item.id); } return newSet; }); } else { // Navigate to item if (item.external) { window.open(item.path, '_blank'); } else { navigate(item.path); if (onClose) onClose(); // Close mobile drawer } } }, [navigate, onClose]); // Scroll to item const scrollToItem = useCallback((path: string) => { const element = sidebarRef.current?.querySelector(`[data-path="${path}"]`); if (element) { element.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, []); // Expand item const expandItem = useCallback((path: string) => { setExpandedItems(prev => new Set(prev).add(path)); }, []); // Collapse item const collapseItem = useCallback((path: string) => { setExpandedItems(prev => { const newSet = new Set(prev); newSet.delete(path); return newSet; }); }, []); // Auto-expand parent items for active path const findParentPaths = useCallback((items: NavigationItem[], targetPath: string, parents: string[] = []): string[] => { for (const item of items) { const currentPath = [...parents, item.id]; if (item.path === targetPath) { return parents; } if (item.children) { const found = findParentPaths(item.children, targetPath, currentPath); if (found.length > 0) { return found; } } } return []; }, []); React.useEffect(() => { const parentPaths = findParentPaths(visibleItems, location.pathname); if (parentPaths.length > 0) { setExpandedItems(prev => new Set([...prev, ...parentPaths])); } }, [location.pathname, findParentPaths, visibleItems]); // Expose ref methods React.useImperativeHandle(ref, () => ({ scrollToItem, expandItem, collapseItem, }), [scrollToItem, expandItem, collapseItem]); // Handle keyboard navigation and touch gestures React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape' && isOpen && onClose) { onClose(); } }; let touchStartX = 0; let touchStartY = 0; const handleTouchStart = (e: TouchEvent) => { if (isOpen) { touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; } }; const handleTouchMove = (e: TouchEvent) => { if (!isOpen || !onClose) return; const touchCurrentX = e.touches[0].clientX; const touchCurrentY = e.touches[0].clientY; const deltaX = touchStartX - touchCurrentX; const deltaY = Math.abs(touchStartY - touchCurrentY); // Only trigger swipe left to close if it's more horizontal than vertical // and the swipe distance is significant if (deltaX > 50 && deltaX > deltaY * 2) { onClose(); } }; document.addEventListener('keydown', handleKeyDown); document.addEventListener('touchstart', handleTouchStart, { passive: true }); document.addEventListener('touchmove', handleTouchMove, { passive: true }); return () => { document.removeEventListener('keydown', handleKeyDown); document.removeEventListener('touchstart', handleTouchStart); document.removeEventListener('touchmove', handleTouchMove); }; }, [isOpen, onClose]); // Render submenu overlay for collapsed sidebar const renderSubmenuOverlay = (item: NavigationItem) => { if (!item.children || item.children.length === 0) return null; return (
{item.label}
    {item.children.map(child => (
  • ))}
); }; // Render navigation item const renderItem = (item: NavigationItem, level = 0) => { const isActive = location.pathname === item.path || location.pathname.startsWith(item.path + '/'); const isExpanded = expandedItems.has(item.id); const hasChildren = item.children && item.children.length > 0; const isHovered = hoveredItem === item.id; const ItemIcon = item.icon; const itemContent = (
0 && 'pl-6', )} >
{ItemIcon && ( )} {/* Submenu indicator for collapsed sidebar */} {isCollapsed && hasChildren && level === 0 && (
)}
{!ItemIcon && level > 0 && ( )} {(!isCollapsed || level > 0) && ( <> {item.label} {item.badge && ( {item.badge.text} )} {hasChildren && ( )} )}
); const button = (
{/* Submenu overlay for collapsed sidebar */} {isCollapsed && hasChildren && level === 0 && isHovered && (
setHoveredItem(item.id)} onMouseLeave={() => setHoveredItem(null)} > {renderSubmenuOverlay(item)}
)}
); return (
  • {isCollapsed && !hasChildren && ItemIcon ? ( {button} ) : ( button )} {hasChildren && isExpanded && (!isCollapsed || level > 0) && (
      {item.children?.map(child => renderItem(child, level + 1))}
    )}
  • ); }; if (!isAuthenticated) { return null; } return ( <> {/* Desktop Sidebar */} {/* Mobile Drawer */} ); }); Sidebar.displayName = 'Sidebar';