frontend_nyla/src/hooks/useUserMandates.ts
2026-02-12 00:39:51 +01:00

286 lines
8.2 KiB
TypeScript

/**
* useUserMandates Hook
*
* Hook for managing user-mandate memberships (which users belong to which mandates with which roles).
* Uses the /api/mandates/{mandateId}/users endpoints.
*/
import { useState, useCallback, useRef } from 'react';
import api from '../api';
// Types
export interface PaginationParams {
page?: number;
pageSize?: number;
search?: string;
filters?: Record<string, any>;
sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
}
export interface PaginationMetadata {
currentPage: number;
pageSize: number;
totalItems: number;
totalPages: number;
}
export interface MandateUser {
id: string; // UserMandate ID as primary key
userId: string;
username: string;
email: string | null;
firstname: string | null;
lastname: string | null;
roleIds: string[];
enabled: boolean;
}
export interface UserMandateCreate {
targetUserId: string;
roleIds: string[];
}
export interface UserMandateResponse {
id: string; // UserMandate ID as primary key
userId: string;
mandateId: string;
roleIds: string[];
enabled: boolean;
}
export interface Role {
id: string;
roleLabel: string;
description?: string | { [key: string]: string };
mandateId?: string;
featureInstanceId?: string;
featureCode?: string;
isSystemRole?: boolean;
}
export interface Mandate {
id: string;
name: string | { [key: string]: string };
label?: string;
code?: string;
language?: string;
isSystem?: boolean;
}
/**
* Hook for managing user-mandate memberships
*/
export function useUserMandates() {
const [users, setUsers] = useState<MandateUser[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [pagination, setPagination] = useState<PaginationMetadata | null>(null);
// Store current mandate for refetch
const currentMandateIdRef = useRef<string>('');
/**
* Fetch all users in a specific mandate with optional pagination
*/
const fetchMandateUsers = useCallback(async (
mandateIdOrPagination?: string | PaginationParams
): Promise<MandateUser[]> => {
setLoading(true);
setError(null);
let mandateId: string;
let paginationParams: PaginationParams = {};
// Handle backward compatibility
if (typeof mandateIdOrPagination === 'string') {
mandateId = mandateIdOrPagination;
currentMandateIdRef.current = mandateId;
} else if (mandateIdOrPagination && typeof mandateIdOrPagination === 'object') {
paginationParams = mandateIdOrPagination;
mandateId = currentMandateIdRef.current;
} else {
mandateId = currentMandateIdRef.current;
}
if (!mandateId) {
setLoading(false);
return [];
}
try {
const params = new URLSearchParams();
if (Object.keys(paginationParams).length > 0) {
params.append('pagination', JSON.stringify(paginationParams));
}
const url = params.toString()
? `/api/mandates/${mandateId}/users?${params.toString()}`
: `/api/mandates/${mandateId}/users`;
const response = await api.get(url);
let data: MandateUser[] = [];
if (response.data?.items && Array.isArray(response.data.items)) {
data = response.data.items;
if (response.data.pagination) {
setPagination(response.data.pagination);
}
} else {
data = Array.isArray(response.data) ? response.data : [];
}
setUsers(data);
return data;
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to fetch mandate users';
setError(errorMessage);
setUsers([]);
setPagination(null);
return [];
} finally {
setLoading(false);
}
}, []);
/**
* Add a user to a mandate with specified roles
*/
const addUserToMandate = useCallback(async (
mandateId: string,
data: UserMandateCreate
): Promise<{ success: boolean; data?: UserMandateResponse; error?: string }> => {
setLoading(true);
setError(null);
try {
const response = await api.post(`/api/mandates/${mandateId}/users`, data);
return { success: true, data: response.data };
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to add user to mandate';
setError(errorMessage);
return { success: false, error: errorMessage };
} finally {
setLoading(false);
}
}, []);
/**
* Remove a user from a mandate
*/
const removeUserFromMandate = useCallback(async (
mandateId: string,
userId: string
): Promise<{ success: boolean; error?: string }> => {
setLoading(true);
setError(null);
try {
await api.delete(`/api/mandates/${mandateId}/users/${userId}`);
// Optimistically update the local state
setUsers(prev => prev.filter(u => u.userId !== userId));
return { success: true };
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to remove user from mandate';
setError(errorMessage);
return { success: false, error: errorMessage };
} finally {
setLoading(false);
}
}, []);
/**
* Update a user's roles within a mandate
*/
const updateUserRoles = useCallback(async (
mandateId: string,
userId: string,
roleIds: string[]
): Promise<{ success: boolean; data?: UserMandateResponse; error?: string }> => {
setLoading(true);
setError(null);
try {
const response = await api.put(`/api/mandates/${mandateId}/users/${userId}/roles`, roleIds);
// Optimistically update the local state
setUsers(prev => prev.map(u =>
u.userId === userId ? { ...u, roleIds } : u
));
return { success: true, data: response.data };
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to update user roles';
setError(errorMessage);
return { success: false, error: errorMessage };
} finally {
setLoading(false);
}
}, []);
/**
* Fetch all available mandates (for selection)
*/
const fetchMandates = useCallback(async (): Promise<Mandate[]> => {
try {
const response = await api.get('/api/mandates/');
if (response.data?.items && Array.isArray(response.data.items)) {
return response.data.items;
}
return Array.isArray(response.data) ? response.data : [];
} catch (err: any) {
console.error('Error fetching mandates:', err);
return [];
}
}, []);
/**
* Fetch available roles for a mandate (mandate-instance roles only).
* Each mandate has its own instances of system roles (admin, user, viewer)
* copied from templates during mandate creation. Only these mandate-bound
* roles should be offered for user assignment - NOT global templates.
*/
const fetchRoles = useCallback(async (mandateId?: string): Promise<Role[]> => {
try {
// Fetch roles server-side filtered by mandate (or templates if no mandateId)
const params = mandateId ? { mandateId } : {};
const response = await api.get('/api/rbac/roles', { params });
let roles: Role[] = [];
if (response.data?.items && Array.isArray(response.data.items)) {
roles = response.data.items;
} else if (Array.isArray(response.data)) {
roles = response.data;
}
return roles;
} catch (err: any) {
console.error('Error fetching roles:', err);
return [];
}
}, []);
/**
* Fetch all users (for selection when adding to mandate)
*/
const fetchAllUsers = useCallback(async (): Promise<Array<{id: string; username: string; email?: string; fullName?: string}>> => {
try {
const response = await api.get('/api/users/');
if (response.data?.items && Array.isArray(response.data.items)) {
return response.data.items;
}
return Array.isArray(response.data) ? response.data : [];
} catch (err: any) {
console.error('Error fetching users:', err);
return [];
}
}, []);
return {
users,
loading,
error,
pagination,
fetchMandateUsers,
addUserToMandate,
removeUserFromMandate,
updateUserRoles,
fetchMandates,
fetchRoles,
fetchAllUsers,
};
}
export default useUserMandates;