/** * useMandateRoles Hook * * Hook for managing roles within a specific mandate. * Uses the /api/rbac/roles endpoints. */ import { useState, useCallback, useRef } from 'react'; import api from '../api'; // Types export interface Role { id: string; roleLabel: string; description?: string | { [key: string]: string }; mandateId?: string; featureInstanceId?: string; featureCode?: string; isSystemRole?: boolean; isTemplate?: boolean; createdAt?: number; updatedAt?: number; } export interface RoleCreate { roleLabel: string; description?: string | { [key: string]: string }; mandateId?: string; featureInstanceId?: string; featureCode?: string; } export interface RoleUpdate { roleLabel?: string; description?: string | { [key: string]: string }; mandateId?: string | null; } export interface PaginationParams { page?: number; pageSize?: number; search?: string; filters?: Record; sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; scopeFilter?: 'all' | 'mandate' | 'global'; // Backend filter for role scope } export interface PaginationMetadata { currentPage: number; pageSize: number; totalItems: number; totalPages: number; } /** * Hook for managing mandate roles */ export function useMandateRoles() { const [roles, setRoles] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [pagination, setPagination] = useState(null); // Store current mandateId for refetch const currentMandateIdRef = useRef(undefined); /** * Fetch all roles with pagination support * @param mandateIdOrParams - Either a mandateId string (backward compatible) or pagination params * @param additionalParams - Additional parameters like scopeFilter (when first param is mandateId) */ const fetchRoles = useCallback(async ( mandateIdOrParams?: string | PaginationParams, additionalParams?: PaginationParams ): Promise => { setLoading(true); setError(null); try { const headers: Record = {}; let paginationParams: PaginationParams = {}; let mandateId: string | undefined; let scopeFilter: string | undefined; // Handle backward compatibility: first param can be mandateId string or pagination object if (typeof mandateIdOrParams === 'string') { mandateId = mandateIdOrParams; currentMandateIdRef.current = mandateId; // If additional params provided, use them if (additionalParams) { paginationParams = additionalParams; scopeFilter = additionalParams.scopeFilter; } } else if (mandateIdOrParams && typeof mandateIdOrParams === 'object') { paginationParams = mandateIdOrParams; mandateId = currentMandateIdRef.current; scopeFilter = mandateIdOrParams.scopeFilter; } if (mandateId) { headers['X-Mandate-Id'] = mandateId; } // Build query params for pagination (exclude scopeFilter from pagination JSON) const { scopeFilter: _, ...paginationWithoutScope } = paginationParams; const queryParams: Record = {}; if (Object.keys(paginationWithoutScope).length > 0) { queryParams.pagination = JSON.stringify(paginationWithoutScope); } // Do NOT include feature template roles - they belong to Feature-Rollen page // According to admin_ui_concept.md: Filter: featureCode=null AND featureInstanceId=null queryParams.includeTemplates = 'false'; // Include mandate-specific roles for the selected mandate if (mandateId) { queryParams.mandateId = mandateId; } // Include scopeFilter as separate query parameter if (scopeFilter) { queryParams.scopeFilter = scopeFilter; } const response = await api.get('/api/rbac/roles', { headers, params: queryParams }); let data: Role[] = []; let paginationMeta: PaginationMetadata | null = null; if (response.data?.items && Array.isArray(response.data.items)) { data = response.data.items; if (response.data.pagination) { paginationMeta = response.data.pagination; } } else if (Array.isArray(response.data)) { data = response.data; } // No client-side filtering needed - backend already filters setRoles(data); setPagination(paginationMeta); return data; } catch (err: any) { const errorMessage = err.response?.data?.detail || err.message || 'Failed to fetch roles'; setError(errorMessage); setRoles([]); setPagination(null); return []; } finally { setLoading(false); } }, []); /** * Get a single role by ID */ const getRole = useCallback(async (roleId: string): Promise => { try { const response = await api.get(`/api/rbac/roles/${roleId}`); return response.data; } catch (err: any) { console.error('Error fetching role:', err); return null; } }, []); /** * Create a new role */ const createRole = useCallback(async ( data: RoleCreate, mandateId?: string ): Promise<{ success: boolean; data?: Role; error?: string }> => { setLoading(true); setError(null); try { const headers: Record = {}; if (mandateId) { headers['X-Mandate-Id'] = mandateId; } const response = await api.post('/api/rbac/roles', data, { headers }); return { success: true, data: response.data }; } catch (err: any) { const errorMessage = err.response?.data?.detail || err.message || 'Failed to create role'; setError(errorMessage); return { success: false, error: errorMessage }; } finally { setLoading(false); } }, []); /** * Update an existing role */ const updateRole = useCallback(async ( roleId: string, data: RoleUpdate ): Promise<{ success: boolean; data?: Role; error?: string }> => { setLoading(true); setError(null); try { const response = await api.put(`/api/rbac/roles/${roleId}`, data); // Optimistically update local state (convert null to undefined for mandateId) const updateData = { ...data, mandateId: data.mandateId === null ? undefined : data.mandateId }; setRoles(prev => prev.map(r => r.id === roleId ? { ...r, ...updateData } : r)); return { success: true, data: response.data }; } catch (err: any) { const errorMessage = err.response?.data?.detail || err.message || 'Failed to update role'; setError(errorMessage); return { success: false, error: errorMessage }; } finally { setLoading(false); } }, []); /** * Delete a role */ const deleteRole = useCallback(async ( roleId: string ): Promise<{ success: boolean; error?: string }> => { setLoading(true); setError(null); try { await api.delete(`/api/rbac/roles/${roleId}`); // Optimistically update local state setRoles(prev => prev.filter(r => r.id !== roleId)); return { success: true }; } catch (err: any) { const errorMessage = err.response?.data?.detail || err.message || 'Failed to delete role'; setError(errorMessage); return { success: false, error: errorMessage }; } finally { setLoading(false); } }, []); /** * Get role options (for dropdowns) */ const fetchRoleOptions = useCallback(async (): Promise> => { try { const response = await api.get('/api/rbac/roles/options'); if (Array.isArray(response.data)) { return response.data.map((r: any) => ({ value: r.id || r.value, label: r.roleLabel || r.label || r.id })); } return []; } catch (err: any) { console.error('Error fetching role options:', err); return []; } }, []); /** * Get users with a specific role */ const getUsersWithRole = useCallback(async ( roleLabel: string ): Promise> => { try { const response = await api.get(`/api/rbac/roles/roles/${roleLabel}/users`); return Array.isArray(response.data) ? response.data : []; } catch (err: any) { console.error('Error fetching users with role:', err); return []; } }, []); /** * Filter roles by type */ const getMandateRoles = useCallback((mandateId: string) => { return roles.filter(r => r.mandateId === mandateId && !r.featureInstanceId ); }, [roles]); const getFeatureRoles = useCallback((featureInstanceId: string) => { return roles.filter(r => r.featureInstanceId === featureInstanceId); }, [roles]); const getGlobalRoles = useCallback(() => { return roles.filter(r => !r.mandateId && !r.featureInstanceId); }, [roles]); const getTemplateRoles = useCallback(() => { return roles.filter(r => r.isTemplate === true); }, [roles]); return { roles, loading, error, pagination, fetchRoles, getRole, createRole, updateRole, deleteRole, fetchRoleOptions, getUsersWithRole, getMandateRoles, getFeatureRoles, getGlobalRoles, getTemplateRoles, }; } export default useMandateRoles;