frontend_nyla/src/hooks/useNavigation.ts
2026-04-12 21:32:22 +02:00

184 lines
4.8 KiB
TypeScript

/**
* useNavigation Hook
*
* Fetches the navigation structure from the Navigation API.
* Static nav items and feature view labels are resolved server-side for the
* request language (resolveText). User-defined mandate/instance names are raw.
*
* API: GET /api/navigation
*/
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;
/** Feature type code, e.g. trustee, workspace (for display: Label (code)) */
featureCode?: string;
order: number;
views: FeatureView[];
isAdmin?: boolean;
}
/** 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 {
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(): 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 {
const response = await api.get<NavigationResponse>('/api/navigation');
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);
}
}, []);
useEffect(() => {
fetchNavigation();
}, [fetchNavigation]);
useEffect(() => {
const onFeaturesChanged = () => {
fetchNavigation();
};
window.addEventListener('features-changed', onFeaturesChanged);
window.addEventListener('userInfoUpdated', onFeaturesChanged);
return () => {
window.removeEventListener('features-changed', onFeaturesChanged);
window.removeEventListener('userInfoUpdated', 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;