fix: restore navigation tree structure and missing routes after merge damage

Restores functionality lost during merge of feat/real-estate into int (00f2158, 13 Feb):

MandateNavigation.tsx:
- Restored 3-section collapsible navigation: Meine Sicht, dynamic mandates (flat), Administration
- Reverted to flat instance structure (Mandate > Instance with feature icon > Views)
- Original commit: 4231727 (10 Feb, enhanced generic navigation tree)

App.tsx:
- Restored FileProvider wrapper around MainLayout
- Restored Billing routes (/billing/transactions)
- Restored Admin routes: /admin/billing, /admin/automation-events
- Restored Admin index redirect (/admin -> /admin/access)
- Re-added missing imports: FileProvider, BillingDataView, BillingAdmin, AdminAutomationEventsPage

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ValueOn AG 2026-02-15 02:15:44 +01:00
parent 6272d21942
commit 30d23ac7df
2 changed files with 95 additions and 73 deletions

View file

@ -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() {
{/* ================================================== */}
<Route path="/" element={
<ProtectedRoute>
<FileProvider>
<MainLayout />
</FileProvider>
</ProtectedRoute>
}>
{/* Dashboard (Root) */}
@ -111,6 +115,14 @@ function App() {
<Route path="connections" element={<ConnectionsPage />} />
</Route>
{/* ============================================== */}
{/* BILLING ROUTES */}
{/* ============================================== */}
<Route path="billing">
<Route index element={<Navigate to="/billing/transactions" replace />} />
<Route path="transactions" element={<BillingDataView />} />
</Route>
{/* Legacy top-level routes redirect to dashboard (migrated to feature-instance routes) */}
<Route path="chatbot" element={<Navigate to="/" replace />} />
<Route path="pek" element={<Navigate to="/" replace />} />
@ -160,6 +172,7 @@ function App() {
{/* ADMIN ROUTES (nur SysAdmin) */}
{/* ============================================== */}
<Route path="admin">
<Route index element={<Navigate to="/admin/access" replace />} />
<Route path="mandates" element={<AdminMandatesPage />} />
<Route path="users" element={<AdminUsersPage />} />
<Route path="user-mandates" element={<AdminUserMandatesPage />} />
@ -171,6 +184,8 @@ function App() {
<Route path="mandate-roles" element={<AdminMandateRolesPage />} />
<Route path="mandate-role-permissions" element={<AdminMandateRolePermissionsPage />} />
<Route path="user-access-overview" element={<AdminUserAccessOverviewPage />} />
<Route path="billing" element={<BillingAdmin />} />
<Route path="automation-events" element={<AdminAutomationEventsPage />} />
</Route>
</Route>

View file

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