import { useEffect, useState, useMemo } from 'react'; import type { ReactElement } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { ChevronDown, ChevronRight } from 'lucide-react'; import { Modal, FormField, PrimaryButton, SecondaryButton, MultiselectPaginatedSelect } from '@/components/shared'; import type { CreateRoleRequest } from '@/types/role'; import { useAppSelector } from '@/hooks/redux-hooks'; // Utility function to generate code from name const generateCodeFromName = (name: string): string => { return name .toLowerCase() .trim() .replace(/[^a-z0-9\s]/g, '') // Remove special characters except spaces .replace(/\s+/g, '_') // Replace spaces with underscores .replace(/_+/g, '_') // Replace multiple underscores with single underscore .replace(/^_|_$/g, ''); // Remove leading/trailing underscores }; // All available resources const ALL_RESOURCES = [ 'users', // 'tenants', 'roles', 'permissions', // 'user_roles', // 'user_tenants', // 'sessions', // 'api_keys', // 'api_key_permissions', 'projects', 'document', 'audit', 'security', 'workflow', 'training', 'capa', 'supplier', 'reports', 'notifications', 'files', 'settings', // 'modules', 'audit_logs', // 'event_logs', // 'health_history', 'qms_connections', 'qms_sync_jobs', 'qms_sync_conflicts', 'qms_entity_mappings', 'ai', 'qms', ]; // All available actions const ALL_ACTIONS = ['create', 'read', 'update', 'delete']; // Validation schema const newRoleSchema = z.object({ name: z.string().min(1, 'Role name is required'), code: z .string() .min(1, 'Code is required') .regex( /^[a-z]+(_[a-z]+)*$/, "Code must be lowercase and use '_' for separation (e.g. abc_def)" ), description: z.string().min(1, 'Description is required'), module_ids: z.array(z.string().uuid()).optional().nullable(), permissions: z.array(z.object({ resource: z.string(), action: z.string(), })).optional().nullable(), }); type NewRoleFormData = z.infer; interface NewRoleModalProps { isOpen: boolean; onClose: () => void; onSubmit: (data: CreateRoleRequest) => Promise; isLoading?: boolean; } export const NewRoleModal = ({ isOpen, onClose, onSubmit, isLoading = false, }: NewRoleModalProps): ReactElement | null => { const permissions = useAppSelector((state) => state.auth.permissions); const roles = useAppSelector((state) => state.auth.roles); const tenant = useAppSelector((state) => state.auth.tenant); const isSuperAdmin = roles.includes('super_admin'); const [selectedModules, setSelectedModules] = useState([]); const [selectedPermissions, setSelectedPermissions] = useState>([]); const [expandedResources, setExpandedResources] = useState>(new Set()); const { register, handleSubmit, setValue, watch, reset, setError, clearErrors, formState: { errors }, } = useForm({ resolver: zodResolver(newRoleSchema), defaultValues: { code: undefined, module_ids: [], permissions: [], }, }); const nameValue = watch('name'); // Auto-generate code from name useEffect(() => { if (nameValue) { const generatedCode = generateCodeFromName(nameValue); setValue('code', generatedCode, { shouldValidate: true }); } }, [nameValue, setValue]); // Reset form when modal closes useEffect(() => { if (!isOpen) { reset({ name: '', code: undefined, description: '', module_ids: [], permissions: [], }); setSelectedModules([]); setSelectedPermissions([]); clearErrors(); } }, [isOpen, reset, clearErrors]); // Load modules from tenant assignedModules const loadModules = async (page: number, limit: number) => { const assignedModules = tenant?.assignedModules || []; const startIndex = (page - 1) * limit; const endIndex = startIndex + limit; const paginatedModules = assignedModules.slice(startIndex, endIndex); return { options: paginatedModules.map((module) => ({ value: module.id, label: module.name, })), pagination: { page, limit, total: assignedModules.length, totalPages: Math.ceil(assignedModules.length / limit), hasMore: endIndex < assignedModules.length, }, }; }; // Build available resources and actions based on user permissions const availableResourcesAndActions = useMemo(() => { const resourceMap = new Map>(); permissions.forEach((perm) => { const { resource, action } = perm; if (resource === '*') { // If resource is *, show all resources ALL_RESOURCES.forEach((res) => { if (!resourceMap.has(res)) { resourceMap.set(res, new Set()); } const actions = resourceMap.get(res)!; if (action === '*') { // If action is also *, add all actions ALL_ACTIONS.forEach((act) => actions.add(act)); } else { actions.add(action); } }); } else { // Specific resource if (!resourceMap.has(resource)) { resourceMap.set(resource, new Set()); } const actions = resourceMap.get(resource)!; if (action === '*') { // If action is *, add all actions for this resource ALL_ACTIONS.forEach((act) => actions.add(act)); } else { actions.add(action); } } }); return resourceMap; }, [permissions]); // Check if a resource has any selected actions const hasSelectedActions = (resource: string, actions: Set): boolean => { return Array.from(actions).some((action) => { return selectedPermissions.some((p) => { // Check for exact match if (p.resource === resource && p.action === action) return true; // Check for wildcard resource with exact action if (p.resource === '*' && p.action === action) return true; // Check for exact resource with wildcard action if (p.resource === resource && p.action === '*') return true; // Check for wildcard resource with wildcard action if (p.resource === '*' && p.action === '*') return true; return false; }); }); }; // Toggle resource expansion const toggleResource = (resource: string) => { setExpandedResources((prev) => { const newSet = new Set(prev); if (newSet.has(resource)) { newSet.delete(resource); } else { newSet.add(resource); } return newSet; }); }; // Handle permission checkbox change const handlePermissionChange = (resource: string, action: string, checked: boolean) => { setSelectedPermissions((prev) => { const newPerms = [...prev]; if (checked) { // Add permission if not already exists if (!newPerms.some((p) => p.resource === resource && p.action === action)) { newPerms.push({ resource, action }); } } else { // Remove permission return newPerms.filter((p) => !(p.resource === resource && p.action === action)); } return newPerms; }); }; // Update form value when permissions change useEffect(() => { setValue('permissions', selectedPermissions.length > 0 ? selectedPermissions : []); }, [selectedPermissions, setValue]); const handleFormSubmit = async (data: NewRoleFormData): Promise => { clearErrors(); try { const submitData = { ...data, // Only include module_ids if user is not super_admin module_ids: isSuperAdmin ? undefined : (selectedModules.length > 0 ? selectedModules : undefined), permissions: selectedPermissions.length > 0 ? selectedPermissions : undefined, }; await onSubmit(submitData as CreateRoleRequest); } catch (error: any) { // Handle validation errors from API if (error?.response?.data?.details && Array.isArray(error.response.data.details)) { const validationErrors = error.response.data.details; validationErrors.forEach((detail: { path: string; message: string }) => { if ( detail.path === 'name' || detail.path === 'code' || detail.path === 'description' || detail.path === 'module_ids' || detail.path === 'permissions' ) { setError(detail.path as keyof NewRoleFormData, { type: 'server', message: detail.message, }); } }); } else { // Handle general errors const errorObj = error?.response?.data?.error; const errorMessage = (typeof errorObj === 'object' && errorObj !== null && 'message' in errorObj ? errorObj.message : null) || (typeof errorObj === 'string' ? errorObj : null) || error?.response?.data?.message || error?.message || 'Failed to create role. Please try again.'; setError('root', { type: 'server', message: typeof errorMessage === 'string' ? errorMessage : 'Failed to create role. Please try again.', }); } } }; return ( Cancel {isLoading ? 'Creating...' : 'Create Role'} } >
{/* General Error Display */} {errors.root && (

{errors.root.message}

)} {/* Role Name and Role Code Row */}
{/* Description */} {/* Module Selection - Only show if user is not super_admin */} {!isSuperAdmin && ( { setSelectedModules(values); setValue('module_ids', values.length > 0 ? values : []); }} onLoadOptions={loadModules} error={errors.module_ids?.message} /> )} {/* Permissions Section */}
{errors.permissions && (

{errors.permissions.message}

)}
{Array.from(availableResourcesAndActions.entries()).length === 0 ? (

No permissions available

) : (
{Array.from(availableResourcesAndActions.entries()).map(([resource, actions]) => { const isExpanded = expandedResources.has(resource); const hasSelected = hasSelectedActions(resource, actions); return (
{isExpanded && (
{Array.from(actions).map((action) => { const isChecked = selectedPermissions.some((p) => { // Check for exact match if (p.resource === resource && p.action === action) return true; // Check for wildcard resource with exact action if (p.resource === '*' && p.action === action) return true; // Check for exact resource with wildcard action if (p.resource === resource && p.action === '*') return true; // Check for wildcard resource with wildcard action if (p.resource === '*' && p.action === '*') return true; return false; }); return ( ); })}
)}
); })}
)}
); };