import { useState, useEffect, useCallback } from 'react'; import { useApiRequest } from './useApi'; import { getUserDataCache } from '../utils/userCache'; import { usePermissions, type UserPermissions } from './usePermissions'; import { fetchRbacRules as fetchRbacRulesApi, fetchRbacRuleById as fetchRbacRuleByIdApi, createRbacRule as createRbacRuleApi, updateRbacRule as updateRbacRuleApi, deleteRbacRule as deleteRbacRuleApi, type RbacRule, type RbacRuleUpdateData, type PaginationParams } from '../api/rbacRulesApi'; import { fetchAttributes } from '../api/attributesApi'; import type { AttributeDefinition } from '../api/attributesApi'; import { isCheckboxType, isSelectType, isMultiselectType, isDateTimeType, isTextareaType, type AttributeType } from '../utils/attributeTypeMapper'; // Re-export types for backward compatibility export type { RbacRule, RbacRuleUpdateData, AttributeDefinition, PaginationParams }; // RBAC rules hook (list, update, delete) - following mandates pattern export function useRbacRules() { const [rbacRules, setRbacRules] = useState([]); const [attributes, setAttributes] = useState([]); const [permissions, setPermissions] = useState(null); const [pagination, setPagination] = useState<{ currentPage: number; pageSize: number; totalItems: number; totalPages: number; } | null>(null); const { request, isLoading: loading, error } = useApiRequest(); const { checkPermission } = usePermissions(); // Fetch attributes from backend const fetchAttributesData = useCallback(async () => { try { const attrs = await fetchAttributes(request, 'AccessRule'); setAttributes(attrs); return attrs; } catch (error: any) { // Don't log 429 errors as errors (they're rate limit warnings) if (error.response?.status === 429) { console.warn('Rate limit exceeded while fetching RBAC rule attributes. Please wait.'); } else if (error.response?.status !== 401) { // Only log non-auth errors (401 is expected when not logged in) console.error('Error fetching attributes:', error); } setAttributes([]); return []; } }, [request]); // Fetch permissions from backend const fetchPermissions = useCallback(async () => { try { const perms = await checkPermission('DATA', 'RbacRule'); setPermissions(perms); return perms; } catch (error: any) { console.error('Error fetching permissions:', error); const defaultPerms: UserPermissions = { view: false, read: 'n', create: 'n', update: 'n', delete: 'n', }; setPermissions(defaultPerms); return defaultPerms; } }, [checkPermission]); const fetchRbacRules = useCallback(async (params?: PaginationParams) => { try { const requestParams: any = {}; // Build pagination object if provided if (params) { const paginationObj: any = {}; if (params.page !== undefined) paginationObj.page = params.page; if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize; if (params.sort) paginationObj.sort = params.sort; if (params.filters) paginationObj.filters = params.filters; if (params.search) paginationObj.search = params.search; if (Object.keys(paginationObj).length > 0) { requestParams.pagination = JSON.stringify(paginationObj); } } const data = await fetchRbacRulesApi(request, params); // Debug logging for pagination if (import.meta.env.DEV) { console.log('📊 useRbacRules: Backend response:', { hasItems: data && typeof data === 'object' && 'items' in data, itemsCount: data && typeof data === 'object' && 'items' in data ? (data.items as any[]).length : 'N/A', pagination: data && typeof data === 'object' && 'pagination' in data ? data.pagination : 'N/A', fullData: data }); } // Handle paginated response if (data && typeof data === 'object' && 'items' in data) { const items = Array.isArray(data.items) ? data.items : []; setRbacRules(items); if (data.pagination) { if (import.meta.env.DEV) { console.log('📊 useRbacRules: Setting pagination:', data.pagination); } setPagination(data.pagination); } else { if (import.meta.env.DEV) { console.warn('⚠️ useRbacRules: No pagination object in response'); } } } else { // Handle non-paginated response (backward compatibility) const items = Array.isArray(data) ? data : []; setRbacRules(items); setPagination(null); } } catch (error: any) { // Error is already handled by useApiRequest setRbacRules([]); setPagination(null); } }, [request]); // Optimistically remove a RBAC rule from the local state const removeOptimistically = (ruleId: string) => { setRbacRules(prevRules => prevRules.filter(rule => rule.id !== ruleId)); }; // Optimistically update a RBAC rule in the local state const updateOptimistically = (ruleId: string, updateData: Partial) => { setRbacRules(prevRules => prevRules.map(rule => rule.id === ruleId ? { ...rule, ...updateData } : rule ) ); }; // Fetch a single RBAC rule by ID const fetchRbacRuleById = useCallback(async (ruleId: string): Promise => { return await fetchRbacRuleByIdApi(request, ruleId); }, [request]); // Generate edit fields from attributes dynamically using attributeTypeMapper utilities const generateEditFieldsFromAttributes = useCallback((): Array<{ key: string; label: string; type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly'; editable?: boolean; required?: boolean; validator?: (value: any) => string | null; minRows?: number; maxRows?: number; options?: Array<{ value: string | number; label: string }>; optionsReference?: string; // For options that need to be fetched (e.g., "user.role") }> => { if (!attributes || attributes.length === 0) { return []; } const editableFields = attributes .filter(attr => { // Filter out non-editable fields based on readonly/editable flags if (attr.readonly === true || attr.editable === false) { return false; // Don't show readonly fields in edit form } // Also filter out common non-editable fields const nonEditableFields = ['id', 'ruleId', 'sysCreatedBy', '_hideDelete']; return !nonEditableFields.includes(attr.name); }) .map(attr => { // Map backend attribute type to form field type using attributeTypeMapper utilities let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' = 'string'; let options: Array<{ value: string | number; label: string }> | undefined = undefined; let optionsReference: string | undefined = undefined; const attrType = attr.type as AttributeType; // Use attributeTypeMapper utilities to determine field type if (isCheckboxType(attrType)) { fieldType = 'boolean'; } else if (attrType === 'email') { fieldType = 'email'; } else if (isDateTimeType(attrType)) { fieldType = 'date'; } else if (isSelectType(attrType)) { fieldType = 'enum'; // Handle options - can be array or string reference const attrOptions = (attr as any).options; if (Array.isArray(attrOptions)) { options = attrOptions.map((opt: any) => { const labelValue = typeof opt.label === 'string' ? opt.label : opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); return { value: opt.value, label: labelValue }; }); } else if (typeof attrOptions === 'string') { // Options reference (e.g., "user.role", "auth.authority") optionsReference = attrOptions; } } else if (isMultiselectType(attrType)) { fieldType = 'multiselect'; // Handle options - can be array or string reference const attrOptions = (attr as any).options; if (Array.isArray(attrOptions)) { options = attrOptions.map((opt: any) => { const labelValue = typeof opt.label === 'string' ? opt.label : opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); return { value: opt.value, label: labelValue }; }); } else if (typeof attrOptions === 'string') { // Options reference (e.g., "user.role", "auth.authority") optionsReference = attrOptions; } } else if (isTextareaType(attrType)) { fieldType = 'textarea'; } else if (attrType === 'readonly') { fieldType = 'readonly'; } else { fieldType = 'string'; } // Define validators and required fields let required = attr.required === true; let validator: ((value: any) => string | null) | undefined = undefined; let minRows: number | undefined = undefined; let maxRows: number | undefined = undefined; // Email validation if (fieldType === 'email') { validator = (value: any) => { if (required && (!value || (typeof value === 'string' && value.trim() === ''))) { return 'Email cannot be empty'; } if (value && typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { return 'Invalid email format'; } return null; }; } // Textarea settings else if (fieldType === 'textarea') { minRows = 4; maxRows = 8; if (attr.name.toLowerCase().includes('content')) { minRows = 6; maxRows = 12; } } // Multiselect validation else if (fieldType === 'multiselect' && required) { validator = (value: any[]) => { if (!value || !Array.isArray(value) || value.length === 0) { return `${attr.label} is required`; } return null; }; } return { key: attr.name, label: attr.label || attr.name, type: fieldType, editable: (attr as any).editable !== false && (attr as any).readonly !== true, required, validator, minRows, maxRows, options, optionsReference }; }); return editableFields; }, [attributes]); // Generate create fields from attributes dynamically using attributeTypeMapper utilities const generateCreateFieldsFromAttributes = useCallback((): Array<{ key: string; label: string; type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly'; required?: boolean; validator?: (value: any) => string | null; minRows?: number; maxRows?: number; options?: Array<{ value: string | number; label: string }>; optionsReference?: string; placeholder?: string; }> => { if (!attributes || attributes.length === 0) { return []; } const createFields = attributes .filter(attr => { // Filter out non-editable fields and auto-generated fields for create forms if (attr.readonly === true || attr.editable === false) { return false; } // Filter out ID fields and other auto-generated fields const nonEditableFields = ['id', 'ruleId', 'sysCreatedBy', '_hideDelete']; return !nonEditableFields.includes(attr.name); }) .map(attr => { // Map backend attribute type to form field type using attributeTypeMapper utilities let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'multiselect' | 'readonly' = 'string'; let options: Array<{ value: string | number; label: string }> | undefined = undefined; let optionsReference: string | undefined = undefined; const attrType = attr.type as AttributeType; // Use attributeTypeMapper utilities to determine field type if (isCheckboxType(attrType)) { fieldType = 'boolean'; } else if (attrType === 'email') { fieldType = 'email'; } else if (isDateTimeType(attrType)) { fieldType = 'date'; } else if (isSelectType(attrType)) { fieldType = 'enum'; const attrOptions = (attr as any).options; if (Array.isArray(attrOptions)) { options = attrOptions.map((opt: any) => { const labelValue = typeof opt.label === 'string' ? opt.label : opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); return { value: opt.value, label: labelValue }; }); } else if (typeof attrOptions === 'string') { optionsReference = attrOptions; } } else if (isMultiselectType(attrType)) { fieldType = 'multiselect'; const attrOptions = (attr as any).options; if (Array.isArray(attrOptions)) { options = attrOptions.map((opt: any) => { const labelValue = typeof opt.label === 'string' ? opt.label : opt.label?.en || opt.label?.[Object.keys(opt.label || {})[0]] || String(opt.value); return { value: opt.value, label: labelValue }; }); } else if (typeof attrOptions === 'string') { optionsReference = attrOptions; } } else if (isTextareaType(attrType)) { fieldType = 'textarea'; } else { fieldType = 'string'; } // Define validators and required fields let required = attr.required === true; let validator: ((value: any) => string | null) | undefined = undefined; let minRows: number | undefined = undefined; let maxRows: number | undefined = undefined; // Email validation if (fieldType === 'email') { validator = (value: any) => { if (required && (!value || (typeof value === 'string' && value.trim() === ''))) { return 'Email cannot be empty'; } if (value && typeof value === 'string' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { return 'Invalid email format'; } return null; }; } // Textarea settings else if (fieldType === 'textarea') { minRows = 4; maxRows = 8; if (attr.name.toLowerCase().includes('content')) { minRows = 6; maxRows = 12; } } // Multiselect validation else if (fieldType === 'multiselect' && required) { validator = (value: any[]) => { if (!value || !Array.isArray(value) || value.length === 0) { return `${attr.label} is required`; } return null; }; } // String validation for required fields else if (fieldType === 'string' && required) { validator = (value: any) => { if (!value || (typeof value === 'string' && value.trim() === '')) { return `${attr.label} is required`; } return null; }; } return { key: attr.name, label: attr.label || attr.name, type: fieldType, required, validator, minRows, maxRows, options, optionsReference, placeholder: (attr as any).placeholder }; }); return createFields; }, [attributes]); // Ensure attributes are loaded - can be called by EditActionButton const ensureAttributesLoaded = useCallback(async () => { // Don't fetch attributes if user is not authenticated (prevents 401 errors) const currentUser = getUserDataCache(); if (!currentUser) { return []; } if (attributes && attributes.length > 0) { return attributes; } const fetchedAttributes = await fetchAttributesData(); return fetchedAttributes; }, [attributes, fetchAttributesData]); // Fetch attributes and permissions on mount (only if user is authenticated) useEffect(() => { const currentUser = getUserDataCache(); if (currentUser) { fetchAttributesData(); fetchPermissions(); } }, [fetchAttributesData, fetchPermissions]); // Initial fetch useEffect(() => { fetchRbacRules(); }, [fetchRbacRules]); return { data: rbacRules, loading, error, refetch: fetchRbacRules, removeOptimistically, updateOptimistically, attributes, permissions, pagination, fetchRbacRuleById, generateEditFieldsFromAttributes, generateCreateFieldsFromAttributes, ensureAttributesLoaded }; } // RBAC rule operations hook export function useRbacRuleOperations() { const [deletingRbacRules, setDeletingRbacRules] = useState>(new Set()); const [editingRbacRules, setEditingRbacRules] = useState>(new Set()); const [creatingRbacRule, setCreatingRbacRule] = useState(false); const { request, isLoading } = useApiRequest(); const [deleteError, setDeleteError] = useState(null); const [createError, setCreateError] = useState(null); const [updateError, setUpdateError] = useState(null); const handleRbacRuleDelete = async (ruleId: string) => { setDeleteError(null); setDeletingRbacRules(prev => new Set(prev).add(ruleId)); try { await deleteRbacRuleApi(request, ruleId); await new Promise(resolve => setTimeout(resolve, 300)); return true; } catch (error: any) { setDeleteError(error.message); return false; } finally { setDeletingRbacRules(prev => { const newSet = new Set(prev); newSet.delete(ruleId); return newSet; }); } }; const handleRbacRuleCreate = async (ruleData: Partial) => { setCreateError(null); setCreatingRbacRule(true); try { const newRule = await createRbacRuleApi(request, ruleData); return { success: true, ruleData: newRule }; } catch (error: any) { setCreateError(error.message); return { success: false, error: error.message }; } finally { setCreatingRbacRule(false); } }; const handleRbacRuleUpdate = async (ruleId: string, updateData: RbacRuleUpdateData, _originalData?: any) => { setUpdateError(null); setEditingRbacRules(prev => new Set(prev).add(ruleId)); try { const updatedRule = await updateRbacRuleApi(request, ruleId, updateData); return { success: true, ruleData: updatedRule }; } catch (error: any) { const errorMessage = error.response?.data?.message || error.message || 'Failed to update RBAC rule'; const statusCode = error.response?.status; setUpdateError(errorMessage); return { success: false, error: errorMessage, statusCode, isPermissionError: statusCode === 403, isValidationError: statusCode === 400 }; } finally { setEditingRbacRules(prev => { const newSet = new Set(prev); newSet.delete(ruleId); return newSet; }); } }; // Generic inline update handler for FormGeneratorTable const handleInlineUpdate = async (ruleId: string, changes: Partial) => { const result = await handleRbacRuleUpdate(ruleId, changes as RbacRuleUpdateData); if (!result.success) { throw new Error(result.error || 'Failed to update'); } return result; }; return { deletingRbacRules, editingRbacRules, creatingRbacRule, deleteError, createError, updateError, handleRbacRuleDelete, handleRbacRuleCreate, handleRbacRuleUpdate, handleInlineUpdate, isLoading }; }