193 lines
5 KiB
TypeScript
193 lines
5 KiB
TypeScript
/**
|
|
* useNavigation Hook
|
|
*
|
|
* Fetches the navigation structure from the new Navigation API.
|
|
* The backend provides a blocks-based structure with static and dynamic blocks.
|
|
*
|
|
* API: GET /api/navigation?language=de
|
|
*
|
|
* Response structure (gemäss Navigation-API-Konzept):
|
|
* {
|
|
* "language": "de",
|
|
* "blocks": [
|
|
* { "type": "static", "id": "system", "title": "SYSTEM", "order": 10, "items": [...] },
|
|
* { "type": "dynamic", "id": "features", "title": "MEINE FEATURES", "order": 15, "mandates": [...] },
|
|
* ...
|
|
* ]
|
|
* }
|
|
*/
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import api from '../api';
|
|
|
|
// =============================================================================
|
|
// TYPES - New Navigation API Structure
|
|
// =============================================================================
|
|
|
|
/** Static block item (system, admin pages) */
|
|
export interface NavigationItem {
|
|
uiComponent: string;
|
|
uiLabel: string;
|
|
uiPath: string;
|
|
order: number;
|
|
objectKey: string;
|
|
}
|
|
|
|
/** Subgroup within a static block (for nested navigation) */
|
|
export interface NavSubgroup {
|
|
id: string;
|
|
title: string;
|
|
order: number;
|
|
items: NavigationItem[];
|
|
}
|
|
|
|
/** Static navigation block */
|
|
export interface StaticBlock {
|
|
type: 'static';
|
|
id: string;
|
|
title: string;
|
|
order: number;
|
|
items: NavigationItem[];
|
|
subgroups?: NavSubgroup[];
|
|
}
|
|
|
|
/** View within a feature instance */
|
|
export interface FeatureView {
|
|
uiComponent: string;
|
|
uiLabel: string;
|
|
uiPath: string;
|
|
order: number;
|
|
objectKey: string;
|
|
}
|
|
|
|
/** Feature instance within a mandate */
|
|
export interface FeatureInstance {
|
|
id: string;
|
|
uiLabel: string;
|
|
order: number;
|
|
views: FeatureView[];
|
|
}
|
|
|
|
/** Feature within a mandate */
|
|
export interface MandateFeature {
|
|
uiComponent: string;
|
|
uiLabel: string;
|
|
order: number;
|
|
instances: FeatureInstance[];
|
|
}
|
|
|
|
/** Mandate in the dynamic block */
|
|
export interface NavigationMandate {
|
|
id: string;
|
|
uiLabel: string;
|
|
order: number;
|
|
features: MandateFeature[];
|
|
}
|
|
|
|
/** Dynamic navigation block (features) */
|
|
export interface DynamicBlock {
|
|
type: 'dynamic';
|
|
id: string;
|
|
title: string;
|
|
order: number;
|
|
mandates: NavigationMandate[];
|
|
}
|
|
|
|
/** Union type for all block types */
|
|
export type NavigationBlock = StaticBlock | DynamicBlock;
|
|
|
|
/** API Response structure */
|
|
export interface NavigationResponse {
|
|
language: string;
|
|
blocks: NavigationBlock[];
|
|
}
|
|
|
|
/** Hook return type */
|
|
interface UseNavigationReturn {
|
|
/** All navigation blocks from API */
|
|
blocks: NavigationBlock[];
|
|
/** Static blocks only (for convenience) */
|
|
staticBlocks: StaticBlock[];
|
|
/** Dynamic block (features) if present */
|
|
dynamicBlock: DynamicBlock | null;
|
|
/** Loading state */
|
|
loading: boolean;
|
|
/** Error message if any */
|
|
error: string | null;
|
|
/** Refresh navigation data */
|
|
refresh: () => Promise<void>;
|
|
}
|
|
|
|
// =============================================================================
|
|
// HELPER FUNCTIONS
|
|
// =============================================================================
|
|
|
|
function isStaticBlock(block: NavigationBlock): block is StaticBlock {
|
|
return block.type === 'static';
|
|
}
|
|
|
|
function isDynamicBlock(block: NavigationBlock): block is DynamicBlock {
|
|
return block.type === 'dynamic';
|
|
}
|
|
|
|
// =============================================================================
|
|
// HOOK
|
|
// =============================================================================
|
|
|
|
export function useNavigation(language: string = 'de'): UseNavigationReturn {
|
|
const [blocks, setBlocks] = useState<NavigationBlock[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const fetchNavigation = useCallback(async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
// New API endpoint: /api/navigation (without /system prefix)
|
|
const response = await api.get<NavigationResponse>(
|
|
`/api/navigation?language=${language}`
|
|
);
|
|
|
|
// Blocks are already sorted by order from backend
|
|
setBlocks(response.data.blocks || []);
|
|
|
|
} catch (err: unknown) {
|
|
const errorMsg = err instanceof Error
|
|
? err.message
|
|
: (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail
|
|
|| 'Fehler beim Laden der Navigation';
|
|
setError(errorMsg);
|
|
setBlocks([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [language]);
|
|
|
|
useEffect(() => {
|
|
fetchNavigation();
|
|
}, [fetchNavigation]);
|
|
|
|
useEffect(() => {
|
|
const onFeaturesChanged = () => {
|
|
fetchNavigation();
|
|
};
|
|
window.addEventListener('features-changed', onFeaturesChanged);
|
|
return () => window.removeEventListener('features-changed', onFeaturesChanged);
|
|
}, [fetchNavigation]);
|
|
|
|
// Derive static and dynamic blocks
|
|
const staticBlocks = blocks.filter(isStaticBlock);
|
|
const dynamicBlock = blocks.find(isDynamicBlock) || null;
|
|
|
|
return {
|
|
blocks,
|
|
staticBlocks,
|
|
dynamicBlock,
|
|
loading,
|
|
error,
|
|
refresh: fetchNavigation,
|
|
};
|
|
}
|
|
|
|
export default useNavigation;
|