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, FormTextArea, } 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"), 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; defaultTenantId?: string; // If provided, automatically include tenant_id in request body } export const NewRoleModal = ({ isOpen, onClose, onSubmit, isLoading = false, defaultTenantId, }: NewRoleModalProps): ReactElement | null => { const permissions = useAppSelector((state) => state.auth.permissions); const roles = useAppSelector((state) => state.auth.roles); const isSuperAdmin = roles.includes("super_admin"); const [selectedPermissions, setSelectedPermissions] = useState< Array<{ resource: string; action: string }> >([]); // const [expandedResources, setExpandedResources] = useState>( // new Set(), // ); const { register, handleSubmit, setValue, watch, reset, setError, clearErrors, formState: { errors }, } = useForm({ resolver: zodResolver(newRoleSchema), defaultValues: { code: undefined, 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: "", permissions: [], }); setSelectedPermissions([]); clearErrors(); } }, [isOpen, reset, clearErrors]); // 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, // For super_admin, always include tenant_id if defaultTenantId is provided tenant_id: isSuperAdmin ? defaultTenantId || undefined : defaultTenantId || 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 === "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 */} {/* */} {/* Permissions Section */}
{/* Header Section */}

Permissions

Select allowed actions for this role by module.

{errors.permissions && (

{errors.permissions.message}

)} {/* Table */}
{/* Table Header */} {["View", "Create", "Edit", "Delete"].map((action) => ( ))} {/* Table Body */} {Array.from(availableResourcesAndActions.entries()).map( ([resource, actions]) => ( {/* Resource Name */} {/* Action Columns */} {[ "read", "create", "update", "delete", // "approve", // "admin", ].map((action) => { const isChecked = selectedPermissions.some((p) => { if (p.resource === resource && p.action === action) return true; if (p.resource === "*" && p.action === action) return true; if (p.resource === resource && p.action === "*") return true; if (p.resource === "*" && p.action === "*") return true; return false; }); const isAvailable = actions.has(action); return ( ); })} ), )}
Platform Services
{action}
{resource.replace(/_/g, " ")}
handlePermissionChange( resource, action, e.target.checked, ) } className=" w-4 h-4 rounded border-[#D1D5DB] text-[#0F3CC9] focus:ring-[#0F3CC9] disabled:opacity-30 disabled:cursor-not-allowed " />
{/*
{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 ( ); })}
)}
); }, )}
)}
*/}
); };