diff --git a/src/App.tsx b/src/App.tsx index 00018db..fec828b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -30,15 +30,17 @@ import { ProtectedRoute } from './providers/auth/ProtectedRoute'; import { LanguageProvider } from './providers/language/LanguageContext'; import { ToastProvider } from './contexts/ToastContext'; import { WorkflowSelectionProvider } from './contexts/WorkflowSelectionContext'; +import { FileProvider } from './contexts/FileContext'; import { MainLayout } from './layouts/MainLayout'; import { FeatureLayout } from './layouts/FeatureLayout'; import { DashboardPage } from './pages/Dashboard'; import { SettingsPage } from './pages/Settings'; import { GDPRPage } from './pages/GDPR'; import { FeatureViewPage } from './pages/FeatureView'; -import { AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolesPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AccessManagementHub } from './pages/admin'; +import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AdminAutomationEventsPage } from './pages/admin'; import { PlaygroundPage, WorkflowsPage, AutomationsPage } from './pages/workflows'; import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata'; +import { BillingDataView, BillingAdmin } from './pages/billing'; function App() { // Load saved theme preference and set app name on app mount useEffect(() => { @@ -83,7 +85,9 @@ function App() { {/* ================================================== */} + + }> {/* Dashboard (Root) */} @@ -111,6 +115,14 @@ function App() { } /> + {/* ============================================== */} + {/* BILLING ROUTES */} + {/* ============================================== */} + + } /> + } /> + + {/* Legacy top-level routes – redirect to dashboard (migrated to feature-instance routes) */} } /> } /> @@ -160,6 +172,7 @@ function App() { {/* ADMIN ROUTES (nur SysAdmin) */} {/* ============================================== */} + } /> } /> } /> } /> @@ -171,6 +184,8 @@ function App() { } /> } /> } /> + } /> + } /> diff --git a/src/components/Navigation/MandateNavigation.tsx b/src/components/Navigation/MandateNavigation.tsx index 7804c17..dc9ffb5 100644 --- a/src/components/Navigation/MandateNavigation.tsx +++ b/src/components/Navigation/MandateNavigation.tsx @@ -8,26 +8,24 @@ * Backend liefert Blocks-Struktur mit Static und Dynamic Blocks. * UI mappt uiComponent zu Icons via pageRegistry. * - * Struktur (gemäss Navigation-API-Konzept): - * - SYSTEM (static block, order: 10) - * - MEINE FEATURES (dynamic block, order: 15) - * - Mandant 1 - * - Feature A - * - Instanz 1 (mit Views) - * - WORKFLOWS (static block, order: 20) - * - BASISDATEN (static block, order: 30) - * - MIGRATE TO FEATURES (static block, order: 40) - * - ADMINISTRATION (static block, order: 200) + * 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 { - StaticBlock, DynamicBlock, NavigationItem, NavigationMandate, - MandateFeature, FeatureInstance, FeatureView } from '../../hooks/useNavigation'; @@ -53,13 +51,20 @@ function navigationItemToTreeNode(item: NavigationItem): TreeNodeItem { } /** - * Convert a StaticBlock to TreeItem (section) + * Convert a list of NavigationItems into a collapsible TreeNodeItem container. + * Used for grouping static items under "Meine Sicht" and "Administration". */ -function staticBlockToTreeItem(block: StaticBlock): TreeItem { +function _staticItemsToTreeNode( + id: string, + label: string, + items: NavigationItem[], + defaultExpanded: boolean = true, +): TreeNodeItem { return { - type: 'section', - title: block.title, - children: block.items.map(navigationItemToTreeNode), + id, + label, + children: items.map(navigationItemToTreeNode), + defaultExpanded, }; } @@ -75,59 +80,52 @@ function featureViewToTreeNode(view: FeatureView): TreeNodeItem { } /** - * Convert a FeatureInstance to TreeNodeItem + * 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): TreeNodeItem { +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: instance.views.map(featureViewToTreeNode), - defaultExpanded: false, - }; -} - -/** - * Convert a MandateFeature to TreeNodeItem - */ -function mandateFeatureToTreeNode(feature: MandateFeature): TreeNodeItem | null { - if (feature.instances.length === 0) { - return null; - } - - return { - id: feature.uiComponent, - label: feature.uiLabel, - icon: getPageIcon(feature.uiComponent), - badge: feature.instances.length, - path: feature.instances.length > 0 && feature.instances[0].views.length > 0 - ? feature.instances[0].views[0].uiPath - : undefined, - children: feature.instances.map(featureInstanceToTreeNode), + 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; } - const children = mandate.features - .map(mandateFeatureToTreeNode) - .filter((node): node is TreeNodeItem => node !== 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 (children.length === 0) { + if (instanceNodes.length === 0) { return null; } return { id: mandate.id, label: mandate.uiLabel, - children, + children: instanceNodes, defaultExpanded: true, }; } @@ -174,40 +172,49 @@ export const MandateNavigation: React.FC = () => { 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[] = []; - - // Process blocks in order (already sorted by backend) + + // Collect static items by category + const meineSichtItems: NavigationItem[] = []; + let adminItems: NavigationItem[] = []; + for (const block of blocks) { if (block.type === 'static') { - // Static block: system, workflows, basedata, migrate, admin - if (block.items.length > 0) { - // Add separator before admin block - if (block.id === 'admin') { - items.push({ type: 'separator' }); - } - items.push(staticBlockToTreeItem(block)); + if (block.id === 'admin') { + adminItems = [...block.items]; + } else if (block.items.length > 0) { + meineSichtItems.push(...block.items); } - } else if (block.type === 'dynamic') { - // Dynamic block: features/mandates - // Add separator before dynamic block - items.push({ type: 'separator' }); - - const mandateNodes = dynamicBlockToTreeNodes(block); - if (mandateNodes.length > 0) { - items.push(...mandateNodes); - } - - // Add separator after dynamic block (before next static blocks) - items.push({ type: 'separator' }); } } - - // Remove trailing separator if present - while (items.length > 0 && (items[items.length - 1] as TreeItem & { type?: string }).type === 'separator') { - items.pop(); + + // "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]);