import { useEffect, useState } from 'react' import { Outlet, NavLink, useNavigate, useLocation } from 'react-router' import { WelcomeDialog } from '../onboarding/WelcomeDialog' import { useLayoutStore } from '../../stores/layoutStore' import { useSessionStore } from '../../stores/sessionStore' import { WorkspaceType } from '../../domain/enums' import { Box, Drawer, Typography, Avatar, IconButton, Tooltip, Chip, Button, useTheme, useMediaQuery, } from '@mui/material' import { LayoutDashboard, Building2, Target, CheckSquare, Search, List, Columns2, Bookmark, ClipboardList, Activity, Shield, ChevronLeft, ChevronRight, Sparkles, Clock, Radar, ServerCog, GitBranch, MessageSquare, Menu, Kanban, } from 'lucide-react' import type { LucideIcon } from 'lucide-react' import { OrganizationContextBadge } from './OrganizationContextBadge' import { UserMenu } from './UserMenu' import { NotificationButton } from './NotificationButton' import { RightContextPanel } from './RightContextPanel' import { CompareTray } from './CompareTray' import { GlobalAIAssistantDrawer, GlobalAIAssistantButton } from '../assistant' import { useAssistantStore } from '../../stores/assistantStore' import { ToastProvider } from '../ui' // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- interface NavItem { path: string label: string icon: LucideIcon } interface WorkspaceConfig { label: string abbreviation: string icon: LucideIcon firstPath: string navItems: NavItem[] chipColor: string } // --------------------------------------------------------------------------- // Workspace configuration // --------------------------------------------------------------------------- const WORKSPACE_CONFIG: Record = { [WorkspaceType.SUPPLY]: { label: 'Verwaltung', abbreviation: 'VW', icon: Building2, firstPath: '/supply/dashboard', chipColor: '#1e3a5f', navItems: [ { path: '/supply/dashboard', label: 'Übersicht', icon: LayoutDashboard }, { path: '/supply/properties', label: 'Meine Objekte', icon: Building2 }, { path: '/supply/anfragen', label: 'Anfragencenter', icon: MessageSquare }, { path: '/supply/data-quality', label: 'Datenpflege', icon: CheckSquare }, { path: '/supply/market-intelligence', label: 'Markt Intelligence', icon: Radar }, ], }, [WorkspaceType.DEMAND]: { label: 'Suche', abbreviation: 'SU', icon: Search, firstPath: '/demand/ai-search', chipColor: '#1a7a4a', navItems: [ { path: '/demand/ai-search', label: 'Flächensuche', icon: Search }, { path: '/demand/results', label: 'Ergebnisse', icon: List }, { path: '/demand/compare', label: 'Vergleich', icon: Columns2 }, { path: '/demand/shortlists', label: 'Shortlists', icon: Bookmark }, { path: '/demand/pipeline', label: 'Deal Pipeline', icon: Kanban }, ], }, [WorkspaceType.OPERATIONS]: { label: 'Administration', abbreviation: 'ADM', icon: Shield, firstPath: '/ops/review-queue', chipColor: '#7c3aed', navItems: [ { path: '/ops/review-queue', label: 'Review Queue', icon: ClipboardList }, { path: '/ops/ai-monitoring', label: 'AI Monitoring', icon: Activity }, { path: '/ops/governance', label: 'Governance', icon: Shield }, { path: '/ops/source-monitoring', label: 'Source Monitoring', icon: ServerCog }, { path: '/ops/signal-pipeline', label: 'Signal Pipeline', icon: GitBranch }, { path: '/ops/activity-timeline', label: 'Aktivitäts-Timeline', icon: Clock }, ], }, } // Ordered list for rendering workspace tabs const WORKSPACE_ORDER: WorkspaceType[] = [ WorkspaceType.SUPPLY, WorkspaceType.DEMAND, WorkspaceType.OPERATIONS, ] // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function getWorkspaceFromPath(pathname: string): WorkspaceType | null { if (pathname.startsWith('/supply')) return WorkspaceType.SUPPLY if (pathname.startsWith('/demand')) return WorkspaceType.DEMAND if (pathname.startsWith('/ops')) return WorkspaceType.OPERATIONS return null } function getPageNameFromPath(pathname: string): string { for (const ws of Object.values(WORKSPACE_CONFIG)) { for (const item of ws.navItems) { if (item.path === pathname) return item.label } } // Fallback: last segment, capitalised const segment = pathname.split('/').filter(Boolean).pop() ?? '' return segment.charAt(0).toUpperCase() + segment.slice(1).replace(/-/g, ' ') } function getUserInitials(name: string): string { return name .split(' ') .map((n) => n[0]) .join('') .toUpperCase() .slice(0, 2) } // --------------------------------------------------------------------------- // Sub-components // --------------------------------------------------------------------------- const SIDEBAR_BG = '#0f1923' const DIVIDER_COLOR = 'rgba(255,255,255,0.08)' const TEXT_MUTED = '#94a3b8' const TEXT_WHITE = '#ffffff' const ACTIVE_BG = 'rgba(255,255,255,0.1)' const NAV_ACTIVE_BG = 'rgba(255,255,255,0.12)' const NAV_HOVER_BG = 'rgba(255,255,255,0.06)' interface SidebarProps { collapsed: boolean activeWorkspace: WorkspaceType allowedWorkspaces: WorkspaceType[] onWorkspaceClick: (workspace: WorkspaceType) => void onToggle: () => void onClose?: () => void userName: string orgName: string } function Sidebar({ collapsed, activeWorkspace, allowedWorkspaces, onWorkspaceClick, onToggle, onClose, userName, orgName, }: SidebarProps) { const config = WORKSPACE_CONFIG[activeWorkspace] const width = collapsed ? 60 : 240 const visibleWorkspaces = WORKSPACE_ORDER.filter((ws) => allowedWorkspaces.includes(ws)) return ( {/* Logo area */} {collapsed ? ( PM ) : ( Property Match )} {/* Workspace tabs */} {visibleWorkspaces.map((ws) => { const wsConfig = WORKSPACE_CONFIG[ws] const Icon = wsConfig.icon const isActive = ws === activeWorkspace const tabContent = ( { onWorkspaceClick(ws); onClose?.() }} sx={{ display: 'flex', alignItems: 'center', gap: 1.25, px: collapsed ? 0 : 1.5, py: 0.75, borderRadius: 1, cursor: 'pointer', justifyContent: collapsed ? 'center' : 'flex-start', backgroundColor: isActive ? ACTIVE_BG : 'transparent', '&:hover': { backgroundColor: isActive ? ACTIVE_BG : NAV_HOVER_BG, }, transition: 'background-color 0.15s ease', }} > {!collapsed && ( {wsConfig.label} )} ) return collapsed ? ( {tabContent} ) : ( {tabContent} ) })} {/* Nav items */} {config.navItems.map((item) => { const Icon = item.icon const navContent = ( onClose?.()} > {({ isActive }) => ( {!collapsed && ( {item.label} )} )} ) return collapsed ? ( {navContent} ) : ( {navContent} ) })} {/* Bottom section */} {!collapsed && ( {getUserInitials(userName)} {userName} {orgName} )} {collapsed ? : } ) } interface TopBarProps { activeWorkspace: WorkspaceType pathname: string onMenuClick?: () => void isMobile?: boolean } function TopBar({ activeWorkspace, pathname, onMenuClick, isMobile }: TopBarProps) { const config = WORKSPACE_CONFIG[activeWorkspace] const pageName = getPageNameFromPath(pathname) const openAssistant = useAssistantStore(s => s.open) return ( {/* Left side */} {isMobile && ( )} {pageName} {/* Right side */} ) } // --------------------------------------------------------------------------- // AppShell // --------------------------------------------------------------------------- export function AppShell() { const { activeWorkspace, sidebarCollapsed, setActiveWorkspace, toggleSidebar } = useLayoutStore() const { currentUser } = useSessionStore() const navigate = useNavigate() const location = useLocation() const theme = useTheme() const isMobile = useMediaQuery(theme.breakpoints.down('md')) const [mobileOpen, setMobileOpen] = useState(false) // Close mobile menu on route change useEffect(() => { setMobileOpen(false) }, [location.pathname]) // Sync active workspace with URL useEffect(() => { const detected = getWorkspaceFromPath(location.pathname) if (detected && detected !== activeWorkspace) { setActiveWorkspace(detected) } }, [location.pathname, activeWorkspace, setActiveWorkspace]) const handleWorkspaceClick = (workspace: WorkspaceType) => { setActiveWorkspace(workspace) navigate(WORKSPACE_CONFIG[workspace].firstPath) } const userName = currentUser?.name ?? 'User' const orgName = currentUser?.organizationName ?? '' const allowedWorkspaces = currentUser?.allowedWorkspaces ?? WORKSPACE_ORDER const sidebarContent = ( setMobileOpen(false) : toggleSidebar} onClose={isMobile ? () => setMobileOpen(false) : undefined} userName={userName} orgName={orgName} /> ) return ( {/* Desktop: permanent sidebar */} {!isMobile && sidebarContent} {/* Mobile: temporary drawer */} {isMobile && ( setMobileOpen(false)} variant="temporary" ModalProps={{ keepMounted: true }} slotProps={{ paper: { sx: { width: 240, bgcolor: 'transparent', boxShadow: 'none' } } }} > {sidebarContent} )} setMobileOpen(true)} /> ) }