frontend_nyla/src/hooks/useMandateRoles.ts
2026-01-21 21:19:55 +01:00

317 lines
9.3 KiB
TypeScript

/**
* 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<string, any>;
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<Role[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [pagination, setPagination] = useState<PaginationMetadata | null>(null);
// Store current mandateId for refetch
const currentMandateIdRef = useRef<string | undefined>(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<Role[]> => {
setLoading(true);
setError(null);
try {
const headers: Record<string, string> = {};
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<string, string> = {};
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<Role | null> => {
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<string, string> = {};
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<Array<{ value: string; label: string }>> => {
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<Array<{ userId: string; username: string; email?: string }>> => {
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;