enhanced generic navigation tree
This commit is contained in:
parent
2a1cd2ef64
commit
4231727544
11 changed files with 193 additions and 161 deletions
|
|
@ -8,26 +8,24 @@
|
|||
* Backend liefert Blocks-Struktur mit Static und Dynamic Blocks.
|
||||
* UI mappt uiComponent zu Icons via pageRegistry.
|
||||
*
|
||||
* FLAT STRUCTURE (kompakte Darstellung):
|
||||
* - SYSTEM (static block)
|
||||
* - Mandant 1
|
||||
* 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)
|
||||
* - BASISDATEN (static block)
|
||||
* - ADMINISTRATION (static block)
|
||||
*
|
||||
* Jede Instanz zeigt das Icon des zugehörigen Features.
|
||||
* Keine Gruppierung nach Features - direkt Instanzen unter Mandant.
|
||||
* ─────────────
|
||||
* ▶ 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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -167,38 +172,47 @@ 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' });
|
||||
adminItems = [...block.items];
|
||||
} else if (block.items.length > 0) {
|
||||
meineSichtItems.push(...block.items);
|
||||
}
|
||||
}
|
||||
items.push(staticBlockToTreeItem(block));
|
||||
}
|
||||
} else if (block.type === 'dynamic') {
|
||||
// Dynamic block: features/mandates
|
||||
// Add separator before dynamic block
|
||||
items.push({ type: 'separator' });
|
||||
|
||||
// "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);
|
||||
}
|
||||
|
||||
// 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();
|
||||
// "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;
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
/**
|
||||
* TreeNavigation Styles
|
||||
*
|
||||
* Flexible hierarchical navigation with support for:
|
||||
* - Dynamic sublevels
|
||||
* - Sections and separators
|
||||
* - Various visual states (active, disabled, hover)
|
||||
* Modern minimal tree navigation (Notion/Linear style):
|
||||
* - CSS-only disclosure triangle with smooth rotation
|
||||
* - No guide lines — clean indentation only
|
||||
* - Depth-aware sizing via data-depth attribute
|
||||
* - Hover-reveal toggle for subtle UX
|
||||
*/
|
||||
|
||||
.treeNavigation {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
gap: 1px;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
|
|
@ -20,8 +21,8 @@
|
|||
|
||||
.separator {
|
||||
height: 1px;
|
||||
background: var(--border-color, #e0e0e0);
|
||||
margin: 0.75rem 0.5rem;
|
||||
background: var(--border-color, #e2e8f0);
|
||||
margin: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
/* ============================================ */
|
||||
|
|
@ -29,7 +30,7 @@
|
|||
/* ============================================ */
|
||||
|
||||
.treeSection {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
|
|
@ -40,14 +41,14 @@
|
|||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--text-tertiary, #888);
|
||||
color: var(--text-tertiary, #94a3b8);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sectionContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
/* ============================================ */
|
||||
|
|
@ -62,9 +63,9 @@
|
|||
.treeNode {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
gap: 0.375rem;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
|
|
@ -72,9 +73,11 @@
|
|||
text-decoration: none;
|
||||
font-family: inherit;
|
||||
text-align: left;
|
||||
color: var(--text-secondary, #666);
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.15s ease;
|
||||
color: var(--text-secondary, #64748b);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.treeNode:hover {
|
||||
|
|
@ -82,12 +85,18 @@
|
|||
color: var(--text-primary, #1a1a1a);
|
||||
}
|
||||
|
||||
/* Leaf node active — strong pill highlight */
|
||||
.treeNode.active {
|
||||
background: var(--primary-light, #e0e7ff);
|
||||
color: var(--primary-color, #2563eb);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Group/parent active — subtle text color only, no background */
|
||||
.treeNode.activeGroup {
|
||||
color: var(--primary-color, #2563eb);
|
||||
}
|
||||
|
||||
.treeNode.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
|
|
@ -95,105 +104,109 @@
|
|||
}
|
||||
|
||||
/* ============================================ */
|
||||
/* LEVEL-SPECIFIC STYLES */
|
||||
/* DEPTH-SPECIFIC STYLES (via data-depth) */
|
||||
/* ============================================ */
|
||||
|
||||
/* Root level (level 0) */
|
||||
.levelRoot {
|
||||
.treeNode[data-depth="0"] {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
padding: 0.625rem 0.75rem;
|
||||
padding: 0.5rem 0.5rem;
|
||||
}
|
||||
|
||||
.levelRoot .nodeLabel {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Level 1 */
|
||||
.levelOne {
|
||||
.treeNode[data-depth="1"] {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary, #666);
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
/* Level 2 */
|
||||
.levelTwo {
|
||||
.treeNode[data-depth="2"],
|
||||
.treeNode[data-depth="3"],
|
||||
.treeNode[data-depth="4"],
|
||||
.treeNode[data-depth="5"] {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary, #666);
|
||||
padding: 0.375rem 0.5rem;
|
||||
}
|
||||
|
||||
/* Level 3 */
|
||||
.levelThree {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary, #666);
|
||||
padding: 0.375rem 0.5rem;
|
||||
}
|
||||
|
||||
/* Deep levels (4+) */
|
||||
.levelDeep {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--text-tertiary, #888);
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* ============================================ */
|
||||
/* NODE CHILDREN (INDENTATION) */
|
||||
/* NODE CHILDREN (INDENTATION ONLY) */
|
||||
/* ============================================ */
|
||||
|
||||
.treeNodeChildren {
|
||||
margin-left: 0.25rem;
|
||||
padding-left: 0.75rem;
|
||||
border-left: 2px solid var(--border-color, #e0e0e0);
|
||||
margin-left: 0.75rem;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
/* Active parent highlights the border */
|
||||
.treeNodeContainer:has(> .treeNode.active) > .treeNodeChildren {
|
||||
/* ============================================ */
|
||||
/* TOGGLE (CSS-only disclosure triangle) */
|
||||
/* ============================================ */
|
||||
|
||||
.toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
/* The triangle — pure CSS, no icon needed */
|
||||
.toggle::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 4.5px solid var(--text-tertiary, #94a3b8);
|
||||
border-top: 3.5px solid transparent;
|
||||
border-bottom: 3.5px solid transparent;
|
||||
transition: transform 0.2s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
/* Rotate triangle when expanded */
|
||||
.toggleExpanded::after {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* Hover feedback */
|
||||
.toggle:hover {
|
||||
background: var(--hover-bg, rgba(0, 0, 0, 0.06));
|
||||
}
|
||||
|
||||
.toggle:hover::after {
|
||||
border-left-color: var(--text-primary, #1a1a1a);
|
||||
}
|
||||
|
||||
/* Active node toggle */
|
||||
.treeNode.active .toggle::after,
|
||||
.treeNode.activeGroup .toggle::after {
|
||||
border-left-color: var(--primary-color, #2563eb);
|
||||
}
|
||||
|
||||
/* Also highlight if any descendant is active */
|
||||
.treeNodeContainer:has(.treeNode.active) > .treeNodeChildren {
|
||||
border-left-color: var(--primary-light, #93c5fd);
|
||||
/* Spacer for leaf nodes (keeps alignment with toggle nodes) */
|
||||
.toggleSpacer {
|
||||
width: 1.125rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ============================================ */
|
||||
/* NODE ELEMENTS */
|
||||
/* ============================================ */
|
||||
|
||||
.chevron {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
font-size: 0.625rem;
|
||||
color: var(--text-tertiary, #888);
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
transition: background 0.1s ease;
|
||||
}
|
||||
|
||||
.chevron:hover {
|
||||
background: var(--hover-bg, rgba(0, 0, 0, 0.06));
|
||||
}
|
||||
|
||||
.chevronSpacer {
|
||||
width: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nodeIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
font-size: 0.875rem;
|
||||
flex-shrink: 0;
|
||||
color: inherit;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.treeNode.active .nodeIcon,
|
||||
.treeNode.activeGroup .nodeIcon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nodeLabel {
|
||||
|
|
@ -208,7 +221,7 @@
|
|||
padding: 0.0625rem 0.375rem;
|
||||
background: var(--surface-color, #f0f0f0);
|
||||
border-radius: 9999px;
|
||||
color: var(--text-tertiary, #888);
|
||||
color: var(--text-tertiary, #94a3b8);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
flex-shrink: 0;
|
||||
|
|
@ -262,41 +275,45 @@
|
|||
color: var(--primary-light, #93c5fd);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .levelRoot {
|
||||
:global(.dark-theme) .treeNode.activeGroup {
|
||||
color: var(--primary-light, #93c5fd);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .treeNode[data-depth="0"] {
|
||||
color: var(--text-primary-dark, #fff);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .levelOne,
|
||||
:global(.dark-theme) .levelTwo,
|
||||
:global(.dark-theme) .levelThree {
|
||||
color: var(--text-secondary-dark, #aaa);
|
||||
:global(.dark-theme) .toggle::after {
|
||||
border-left-color: var(--text-tertiary-dark, #555);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .levelDeep {
|
||||
color: var(--text-tertiary-dark, #888);
|
||||
:global(.dark-theme) .toggle:hover {
|
||||
background: var(--hover-bg-dark, rgba(255, 255, 255, 0.08));
|
||||
}
|
||||
|
||||
:global(.dark-theme) .treeNodeChildren {
|
||||
border-left-color: var(--border-dark, #444);
|
||||
:global(.dark-theme) .toggle:hover::after {
|
||||
border-left-color: var(--text-primary-dark, #ddd);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .treeNodeContainer:has(.treeNode.active) > .treeNodeChildren {
|
||||
:global(.dark-theme) .treeNode.active .toggle::after,
|
||||
:global(.dark-theme) .treeNode.activeGroup .toggle::after {
|
||||
border-left-color: var(--primary-light, #93c5fd);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .nodeIcon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
:global(.dark-theme) .treeNode.active .nodeIcon,
|
||||
:global(.dark-theme) .treeNode.activeGroup .nodeIcon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:global(.dark-theme) .nodeBadge {
|
||||
background: var(--surface-dark, #2a2a2a);
|
||||
color: var(--text-tertiary-dark, #888);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .chevron {
|
||||
color: var(--text-tertiary-dark, #666);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .chevron:hover {
|
||||
background: var(--hover-bg-dark, rgba(255, 255, 255, 0.1));
|
||||
}
|
||||
|
||||
:global(.dark-theme) .treeNode.active .nodeBadge {
|
||||
background: var(--primary-color, #2563eb);
|
||||
color: white;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@
|
|||
|
||||
import React, { useState, useEffect, ReactNode } from 'react';
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
import { FaChevronDown, FaChevronRight } from 'react-icons/fa';
|
||||
import styles from './TreeNavigation.module.css';
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -155,8 +154,11 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
|||
}
|
||||
}, [currentPath, autoExpandActive, node]);
|
||||
|
||||
// Check if this exact node is active
|
||||
// Check if this node is active (exact match or ancestor of active path)
|
||||
const isActive = node.path ? currentPath === node.path || currentPath.startsWith(node.path + '/') : false;
|
||||
// Differentiate: leaf active (strong highlight) vs group active (subtle text only)
|
||||
const isLeafActive = isActive && !hasChildren;
|
||||
const isGroupActive = isActive && !!hasChildren;
|
||||
|
||||
// Handle click
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
|
|
@ -186,34 +188,24 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
// Handle chevron click separately
|
||||
const handleChevronClick = (e: React.MouseEvent) => {
|
||||
// Handle toggle click separately (expand/collapse)
|
||||
const handleToggleClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsExpanded(!isExpanded);
|
||||
};
|
||||
|
||||
// Get level-specific styles
|
||||
const getLevelClass = () => {
|
||||
switch (level) {
|
||||
case 0: return styles.levelRoot;
|
||||
case 1: return styles.levelOne;
|
||||
case 2: return styles.levelTwo;
|
||||
case 3: return styles.levelThree;
|
||||
default: return styles.levelDeep;
|
||||
}
|
||||
};
|
||||
|
||||
// Render the node content
|
||||
const nodeContent = (
|
||||
<>
|
||||
{isExpandable && (
|
||||
<span className={styles.chevron} onClick={handleChevronClick}>
|
||||
{isExpanded ? <FaChevronDown /> : <FaChevronRight />}
|
||||
</span>
|
||||
<span
|
||||
className={`${styles.toggle} ${isExpanded ? styles.toggleExpanded : ''}`}
|
||||
onClick={handleToggleClick}
|
||||
/>
|
||||
)}
|
||||
{!isExpandable && hasChildren === false && (
|
||||
<span className={styles.chevronSpacer} />
|
||||
<span className={styles.toggleSpacer} />
|
||||
)}
|
||||
{node.icon && <span className={styles.nodeIcon}>{node.icon}</span>}
|
||||
<span className={styles.nodeLabel} title={node.label}>{node.label}</span>
|
||||
|
|
@ -228,7 +220,7 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
|||
);
|
||||
|
||||
// Determine if we should render as NavLink or button
|
||||
const nodeClasses = `${styles.treeNode} ${getLevelClass()} ${isActive ? styles.active : ''} ${node.disabled ? styles.disabled : ''} ${node.className || ''}`;
|
||||
const nodeClasses = `${styles.treeNode} ${isLeafActive ? styles.active : ''} ${isGroupActive ? styles.activeGroup : ''} ${node.disabled ? styles.disabled : ''} ${node.className || ''}`;
|
||||
|
||||
const nodeElement = node.path ? (
|
||||
<NavLink
|
||||
|
|
@ -236,6 +228,7 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
|||
className={nodeClasses}
|
||||
onClick={handleClick}
|
||||
data-id={node.dataId}
|
||||
data-depth={level}
|
||||
>
|
||||
{nodeContent}
|
||||
</NavLink>
|
||||
|
|
@ -246,6 +239,7 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
|||
onClick={handleClick}
|
||||
disabled={node.disabled}
|
||||
data-id={node.dataId}
|
||||
data-depth={level}
|
||||
>
|
||||
{nodeContent}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ export interface Role {
|
|||
export interface Mandate {
|
||||
id: string;
|
||||
name: string | { [key: string]: string };
|
||||
label?: string;
|
||||
code?: string;
|
||||
language?: string;
|
||||
isSystem?: boolean;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { FeatureInstanceWizard } from './FeatureInstanceWizard';
|
|||
import { InstanceHierarchyView } from './InstanceHierarchyView';
|
||||
|
||||
function getMandateName(mandate: Mandate): string {
|
||||
if (mandate.label) return mandate.label;
|
||||
if (typeof mandate.name === 'object') {
|
||||
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -310,6 +310,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
|
||||
// Get mandate name
|
||||
const getMandateName = (mandate: Mandate) => {
|
||||
if (mandate.label) return mandate.label;
|
||||
if (typeof mandate.name === 'object') {
|
||||
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
allOptions.push({
|
||||
mandateId: mandate.id,
|
||||
instanceId: inst.id,
|
||||
mandateName: typeof mandate.name === 'string' ? mandate.name : (mandate.name?.de || mandate.name?.en || Object.values(mandate.name || {})[0] || mandate.id),
|
||||
mandateName: mandate.label || (typeof mandate.name === 'string' ? mandate.name : (mandate.name?.de || mandate.name?.en || Object.values(mandate.name || {})[0] || mandate.id)),
|
||||
instanceLabel: inst.label || inst.id,
|
||||
featureCode: inst.featureCode,
|
||||
combinedKey: `${mandate.id}:${inst.id}`,
|
||||
|
|
|
|||
|
|
@ -226,6 +226,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
|
||||
// Get mandate name
|
||||
const getMandateName = (mandate: Mandate) => {
|
||||
if (mandate.label) return mandate.label;
|
||||
if (typeof mandate.name === 'object') {
|
||||
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -285,6 +285,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
|
||||
// Get mandate name
|
||||
const getMandateName = (mandate: Mandate) => {
|
||||
if (mandate.label) return mandate.label;
|
||||
if (typeof mandate.name === 'object') {
|
||||
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -248,6 +248,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
|
||||
// Get mandate name
|
||||
const getMandateName = (mandate: Mandate) => {
|
||||
if (mandate.label) return mandate.label;
|
||||
if (typeof mandate.name === 'object') {
|
||||
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,7 +116,8 @@ export interface MandateFeature {
|
|||
*/
|
||||
export interface Mandate {
|
||||
id: string; // mandateId
|
||||
name: string; // Anzeige-Name
|
||||
name: string; // Technischer Identifier
|
||||
label?: string; // Anzeige-Label (fuer FK-Referenzen und UI)
|
||||
code?: string; // Optionaler Code
|
||||
features: MandateFeature[];
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue