/** * MainLayout * * Hauptlayout der Anwendung mit Sidebar und Content-Bereich. * Enthält den FeatureProvider für das Multi-Tenant-System. */ import React, { useEffect, useRef, useState } from 'react'; import { Outlet, useLocation } from 'react-router-dom'; import { FeatureProvider, useFeatureStore } from '../stores/featureStore'; import { MandateNavigation } from '../components/Navigation/MandateNavigation'; import { UserSection } from '../components/Navigation/UserSection'; import { RagRunningBadge } from '../components/RagRunningBadge/RagRunningBadge'; import { KEEP_ALIVE_ROUTES, hideFeatureOutlet } from '../config/keepAliveRoutes'; import type { KeepAliveEntry, KeepAliveScopedEntry, KeepAliveUnscopedEntry } from '../types/keepAlive.types'; import { isKeepAliveScoped } from '../types/keepAlive.types'; import styles from './MainLayout.module.css'; import { useLanguage } from '../providers/language/LanguageContext'; const keepAliveShellStyle = (isVisible: boolean, shellOverflowHidden: boolean): React.CSSProperties => ({ display: isVisible ? 'flex' : 'none', flexDirection: 'column', position: 'absolute', top: 'var(--mobile-topbar-height, 0px)', left: 0, right: 0, bottom: 0, ...(shellOverflowHidden ? { overflow: 'hidden' as const } : {}), }); const RoutedKeepAliveUnscoped: React.FC<{ entry: KeepAliveUnscopedEntry; pathname: string }> = ({ entry, pathname, }) => { const isVisible = entry.pathRegex.test(pathname); return (
{entry.render()}
); }; const RoutedKeepAliveScoped: React.FC<{ entry: KeepAliveScopedEntry; pathname: string }> = ({ entry, pathname, }) => { const isVisible = entry.pathRegex.test(pathname); const { scopeRegex, requireMandateForMount = true, shellOverflowHidden = true, render, } = entry; const cachedMandateIdRef = useRef(''); const cachedInstanceIdRef = useRef(''); const match = pathname.match(scopeRegex); if (match?.[1] && match?.[2]) { cachedMandateIdRef.current = match[1]; cachedInstanceIdRef.current = match[2]; } const mandateId = cachedMandateIdRef.current; const instanceId = cachedInstanceIdRef.current; const scopeReady = requireMandateForMount ? !!(mandateId && instanceId) : !!instanceId; if (!scopeReady) { return null; } const scopeKey = `${mandateId}:${instanceId}`; return (
{render({ mandateId, instanceId, scopeKey })}
); }; const RoutedKeepAliveSlot: React.FC<{ entry: KeepAliveEntry; pathname: string }> = ({ entry, pathname, }) => { if (!isKeepAliveScoped(entry)) { return ; } return ; }; // ============================================================================= // INNER LAYOUT (mit Zugriff auf Store) // ============================================================================= const MainLayoutInner: React.FC = () => { const { t } = useLanguage(); const { loadFeatures, initialized, loading, error } = useFeatureStore(); const location = useLocation(); const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false); const hideOutletShell = hideFeatureOutlet(location.pathname); // Features laden beim Mount useEffect(() => { if (!initialized && !loading) { loadFeatures(); } }, [initialized, loading, loadFeatures]); useEffect(() => { setIsMobileSidebarOpen(false); }, [location.pathname]); useEffect(() => { const handleResize = () => { if (window.innerWidth > 1024) { setIsMobileSidebarOpen(false); } }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); return (
{isMobileSidebarOpen && ( PowerOn
{KEEP_ALIVE_ROUTES.map((routeEntry) => ( ))}
); }; // ============================================================================= // MAIN LAYOUT (mit Provider) // ============================================================================= export const MainLayout: React.FC = () => { return ( ); }; export default MainLayout;