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]);