ui-nyla/src/layouts/MainLayout.tsx
ValueOn AG 9b99020686 feat(billing): Nutzerhinweise bei leerem Budget + Mandats-Mail (402/SSE)
Gateway
- InsufficientBalanceException: billingModel, userAction (TOP_UP_SELF /
  CONTACT_MANDATE_ADMIN), DE/EN-Texte, toClientDict(), fromBalanceCheck()
- HTTP 402 + JSON detail für globale API-Fehlerbehandlung
- AI/Chatbot: vor Raise ggf. E-Mail an BillingSettings.notifyEmails
  (PREPAY_MANDATE, Throttle 1h/Mandat) via billingExhaustedNotify
- Agent-Loop & Workspace-Route: SSE-ERROR mit strukturiertem Billing-Payload
- datamodelBilling: notifyEmails-Doku für Pool-Alerts
frontend_nyla
- useWorkspace: SSE onError für INSUFFICIENT_BALANCE mit messageDe/En
  und Hinweis auf Billing-Pfad bei TOP_UP_SELF
2026-03-21 01:34:47 +01:00

133 lines
3.8 KiB
TypeScript

/**
* MainLayout
*
* Hauptlayout der Anwendung mit Sidebar und Content-Bereich.
* Enthält den FeatureProvider für das Multi-Tenant-System.
*/
import React, { useEffect, 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 { WorkspaceKeepAlive } from '../pages/views/workspace/WorkspaceKeepAlive';
import styles from './MainLayout.module.css';
const _WORKSPACE_ROUTE_RE = /\/mandates\/[^/]+\/workspace\/[^/]+\/dashboard/;
// =============================================================================
// INNER LAYOUT (mit Zugriff auf Store)
// =============================================================================
const MainLayoutInner: React.FC = () => {
const { loadFeatures, initialized, loading, error } = useFeatureStore();
const location = useLocation();
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
// 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 (
<div className={styles.mainLayout}>
{isMobileSidebarOpen && (
<button
className={styles.mobileBackdrop}
onClick={() => setIsMobileSidebarOpen(false)}
aria-label="Navigation schliessen"
/>
)}
{/* Sidebar */}
<aside className={`${styles.sidebar} ${isMobileSidebarOpen ? styles.sidebarOpen : ''}`}>
<div className={styles.logoContainer}>
<img
src="/logos/poweron-logo.png"
alt="PowerOn"
className={styles.logoImage}
/>
</div>
<nav className={styles.navigation}>
{loading && (
<div className={styles.loadingNav}>
Lade Navigation...
</div>
)}
{error && (
<div className={styles.errorNav}>
Fehler: {error}
</div>
)}
{initialized && !loading && (
<MandateNavigation />
)}
</nav>
{/* User-Bereich am unteren Rand */}
<UserSection />
</aside>
{/* Content */}
<main className={styles.content}>
<div className={styles.mobileTopBar}>
<button
className={styles.mobileMenuButton}
onClick={() => setIsMobileSidebarOpen(true)}
aria-label="Navigation oeffnen"
>
</button>
<img
src="/logos/poweron-logo.png"
alt="PowerOn"
className={styles.mobileLogo}
/>
</div>
<WorkspaceKeepAlive isVisible={_WORKSPACE_ROUTE_RE.test(location.pathname)} />
<div
className={styles.outletShell}
style={{ display: _WORKSPACE_ROUTE_RE.test(location.pathname) ? 'none' : undefined }}
>
<Outlet />
</div>
</main>
</div>
);
};
// =============================================================================
// MAIN LAYOUT (mit Provider)
// =============================================================================
export const MainLayout: React.FC = () => {
return (
<FeatureProvider>
<MainLayoutInner />
</FeatureProvider>
);
};
export default MainLayout;