/** * 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; 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; // 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; // Instance-specific configuration (JSONB) } /** * Hook for managing feature access */ export function useFeatureAccess() { const [features, setFeatures] = useState([]); const [instances, setInstances] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [instancesPagination, setInstancesPagination] = useState(null); // Store current context for refetch const currentMandateIdRef = useRef(''); const currentFeatureCodeRef = useRef(undefined); /** * Fetch all available features */ const fetchFeatures = useCallback(async (): Promise => { 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 => { 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 } ): 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 => { 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 => { 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 => { 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;