frontend_nyla/src/hooks/useFeatureAccess.ts

509 lines
14 KiB
TypeScript

/**
* useFeatureAccess Hook
*
* Hook for managing feature instance access (which users can access which feature instances with which roles).
* Uses the /api/features 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 Feature {
code: string;
label: string | { [key: string]: string };
icon?: string;
enabled?: boolean;
}
export interface FeatureInstance {
id: string;
featureCode: string;
mandateId: string;
label: string;
enabled: boolean;
config?: Record<string, any>; // Instance-specific configuration (JSONB)
}
export interface FeatureAccess {
id: string;
userId: string;
featureInstanceId: string;
enabled: boolean;
roleIds?: string[];
}
export interface FeatureAccessUser {
id: string; // FeatureAccess ID as primary key
userId: string;
username: string;
email?: string;
fullName?: string;
roleIds: string[];
roleLabels: string[];
enabled: boolean;
}
export interface FeatureInstanceRole {
id: string;
roleLabel: string;
description?: { [key: string]: string };
featureCode?: string;
isSystemRole?: boolean;
}
export interface AddUserToInstanceRequest {
userId: string;
roleIds: string[];
}
export interface FeatureInstanceCreate {
featureCode: string;
label: string;
enabled?: boolean;
copyTemplateRoles?: boolean;
config?: Record<string, any>; // Instance-specific configuration (JSONB)
}
/**
* Hook for managing feature access
*/
export function useFeatureAccess() {
const [features, setFeatures] = useState<Feature[]>([]);
const [instances, setInstances] = useState<FeatureInstance[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [instancesPagination, setInstancesPagination] = useState<PaginationMetadata | null>(null);
// Store current context for refetch
const currentMandateIdRef = useRef<string>('');
const currentFeatureCodeRef = useRef<string | undefined>(undefined);
/**
* Fetch all available features
*/
const fetchFeatures = useCallback(async (): Promise<Feature[]> => {
setLoading(true);
setError(null);
try {
const response = await api.get('/api/features/');
// Handle different API response formats (array, {items}, {data})
let data: Feature[] = [];
if (Array.isArray(response.data)) {
data = response.data;
} else if (response.data?.items && Array.isArray(response.data.items)) {
data = response.data.items;
} else if (response.data?.data && Array.isArray(response.data.data)) {
data = response.data.data;
}
setFeatures(data);
return data;
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to fetch features';
setError(errorMessage);
setFeatures([]);
return [];
} finally {
setLoading(false);
}
}, []);
/**
* Fetch feature instances for a mandate with optional pagination
*/
const fetchInstances = useCallback(async (
mandateIdOrPagination?: string | PaginationParams,
featureCode?: string
): Promise<FeatureInstance[]> => {
setLoading(true);
setError(null);
let mandateId: string;
let paginationParams: PaginationParams = {};
// Handle backward compatibility
if (typeof mandateIdOrPagination === 'string') {
mandateId = mandateIdOrPagination;
currentMandateIdRef.current = mandateId;
currentFeatureCodeRef.current = featureCode;
} else if (mandateIdOrPagination && typeof mandateIdOrPagination === 'object') {
paginationParams = mandateIdOrPagination;
mandateId = currentMandateIdRef.current;
featureCode = currentFeatureCodeRef.current;
} else {
mandateId = currentMandateIdRef.current;
featureCode = currentFeatureCodeRef.current;
}
if (!mandateId) {
setLoading(false);
return [];
}
try {
const params = new URLSearchParams();
if (featureCode) {
params.append('featureCode', featureCode);
}
if (Object.keys(paginationParams).length > 0) {
params.append('pagination', JSON.stringify(paginationParams));
}
const url = params.toString()
? `/api/features/instances?${params.toString()}`
: '/api/features/instances';
const response = await api.get(url, {
headers: {
'X-Mandate-Id': mandateId
}
});
let data: FeatureInstance[] = [];
if (response.data?.items && Array.isArray(response.data.items)) {
data = response.data.items;
if (response.data.pagination) {
setInstancesPagination(response.data.pagination);
}
} else {
data = Array.isArray(response.data) ? response.data : [];
}
setInstances(data);
return data;
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to fetch feature instances';
setError(errorMessage);
setInstances([]);
setInstancesPagination(null);
return [];
} finally {
setLoading(false);
}
}, []);
/**
* Create a new feature instance
*/
const createInstance = useCallback(async (
mandateId: string,
data: FeatureInstanceCreate
): Promise<{ success: boolean; data?: FeatureInstance; error?: string }> => {
setLoading(true);
setError(null);
try {
const response = await api.post('/api/features/instances', data, {
headers: {
'X-Mandate-Id': mandateId
}
});
return { success: true, data: response.data };
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to create feature instance';
setError(errorMessage);
return { success: false, error: errorMessage };
} finally {
setLoading(false);
}
}, []);
/**
* Update a feature instance (label, enabled, config)
*/
const updateInstance = useCallback(async (
mandateId: string,
instanceId: string,
data: { label?: string; enabled?: boolean; config?: Record<string, any> }
): Promise<{ success: boolean; data?: FeatureInstance; error?: string }> => {
setLoading(true);
setError(null);
try {
const response = await api.put(`/api/features/instances/${instanceId}`, data, {
headers: {
'X-Mandate-Id': mandateId
}
});
// Update local state
setInstances(prev => prev.map(i => i.id === instanceId ? { ...i, ...response.data } : i));
return { success: true, data: response.data };
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to update feature instance';
setError(errorMessage);
return { success: false, error: errorMessage };
} finally {
setLoading(false);
}
}, []);
/**
* Delete a feature instance
*/
const deleteInstance = useCallback(async (
mandateId: string,
instanceId: string
): Promise<{ success: boolean; error?: string }> => {
setLoading(true);
setError(null);
try {
await api.delete(`/api/features/instances/${instanceId}`, {
headers: {
'X-Mandate-Id': mandateId
}
});
// Optimistically update the local state
setInstances(prev => prev.filter(i => i.id !== instanceId));
return { success: true };
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to delete feature instance';
setError(errorMessage);
return { success: false, error: errorMessage };
} finally {
setLoading(false);
}
}, []);
/**
* Sync roles for a feature instance from templates
*/
const syncInstanceRoles = useCallback(async (
mandateId: string,
instanceId: string,
addOnly: boolean = true
): Promise<{ success: boolean; data?: { added: number; removed: number; unchanged: number }; error?: string }> => {
setLoading(true);
setError(null);
try {
const response = await api.post(`/api/features/instances/${instanceId}/sync-roles?addOnly=${addOnly}`, {}, {
headers: {
'X-Mandate-Id': mandateId
}
});
return { success: true, data: response.data };
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to sync instance roles';
setError(errorMessage);
return { success: false, error: errorMessage };
} finally {
setLoading(false);
}
}, []);
/**
* Get current user's feature instances (grouped by mandate)
*/
const fetchMyFeatureInstances = useCallback(async (): Promise<{
mandates: Array<{
id: string;
name: string;
features: Array<{
code: string;
label: string | { [key: string]: string };
instances: Array<{
id: string;
featureCode: string;
mandateId: string;
instanceLabel: string;
}>;
}>;
}>;
}> => {
try {
const response = await api.get('/api/features/my');
return response.data || { mandates: [] };
} catch (err: any) {
console.error('Error fetching my feature instances:', err);
return { mandates: [] };
}
}, []);
/**
* Get template roles for features
*/
const fetchTemplateRoles = useCallback(async (featureCode?: string): Promise<any[]> => {
try {
let url = '/api/features/templates/roles';
if (featureCode) {
url += `?featureCode=${encodeURIComponent(featureCode)}`;
}
const response = await api.get(url);
return Array.isArray(response.data) ? response.data : [];
} catch (err: any) {
console.error('Error fetching template roles:', err);
return [];
}
}, []);
// ============================================
// Feature Instance Users Management
// ============================================
/**
* Fetch all users with access to a specific feature instance
*/
const fetchInstanceUsers = useCallback(async (
mandateId: string,
instanceId: string
): Promise<FeatureAccessUser[]> => {
setLoading(true);
setError(null);
try {
const response = await api.get(`/api/features/instances/${instanceId}/users`, {
headers: {
'X-Mandate-Id': mandateId
}
});
return Array.isArray(response.data) ? response.data : [];
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to fetch instance users';
setError(errorMessage);
return [];
} finally {
setLoading(false);
}
}, []);
/**
* Add a user to a feature instance with specified roles
*/
const addUserToInstance = useCallback(async (
mandateId: string,
instanceId: string,
data: AddUserToInstanceRequest
): Promise<{ success: boolean; data?: any; error?: string }> => {
setLoading(true);
setError(null);
try {
const response = await api.post(`/api/features/instances/${instanceId}/users`, data, {
headers: {
'X-Mandate-Id': mandateId
}
});
return { success: true, data: response.data };
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to add user to instance';
setError(errorMessage);
return { success: false, error: errorMessage };
} finally {
setLoading(false);
}
}, []);
/**
* Remove a user's access from a feature instance
*/
const removeUserFromInstance = useCallback(async (
mandateId: string,
instanceId: string,
userId: string
): Promise<{ success: boolean; error?: string }> => {
setLoading(true);
setError(null);
try {
await api.delete(`/api/features/instances/${instanceId}/users/${userId}`, {
headers: {
'X-Mandate-Id': mandateId
}
});
return { success: true };
} catch (err: any) {
const errorMessage = err.response?.data?.detail || err.message || 'Failed to remove user from instance';
setError(errorMessage);
return { success: false, error: errorMessage };
} finally {
setLoading(false);
}
}, []);
/**
* Update a user's roles and active flag in a feature instance
*/
const updateInstanceUserRoles = useCallback(async (
mandateId: string,
instanceId: string,
userId: string,
payload: { roleIds: string[]; enabled?: boolean }
): Promise<{ success: boolean; data?: any; error?: string }> => {
setLoading(true);
setError(null);
try {
const body = payload.enabled !== undefined
? { roleIds: payload.roleIds, enabled: payload.enabled }
: { roleIds: payload.roleIds };
const response = await api.put(
`/api/features/instances/${instanceId}/users/${userId}/roles`,
body,
{
headers: {
'X-Mandate-Id': mandateId
}
}
);
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);
}
}, []);
/**
* Get available roles for a feature instance
*/
const fetchInstanceRoles = useCallback(async (
mandateId: string,
instanceId: string
): Promise<FeatureInstanceRole[]> => {
try {
const response = await api.get(`/api/features/instances/${instanceId}/available-roles`, {
headers: {
'X-Mandate-Id': mandateId
}
});
return Array.isArray(response.data) ? response.data : [];
} catch (err: any) {
console.error('Error fetching instance roles:', err);
return [];
}
}, []);
return {
features,
instances,
instancesPagination,
loading,
error,
fetchFeatures,
fetchInstances,
createInstance,
updateInstance,
deleteInstance,
syncInstanceRoles,
fetchMyFeatureInstances,
fetchTemplateRoles,
// Instance users management
fetchInstanceUsers,
addUserToInstance,
removeUserFromInstance,
updateInstanceUserRoles,
fetchInstanceRoles,
};
}
export default useFeatureAccess;