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 { LanguageProvider } from './providers/language/LanguageContext';
import { ToastProvider } from './contexts/ToastContext'; import { ToastProvider } from './contexts/ToastContext';
import { WorkflowSelectionProvider } from './contexts/WorkflowSelectionContext'; import { WorkflowSelectionProvider } from './contexts/WorkflowSelectionContext';
import { FileProvider } from './contexts/FileContext';
import { MainLayout } from './layouts/MainLayout'; import { MainLayout } from './layouts/MainLayout';
import { FeatureLayout } from './layouts/FeatureLayout'; import { FeatureLayout } from './layouts/FeatureLayout';
import { DashboardPage } from './pages/Dashboard'; import { DashboardPage } from './pages/Dashboard';
import { SettingsPage } from './pages/Settings'; import { SettingsPage } from './pages/Settings';
import { GDPRPage } from './pages/GDPR'; import { GDPRPage } from './pages/GDPR';
import { FeatureViewPage } from './pages/FeatureView'; 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 { PlaygroundPage, WorkflowsPage, AutomationsPage } from './pages/workflows';
import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata'; import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata';
import { BillingDataView, BillingAdmin } from './pages/billing';
function App() { function App() {
// Load saved theme preference and set app name on app mount // Load saved theme preference and set app name on app mount
useEffect(() => { useEffect(() => {
@ -83,7 +85,9 @@ function App() {
{/* ================================================== */} {/* ================================================== */}
<Route path="/" element={ <Route path="/" element={
<ProtectedRoute> <ProtectedRoute>
<FileProvider>
<MainLayout /> <MainLayout />
</FileProvider>
</ProtectedRoute> </ProtectedRoute>
}> }>
{/* Dashboard (Root) */} {/* Dashboard (Root) */}
@ -111,6 +115,14 @@ function App() {
<Route path="connections" element={<ConnectionsPage />} /> <Route path="connections" element={<ConnectionsPage />} />
</Route> </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) */} {/* Legacy top-level routes redirect to dashboard (migrated to feature-instance routes) */}
<Route path="chatbot" element={<Navigate to="/" replace />} /> <Route path="chatbot" element={<Navigate to="/" replace />} />
<Route path="pek" element={<Navigate to="/" replace />} /> <Route path="pek" element={<Navigate to="/" replace />} />
@ -160,6 +172,7 @@ function App() {
{/* ADMIN ROUTES (nur SysAdmin) */} {/* ADMIN ROUTES (nur SysAdmin) */}
{/* ============================================== */} {/* ============================================== */}
<Route path="admin"> <Route path="admin">
<Route index element={<Navigate to="/admin/access" replace />} />
<Route path="mandates" element={<AdminMandatesPage />} /> <Route path="mandates" element={<AdminMandatesPage />} />
<Route path="users" element={<AdminUsersPage />} /> <Route path="users" element={<AdminUsersPage />} />
<Route path="user-mandates" element={<AdminUserMandatesPage />} /> <Route path="user-mandates" element={<AdminUserMandatesPage />} />
@ -171,6 +184,8 @@ function App() {
<Route path="mandate-roles" element={<AdminMandateRolesPage />} /> <Route path="mandate-roles" element={<AdminMandateRolesPage />} />
<Route path="mandate-role-permissions" element={<AdminMandateRolePermissionsPage />} /> <Route path="mandate-role-permissions" element={<AdminMandateRolePermissionsPage />} />
<Route path="user-access-overview" element={<AdminUserAccessOverviewPage />} /> <Route path="user-access-overview" element={<AdminUserAccessOverviewPage />} />
<Route path="billing" element={<BillingAdmin />} />
<Route path="automation-events" element={<AdminAutomationEventsPage />} />
</Route> </Route>
</Route> </Route>

View file

@ -8,26 +8,24 @@
* Backend liefert Blocks-Struktur mit Static und Dynamic Blocks. * Backend liefert Blocks-Struktur mit Static und Dynamic Blocks.
* UI mappt uiComponent zu Icons via pageRegistry. * UI mappt uiComponent zu Icons via pageRegistry.
* *
* Struktur (gemäss Navigation-API-Konzept): * TREE STRUCTURE (alles collapsible):
* - SYSTEM (static block, order: 10) * Meine Sicht
* - MEINE FEATURES (dynamic block, order: 15) * - Übersicht, Einstellungen, Prompts, Dateien, Verbindungen, Billing
* - Mandant 1 *
* - Feature A * Mandant 1
* - Instanz 1 (mit Views) * - 🎯 Instanz 1 (Feature-Icon + Instanz-Name)
* - WORKFLOWS (static block, order: 20) * - 💼 Instanz 2 (Feature-Icon + Instanz-Name)
* - BASISDATEN (static block, order: 30) *
* - MIGRATE TO FEATURES (static block, order: 40) * Administration
* - ADMINISTRATION (static block, order: 200) * - Users, Mandates, Roles, ...
*/ */
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { useNavigation } from '../../hooks/useNavigation'; import { useNavigation } from '../../hooks/useNavigation';
import type { import type {
StaticBlock,
DynamicBlock, DynamicBlock,
NavigationItem, NavigationItem,
NavigationMandate, NavigationMandate,
MandateFeature,
FeatureInstance, FeatureInstance,
FeatureView FeatureView
} from '../../hooks/useNavigation'; } 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 { return {
type: 'section', id,
title: block.title, label,
children: block.items.map(navigationItemToTreeNode), 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 { return {
id: instance.id, id: instance.id,
label: instance.uiLabel, label: instance.uiLabel,
icon: getPageIcon(featureUiComponent), // Use feature icon for instance
path: instance.views.length > 0 ? instance.views[0].uiPath : undefined, path: instance.views.length > 0 ? instance.views[0].uiPath : undefined,
children: instance.views.map(featureViewToTreeNode), children,
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),
defaultExpanded: false, defaultExpanded: false,
}; };
} }
/** /**
* Convert a NavigationMandate to TreeNodeItem * 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 { function navigationMandateToTreeNode(mandate: NavigationMandate): TreeNodeItem | null {
if (mandate.features.length === 0) { if (mandate.features.length === 0) {
return null; return null;
} }
const children = mandate.features // Flatten: collect all instances from all features directly under mandate
.map(mandateFeatureToTreeNode) const instanceNodes: TreeNodeItem[] = [];
.filter((node): node is TreeNodeItem => node !== null); 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 null;
} }
return { return {
id: mandate.id, id: mandate.id,
label: mandate.uiLabel, label: mandate.uiLabel,
children, children: instanceNodes,
defaultExpanded: true, defaultExpanded: true,
}; };
} }
@ -174,40 +172,49 @@ export const MandateNavigation: React.FC = () => {
const { blocks, loading } = useNavigation('de'); const { blocks, loading } = useNavigation('de');
// Build navigation items from blocks // 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 navigationItems: TreeItem[] = useMemo(() => {
const items: TreeItem[] = []; 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) { for (const block of blocks) {
if (block.type === 'static') { if (block.type === 'static') {
// Static block: system, workflows, basedata, migrate, admin if (block.id === 'admin') {
if (block.items.length > 0) { adminItems = [...block.items];
// Add separator before admin block } else if (block.items.length > 0) {
if (block.id === 'admin') { meineSichtItems.push(...block.items);
items.push({ type: 'separator' });
}
items.push(staticBlockToTreeItem(block));
} }
} 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 // "Meine Sicht" - collapsible container for user-facing pages
while (items.length > 0 && (items[items.length - 1] as TreeItem & { type?: string }).type === 'separator') { if (meineSichtItems.length > 0) {
items.pop(); 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; return items;
}, [blocks]); }, [blocks]);