import { useEffect, useRef } from 'react' import { APP_VERSION } from '@/lib/version' import { Link, useRouterState, useNavigate } from '@tanstack/react-router' import { Monitor, Building2, Users, Settings, LayoutDashboard, Wifi, MapPin, Bell, Map, Terminal, FileCode, Download, Wrench, ClipboardList, BellRing, Calendar, FileBarChart, Sun, Moon, LogOut, ChevronDown, } from 'lucide-react' import { cn } from '@/lib/utils' import { useAuth, isSuperAdmin, isTenantAdmin } from '@/lib/auth' import { useUIStore } from '@/lib/store' import { useEventStreamContext } from '@/contexts/EventStreamContext' import { alertEventsApi, tenantsApi } from '@/lib/api' import { useQuery } from '@tanstack/react-query' import { RugLogo } from '@/components/brand/RugLogo' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import type { ConnectionState } from '@/hooks/useEventStream' // ─── Types ────────────────────────────────────────────────────────────────── interface NavItem { label: string href: string icon: React.FC<{ className?: string }> exact?: boolean badge?: number } // ─── Constants ────────────────────────────────────────────────────────────── const SYSTEM_TENANT_ID = '00000000-0000-0000-0000-000000000000' const CONNECTION_LABELS: Record = { connected: 'Connected', connecting: 'Connecting', reconnecting: 'Reconnecting', disconnected: 'Disconnected', } // ─── Styles ───────────────────────────────────────────────────────────────── const navItemBase = 'flex items-center gap-2 text-[13px] py-[5px] px-2 pl-[10px] border-l-2 border-transparent transition-[border-color,color] duration-[50ms] linear' const navItemInactive = 'text-text-secondary hover:border-accent' const navItemActive = 'text-text-primary font-medium border-accent bg-accent-soft rounded-r-sm' const lowFreqBase = 'flex items-center gap-2 text-[13px] text-text-muted py-[3px] px-2 pl-[10px] border-l-2 border-transparent transition-[border-color,color] duration-[50ms] linear hover:border-accent' const iconClass = 'h-4 w-4 text-text-muted flex-shrink-0' // ─── Component ────────────────────────────────────────────────────────────── export function Sidebar() { const { user, logout } = useAuth() const { sidebarCollapsed, toggleSidebar, mobileSidebarOpen, setMobileSidebarOpen, selectedTenantId, setSelectedTenantId, theme, setTheme, } = useUIStore() const { connectionState } = useEventStreamContext() const routerState = useRouterState() const currentPath = routerState.location.pathname const navigate = useNavigate() const navRef = useRef(null) const superAdmin = isSuperAdmin(user) const tenantAdmin = isTenantAdmin(user) const tenantId = superAdmin ? selectedTenantId : user?.tenant_id // ─── Queries ──────────────────────────────────────────────────────────── const { data: tenants } = useQuery({ queryKey: ['tenants'], queryFn: tenantsApi.list, enabled: superAdmin, select: (data) => data.filter((t) => t.id !== SYSTEM_TENANT_ID), }) const selectedTenant = tenants?.find((t) => t.id === selectedTenantId) // Auto-select when there's exactly one tenant and nothing selected useEffect(() => { if (superAdmin && tenants && tenants.length === 1 && !selectedTenantId) { setSelectedTenantId(tenants[0].id) } }, [tenants, selectedTenantId, superAdmin, setSelectedTenantId]) const { data: alertCount } = useQuery({ queryKey: ['alert-active-count', tenantId], queryFn: () => alertEventsApi.activeCount(tenantId!), enabled: !!tenantId, refetchInterval: 30_000, }) // ─── Tenant display name ─────────────────────────────────────────────── const tenantName = superAdmin ? (selectedTenant?.name ?? 'All Orgs') : (user?.name ?? user?.email ?? 'Tenant') // ─── Nav items ──────────────────────────────────────────────────────── const operateItems: NavItem[] = [ { label: 'Overview', href: '/', icon: LayoutDashboard, exact: true }, ...(!superAdmin && user?.tenant_id ? [ { label: 'Devices', href: `/tenants/${user.tenant_id}/devices`, icon: Monitor }, { label: 'Sites', href: `/tenants/${user.tenant_id}/sites`, icon: MapPin }, ] : []), { label: 'Alerts', href: '/alerts', icon: Bell, badge: alertCount && alertCount > 0 ? alertCount : undefined, }, ...(!superAdmin && user?.tenant_id ? [{ label: 'Wireless', href: `/tenants/${user.tenant_id}/wireless-links`, icon: Wifi }] : [{ label: 'Wireless', href: '/wireless', icon: Wifi }] ), { label: 'Map', href: '/map', icon: Map }, ] const actItems: NavItem[] = [ { label: 'Config', href: '/config-editor', icon: Terminal }, { label: 'Templates', href: '/templates', icon: FileCode }, { label: 'Firmware', href: '/firmware', icon: Download }, { label: 'Commands', href: '/bulk-commands', icon: Wrench }, ] const lowFreqItems: NavItem[] = [ ...(superAdmin || tenantAdmin ? [{ label: 'Organizations', href: '/tenants', icon: Building2 }] : []), ...(tenantAdmin && user?.tenant_id ? [{ label: 'Users', href: `/tenants/${user.tenant_id}/users`, icon: Users }] : []), { label: 'Alert Rules', href: '/alert-rules', icon: BellRing }, { label: 'Maintenance', href: '/maintenance', icon: Calendar }, { label: 'Settings', href: '/settings', icon: Settings }, { label: 'Audit Log', href: '/audit', icon: ClipboardList }, { label: 'Reports', href: '/reports', icon: FileBarChart }, ] // ─── Active state ───────────────────────────────────────────────────── const isActive = (item: NavItem) => { if (item.exact) return currentPath === item.href if (item.href === '/settings') return currentPath === '/settings' || currentPath.startsWith('/settings/') return currentPath.startsWith(item.href) && item.href.length > 1 } // ─── Focus trap for mobile ─────────────────────────────────────────── useEffect(() => { if (!mobileSidebarOpen) return const sidebar = document.getElementById('mobile-sidebar') if (!sidebar) return const focusable = sidebar.querySelectorAll('a, button, input') if (focusable.length) focusable[0].focus() function handleKeyDown(e: KeyboardEvent) { if (e.key === 'Escape') { setMobileSidebarOpen(false) return } if (e.key === 'Tab') { const els = sidebar!.querySelectorAll('a, button, input') const first = els[0] const last = els[els.length - 1] if (e.shiftKey && document.activeElement === first) { e.preventDefault() last.focus() } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault() first.focus() } } } document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) }, [mobileSidebarOpen, setMobileSidebarOpen]) // ─── Keyboard shortcut: [ to toggle ─────────────────────────────────── useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ( e.key === '[' && !['INPUT', 'TEXTAREA', 'SELECT'].includes( (e.target as HTMLElement).tagName, ) ) { e.preventDefault() toggleSidebar() } } document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) }, [toggleSidebar]) // ─── Logout handler ─────────────────────────────────────────────────── const handleLogout = async () => { await logout() void navigate({ to: '/login' }) } // ─── Render helpers ─────────────────────────────────────────────────── const renderNavItem = (item: NavItem, collapsed: boolean) => { const Icon = item.icon const active = isActive(item) return ( setMobileSidebarOpen(false)} data-testid={`nav-${item.label.toLowerCase().replace(/\s+/g, '-')}`} className={cn( collapsed ? 'flex items-center justify-center py-[5px] px-2 border-l-2 border-transparent transition-[border-color,color] duration-[50ms] linear hover:border-accent' : navItemBase, active ? navItemActive : collapsed ? 'text-text-secondary' : navItemInactive, )} title={collapsed ? item.label : undefined} aria-label={collapsed ? item.label : undefined} aria-current={active ? 'page' : undefined} >