frontend_nyla/src/hooks/useNavigation.ts
2026-02-23 17:13:31 +01:00

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;