/** * MandateNavigation * * Hierarchische Navigation für das Multi-Tenant-System. * Verwendet TreeNavigation für flexible Baumstruktur. * * Navigation wird vollständig vom Backend geladen (/api/navigation). * Backend liefert Blocks-Struktur mit Static und Dynamic Blocks. * UI mappt uiComponent zu Icons via pageRegistry. * * TREE STRUCTURE (alles collapsible): * ▼ Meine Sicht * - Übersicht, Einstellungen, Prompts, Dateien, Verbindungen, Billing * ───────────── * ▼ Mandant 1 * - 🎯 Instanz 1 (Feature-Icon + Instanz-Name) * - 💼 Instanz 2 (Feature-Icon + Instanz-Name) * ───────────── * ▶ Administration * - Users, Mandates, Roles, ... */ import React, { useMemo } from 'react'; import { useNavigation } from '../../hooks/useNavigation'; import type { DynamicBlock, NavigationItem, NavigationMandate, FeatureInstance, FeatureView } from '../../hooks/useNavigation'; import { getPageIcon } from '../../config/pageRegistry'; import { FaSpinner } from 'react-icons/fa'; import { TreeNavigation, type TreeItem, type TreeNodeItem } from './TreeNavigation'; import styles from './MandateNavigation.module.css'; // ============================================================================= // HELPER FUNCTIONS - Convert API blocks to TreeItems // ============================================================================= /** * Convert a NavigationItem (from static block) to TreeNodeItem */ function navigationItemToTreeNode(item: NavigationItem): TreeNodeItem { return { id: item.objectKey, label: item.uiLabel, icon: getPageIcon(item.uiComponent), path: item.uiPath, }; } /** * Convert a list of NavigationItems into a collapsible TreeNodeItem container. * Used for grouping static items under "Meine Sicht" and "Administration". */ function _staticItemsToTreeNode( id: string, label: string, items: NavigationItem[], defaultExpanded: boolean = true, ): TreeNodeItem { return { id, label, children: items.map(navigationItemToTreeNode), defaultExpanded, }; } /** * Convert a FeatureView to TreeNodeItem */ function featureViewToTreeNode(view: FeatureView): TreeNodeItem { return { id: view.objectKey, label: view.uiLabel, path: view.uiPath, }; } /** * Convert a FeatureInstance to TreeNodeItem (with feature icon) * Instance node gets path to first view so clicking the instance name navigates to dashboard. * Shows the feature icon next to the instance name for visual distinction. */ function featureInstanceToTreeNode(instance: FeatureInstance, featureUiComponent: string): TreeNodeItem { const children = instance.views.map(featureViewToTreeNode); return { id: instance.id, label: instance.uiLabel, icon: getPageIcon(featureUiComponent), // Use feature icon for instance path: instance.views.length > 0 ? instance.views[0].uiPath : undefined, children, defaultExpanded: false, }; } /** * Convert a NavigationMandate to TreeNodeItem * * FLAT STRUCTURE: Instances are listed directly under mandate (no feature grouping). * Each instance shows the feature's icon for visual distinction. * * Before: Mandate → Feature → Instance → Views * Now: Mandate → Instance (with feature icon) → Views */ function navigationMandateToTreeNode(mandate: NavigationMandate): TreeNodeItem | null { if (mandate.features.length === 0) { return null; } // Flatten: collect all instances from all features directly under mandate const instanceNodes: TreeNodeItem[] = []; for (const feature of mandate.features) { for (const instance of feature.instances) { instanceNodes.push(featureInstanceToTreeNode(instance, feature.uiComponent)); } } if (instanceNodes.length === 0) { return null; } return { id: mandate.id, label: mandate.uiLabel, children: instanceNodes, defaultExpanded: true, }; } /** * Convert a DynamicBlock to array of TreeNodeItems (mandate nodes) */ function dynamicBlockToTreeNodes(block: DynamicBlock): TreeNodeItem[] { return block.mandates .map(navigationMandateToTreeNode) .filter((node): node is TreeNodeItem => node !== null); } // ============================================================================= // LOADING STATE // ============================================================================= const LoadingState: React.FC = () => (
Navigation wird geladen...
); // ============================================================================= // EMPTY STATE // ============================================================================= const EmptyState: React.FC = () => (

Keine Feature-Instanzen verfügbar.

Kontaktiere einen Administrator, um Zugriff zu erhalten.

); // ============================================================================= // MAIN COMPONENT // ============================================================================= export const MandateNavigation: React.FC = () => { // Fetch navigation from new API (blocks structure, already filtered by permissions) const { blocks, loading } = useNavigation('de'); // Build navigation items from blocks // Groups static items into collapsible containers: // - "Meine Sicht": all non-admin static items (Übersicht, Einstellungen, Prompts, etc.) // - "Administration": all admin static items // - Dynamic block (mandates) renders between them const navigationItems: TreeItem[] = useMemo(() => { const items: TreeItem[] = []; // Collect static items by category const meineSichtItems: NavigationItem[] = []; let adminItems: NavigationItem[] = []; for (const block of blocks) { if (block.type === 'static') { if (block.id === 'admin') { adminItems = [...block.items]; } else if (block.items.length > 0) { meineSichtItems.push(...block.items); } } } // "Meine Sicht" - collapsible container for user-facing pages if (meineSichtItems.length > 0) { items.push(_staticItemsToTreeNode('meine-sicht', 'Meine Sicht', meineSichtItems, true)); } // Dynamic block: mandates with feature instances for (const block of blocks) { if (block.type === 'dynamic') { const mandateNodes = dynamicBlockToTreeNodes(block); if (mandateNodes.length > 0) { if (items.length > 0) items.push({ type: 'separator' }); items.push(...mandateNodes); } } } // "Administration" - collapsible container for admin pages if (adminItems.length > 0) { if (items.length > 0) items.push({ type: 'separator' }); items.push(_staticItemsToTreeNode('administration', 'Administration', adminItems, false)); } return items; }, [blocks]); // Check if user has any navigation (static or dynamic) const hasNavigation = blocks.length > 0; // Show loading state while navigation is being fetched if (loading) { return (
); } return (
{hasNavigation ? ( ) : ( )}
); }; export default MandateNavigation;