import React, { useState, useEffect, useMemo } from 'react'; import clsx from 'clsx'; import { NavLink, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useUIStore } from '@/store/uiStore'; import { ChevronRight, FileText, Home, Settings, Users } from 'lucide-react'; import { usePluginStore } from '@/store/pluginStore'; import * as LucideIcons from 'lucide-react'; import { useMenuOrder } from '@/api/users'; interface NavSingleItem { to: string; labelKey: string; icon: React.ReactNode; order: number; } const chevronIcon = (expanded: boolean) => ( ); function getIcon(name: string): React.ReactNode { const Icon = (LucideIcons as any)[name]; return Icon ? : ; } // Only non-plugin items: dashboard, contacts, settings. // All other menu entries (calendar, dms, mail, ai-assistant, automation, reports, tasks, communication) // are provided by plugin manifests via usePluginStore(s => s.getAllMenuItems()). const singleItems: NavSingleItem[] = [ { to: '/dashboard', labelKey: 'nav.dashboard', icon: , order: 0 }, { to: '/contacts', labelKey: 'nav.contacts', icon: , order: 10 }, ]; const bottomItems: NavSingleItem[] = []; export function Sidebar() { const { t } = useTranslation(); const { sidebarOpen, setSidebarOpen } = useUIStore(); const location = useLocation(); const manifests = usePluginStore(s => s.manifests); const { data: menuOrderData } = useMenuOrder(); const allMenuItems = useMemo(() => { const staticItems = singleItems.map(item => ({ path: item.to, labelKey: item.labelKey, label: item.labelKey, icon: item.icon, order: item.order, group: undefined as string | undefined, isStatic: true as const, })); const pluginItems = manifests .flatMap((m) => m.menu_items) .map(item => ({ path: item.path, labelKey: item.label_key, label: item.label, icon: item.icon, order: item.order, group: item.group, isStatic: false as const, })); const allItems = [...staticItems, ...pluginItems]; // Apply saved menu order if available const savedOrder = menuOrderData?.menu_order; if (savedOrder && savedOrder.length > 0) { const orderMap = new Map(savedOrder.map((id, idx) => [id, idx])); return allItems.sort((a, b) => { // Map path to menu ID: static items use their path without leading / const aId = a.path.startsWith('/') ? a.path.slice(1) : a.path; const bId = b.path.startsWith('/') ? b.path.slice(1) : b.path; const aIdx = orderMap.get(aId); const bIdx = orderMap.get(bId); if (aIdx !== undefined && bIdx !== undefined) return aIdx - bIdx; if (aIdx !== undefined) return -1; if (bIdx !== undefined) return 1; // Fall back to order field for unknown items if (a.order !== b.order) return a.order - b.order; return a.label.localeCompare(b.label); }); } return allItems.sort((a, b) => { if (a.order !== b.order) return a.order - b.order; return a.label.localeCompare(b.label); }); }, [manifests, menuOrderData]); const [expandedItems, setExpandedItems] = useState>(new Set()); useEffect(() => { setExpandedItems((prev) => { const next = new Set(prev); // Auto-expand groups whose child route is active for (const item of allMenuItems) { if (item.group && location.pathname.startsWith(item.path)) { next.add(`plugin-group-${item.group}`); } } return next; }); }, [location.pathname, allMenuItems]); const toggleExpand = (labelKey: string) => { setExpandedItems((prev) => { const next = new Set(prev); if (next.has(labelKey)) next.delete(labelKey); else next.add(labelKey); return next; }); }; return ( <> {sidebarOpen && ( setSidebarOpen(false)} aria-hidden="true" data-testid="sidebar-overlay" /> )} > ); }