6aa4f96bd2
- Auto-collapse sidebar at <1536px (all laptops), expand at ≥1536px - Responsive drawer/panel widths across Pipeline, Properties, MyListings, Anfragen, MatchDetail, AISearch - Reminder Manager: dot+label priority badge, Fläche column removed, status only for non-active rows - ReminderKpiBar: focal Überfällig card, three secondary KPIs - ReminderDetailDrawer: Task Panel redesign — Finanzen removed, Notiz promoted, Pre-Market as inline badge, full create form - useCreateReminder hook wired to reminderService.create with cache invalidation - Mock data: contract-derived reminders (LEASE_EXPIRY, BREAK_OPTION, RENT_REVIEW, INSURANCE_RENEWAL, SCHATTENMARKT_RELEASE) now show auto-creation note in Verlauf Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
124 lines
4.5 KiB
TypeScript
124 lines
4.5 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { Outlet, useNavigate, useLocation } from 'react-router'
|
|
import { Box, Drawer, useTheme, useMediaQuery } from '@mui/material'
|
|
import { useLayoutStore } from '../../stores/layoutStore'
|
|
import { useSessionStore } from '../../stores/sessionStore'
|
|
import { WelcomeDialog } from '../onboarding/WelcomeDialog'
|
|
import { RightContextPanel } from './RightContextPanel'
|
|
import { GlobalAIAssistantDrawer } from '../assistant'
|
|
import { ToastProvider } from '../ui'
|
|
import { Sidebar } from './AppShellSidebar'
|
|
import { TopBar } from './AppShellTopBar'
|
|
import { WORKSPACE_CONFIG, WORKSPACE_ORDER, getWorkspaceFromPath } from './appShellConfig'
|
|
import { WorkspaceType } from '../../domain/enums'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AppShell
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export function AppShell() {
|
|
const activeWorkspace = useLayoutStore(s => s.activeWorkspace)
|
|
const sidebarCollapsed = useLayoutStore(s => s.sidebarCollapsed)
|
|
const setActiveWorkspace = useLayoutStore(s => s.setActiveWorkspace)
|
|
const toggleSidebar = useLayoutStore(s => s.toggleSidebar)
|
|
const setSidebarCollapsed = useLayoutStore(s => s.setSidebarCollapsed)
|
|
const { currentUser } = useSessionStore()
|
|
const navigate = useNavigate()
|
|
const location = useLocation()
|
|
const theme = useTheme()
|
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
|
|
const isCompact = useMediaQuery(theme.breakpoints.down('xl'))
|
|
const [mobileOpen, setMobileOpen] = useState(false)
|
|
|
|
// Close mobile menu on route change
|
|
useEffect(() => { setMobileOpen(false) }, [location.pathname])
|
|
|
|
// Auto-collapse sidebar on compact screens (laptops < 1536px)
|
|
useEffect(() => { setSidebarCollapsed(isCompact) }, [isCompact, setSidebarCollapsed])
|
|
|
|
// Sync active workspace with URL
|
|
useEffect(() => {
|
|
const detected = getWorkspaceFromPath(location.pathname)
|
|
if (detected && detected !== activeWorkspace) {
|
|
setActiveWorkspace(detected)
|
|
}
|
|
}, [location.pathname, activeWorkspace, setActiveWorkspace])
|
|
|
|
// When the user changes (role switch), redirect to their first allowed workspace
|
|
// if the current workspace is no longer permitted
|
|
useEffect(() => {
|
|
if (!currentUser) return
|
|
const allowed = currentUser.allowedWorkspaces
|
|
if (!allowed.includes(activeWorkspace)) {
|
|
const first = allowed[0]
|
|
if (first) {
|
|
setActiveWorkspace(first)
|
|
navigate(WORKSPACE_CONFIG[first].firstPath, { replace: true })
|
|
}
|
|
}
|
|
// currentUser object reference changes on every role switch — that's the correct trigger
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [currentUser])
|
|
|
|
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 = (
|
|
<Sidebar
|
|
collapsed={isMobile ? false : sidebarCollapsed}
|
|
activeWorkspace={activeWorkspace}
|
|
allowedWorkspaces={allowedWorkspaces}
|
|
onWorkspaceClick={handleWorkspaceClick}
|
|
onToggle={isMobile ? () => setMobileOpen(false) : toggleSidebar}
|
|
onClose={isMobile ? () => setMobileOpen(false) : undefined}
|
|
userName={userName}
|
|
orgName={orgName}
|
|
/>
|
|
)
|
|
|
|
return (
|
|
<Box sx={{ display: 'flex', height: '100vh', overflow: 'hidden' }}>
|
|
{/* Desktop: permanent sidebar */}
|
|
{!isMobile && sidebarContent}
|
|
|
|
{/* Mobile: temporary drawer */}
|
|
{isMobile && (
|
|
<Drawer
|
|
open={mobileOpen}
|
|
onClose={() => setMobileOpen(false)}
|
|
variant="temporary"
|
|
ModalProps={{ keepMounted: true }}
|
|
slotProps={{ paper: { sx: { width: 264, bgcolor: 'transparent', boxShadow: 'none' } } }}
|
|
>
|
|
{sidebarContent}
|
|
</Drawer>
|
|
)}
|
|
|
|
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
|
<TopBar
|
|
activeWorkspace={activeWorkspace}
|
|
pathname={location.pathname}
|
|
isMobile={isMobile}
|
|
onMenuClick={() => setMobileOpen(true)}
|
|
/>
|
|
|
|
<Box component="main" sx={{ flex: 1, overflowY: 'auto' }}>
|
|
<Outlet />
|
|
</Box>
|
|
|
|
<RightContextPanel />
|
|
</Box>
|
|
|
|
<GlobalAIAssistantDrawer />
|
|
<ToastProvider />
|
|
<WelcomeDialog />
|
|
</Box>
|
|
)
|
|
}
|