240 lines
6.7 KiB
TypeScript
240 lines
6.7 KiB
TypeScript
/**
|
|
* 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.
|
|
*
|
|
* 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)
|
|
*/
|
|
|
|
import React, { useMemo } from 'react';
|
|
import { useNavigation } from '../../hooks/useNavigation';
|
|
import type {
|
|
StaticBlock,
|
|
DynamicBlock,
|
|
NavigationItem,
|
|
NavigationMandate,
|
|
MandateFeature,
|
|
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 StaticBlock to TreeItem (section)
|
|
*/
|
|
function staticBlockToTreeItem(block: StaticBlock): TreeItem {
|
|
return {
|
|
type: 'section',
|
|
title: block.title,
|
|
children: block.items.map(navigationItemToTreeNode),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
function featureInstanceToTreeNode(instance: FeatureInstance): TreeNodeItem {
|
|
return {
|
|
id: instance.id,
|
|
label: instance.uiLabel,
|
|
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),
|
|
defaultExpanded: false,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Convert a NavigationMandate to TreeNodeItem
|
|
*/
|
|
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);
|
|
|
|
if (children.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
id: mandate.id,
|
|
label: mandate.uiLabel,
|
|
children,
|
|
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 = () => (
|
|
<div className={styles.loadingState}>
|
|
<FaSpinner className={styles.spinner} />
|
|
<span>Navigation wird geladen...</span>
|
|
</div>
|
|
);
|
|
|
|
// =============================================================================
|
|
// EMPTY STATE
|
|
// =============================================================================
|
|
|
|
const EmptyState: React.FC = () => (
|
|
<div className={styles.emptyState}>
|
|
<p>Keine Feature-Instanzen verfügbar.</p>
|
|
<p className={styles.emptyHint}>
|
|
Kontaktiere einen Administrator, um Zugriff zu erhalten.
|
|
</p>
|
|
</div>
|
|
);
|
|
|
|
// =============================================================================
|
|
// 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
|
|
const navigationItems: TreeItem[] = useMemo(() => {
|
|
const items: TreeItem[] = [];
|
|
|
|
// Process blocks in order (already sorted by backend)
|
|
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));
|
|
}
|
|
} 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();
|
|
}
|
|
|
|
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 (
|
|
<div className={styles.navigation}>
|
|
<LoadingState />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={styles.navigation}>
|
|
{hasNavigation ? (
|
|
<TreeNavigation
|
|
items={navigationItems}
|
|
autoExpandActive={true}
|
|
/>
|
|
) : (
|
|
<EmptyState />
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default MandateNavigation;
|