import { useState, useEffect, type ReactElement, forwardRef, useImperativeHandle, useMemo, useRef, } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Plus, ArrowUpDown, Loader2 } from "lucide-react"; import { PrimaryButton, SecondaryButton, ActionDropdown, DataTable, Pagination, FilterDropdown, SearchBox, Modal, FormField, FormTextArea, // DeleteConfirmationModal, type Column, } from "@/components/shared"; import { roleService } from "@/services/role-service"; import type { Role, CreateRoleRequest, UpdateRoleRequest } from "@/types/role"; import { showToast } from "@/utils/toast"; import { formatDate } from "@/utils/format-date"; import { useAppSelector } from "@/hooks/redux-hooks"; import CodeBadge from "@/components/shared/CodeBadge"; import { usePermissions } from "@/hooks/usePermissions"; // 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", "roles", "tenants", "modules", // "permissions", // "projects", "document", // "audit", "security", "workflow", // "training", // "capa", "supplier", // "reports", "notifications", "files", "settings", "audit_logs", "audit_resources", // "qms_connections", // "qms_sync_jobs", // "qms_sync_conflicts", // "qms_entity_mappings", "ai", // "qms", ]; // All available actions const ALL_ACTIONS = ["create", "read", "update", "delete"]; // Resources that should not be visible or managed on the platform roles UI const EXCLUDED_RESOURCES = new Set([ "usage", "licenses", "pricing_plans", "pricing-plans", "pricing plans", "invoices", "billing", "payments", ]); // Validation schema for creation const newPlatformRoleSchema = 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 NewPlatformRoleFormData = z.infer; // Validation schema for update const editPlatformRoleSchema = 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 EditPlatformRoleFormData = z.infer; // Dedicated Creation Modal interface NewPlatformRoleModalProps { isOpen: boolean; onClose: () => void; onSubmit: (data: CreateRoleRequest) => Promise; isLoading: boolean; } const NewPlatformRoleModal = ({ isOpen, onClose, onSubmit, isLoading = false, }: NewPlatformRoleModalProps): ReactElement | null => { const permissions = useAppSelector((state) => state.auth.permissions); const [selectedPermissions, setSelectedPermissions] = useState< Array<{ resource: string; action: string }> >([]); const { register, handleSubmit, setValue, watch, reset, setError, clearErrors, formState: { errors }, } = useForm({ resolver: zodResolver(newPlatformRoleSchema), 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 (EXCLUDED_RESOURCES.has(resource)) return; if (resource === "*") { ALL_RESOURCES.forEach((res) => { if (EXCLUDED_RESOURCES.has(res)) return; if (!resourceMap.has(res)) { resourceMap.set(res, new Set()); } const actions = resourceMap.get(res)!; if (action === "*") { ALL_ACTIONS.forEach((act) => actions.add(act)); } else { actions.add(action); } }); } else { if (!resourceMap.has(resource)) { resourceMap.set(resource, new Set()); } const actions = resourceMap.get(resource)!; if (action === "*") { ALL_ACTIONS.forEach((act) => actions.add(act)); } else { actions.add(action); } } }); return resourceMap; }, [permissions]); // Handle permission checkbox change const handlePermissionChange = ( resource: string, action: string, checked: boolean, ) => { setSelectedPermissions((prev) => { const newPerms = [...prev]; if (checked) { if ( !newPerms.some((p) => p.resource === resource && p.action === action) ) { newPerms.push({ resource, action }); } } else { return newPerms.filter( (p) => !(p.resource === resource && p.action === action), ); } return newPerms; }); }; useEffect(() => { setValue( "permissions", selectedPermissions.length > 0 ? selectedPermissions : [], ); }, [selectedPermissions, setValue]); const handleFormSubmit = async ( data: NewPlatformRoleFormData, ): Promise => { clearErrors(); try { await onSubmit(data as CreateRoleRequest); } catch (error: any) { 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 NewPlatformRoleFormData, { type: "server", message: detail.message, }); } }, ); } else { 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 platform role. Please try again."; setError("root", { type: "server", message: typeof errorMessage === "string" ? errorMessage : "Failed to create platform role. Please try again.", }); } } }; return ( Cancel {isLoading ? "Creating..." : "Create Role"} } >
{errors.root && (

{errors.root.message}

)}

Permissions

Select allowed actions for this role by platform service.

{errors.permissions && (

{errors.permissions.message}

)}
{["View", "Create", "Edit", "Delete"].map((action) => ( ))} {Array.from(availableResourcesAndActions.entries()).map( ([resource, actions]) => ( {["read", "create", "update", "delete"].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" />
); }; // Dedicated Edit Modal interface EditPlatformRoleModalProps { isOpen: boolean; onClose: () => void; roleId: string | null; onLoadRole: (id: string) => Promise; onSubmit: (id: string, data: UpdateRoleRequest) => Promise; isLoading: boolean; } const EditPlatformRoleModal = ({ isOpen, onClose, roleId, onLoadRole, onSubmit, isLoading = false, }: EditPlatformRoleModalProps): ReactElement | null => { const [isLoadingRole, setIsLoadingRole] = useState(false); const [loadError, setLoadError] = useState(null); const loadedRoleIdRef = useRef(null); const permissions = useAppSelector((state) => state.auth.permissions); const [selectedPermissions, setSelectedPermissions] = useState< Array<{ resource: string; action: string }> >([]); const { register, handleSubmit, setValue, reset, setError, clearErrors, formState: { errors }, } = useForm({ resolver: zodResolver(editPlatformRoleSchema), defaultValues: { permissions: [], }, }); // Build available resources and actions based on user permissions const availableResourcesAndActions = useMemo(() => { const resourceMap = new Map>(); permissions.forEach((perm) => { const { resource, action } = perm; if (EXCLUDED_RESOURCES.has(resource)) return; if (resource === "*") { ALL_RESOURCES.forEach((res) => { if (EXCLUDED_RESOURCES.has(res)) return; if (!resourceMap.has(res)) { resourceMap.set(res, new Set()); } const actions = resourceMap.get(res)!; if (action === "*") { ALL_ACTIONS.forEach((act) => actions.add(act)); } else { actions.add(action); } }); } else { if (!resourceMap.has(resource)) { resourceMap.set(resource, new Set()); } const actions = resourceMap.get(resource)!; if (action === "*") { ALL_ACTIONS.forEach((act) => actions.add(act)); } else { actions.add(action); } } }); return resourceMap; }, [permissions]); // Handle permission checkbox change const handlePermissionChange = ( resource: string, action: string, checked: boolean, ) => { setSelectedPermissions((prev) => { const newPerms = [...prev]; if (checked) { if ( !newPerms.some((p) => p.resource === resource && p.action === action) ) { newPerms.push({ resource, action }); } } else { return newPerms.filter( (p) => !(p.resource === resource && p.action === action), ); } return newPerms; }); }; useEffect(() => { setValue( "permissions", selectedPermissions.length > 0 ? selectedPermissions : [], ); }, [selectedPermissions, setValue]); // Load role details useEffect(() => { if (isOpen && roleId) { if (loadedRoleIdRef.current !== roleId) { const loadRoleData = async () => { try { setIsLoadingRole(true); setLoadError(null); clearErrors(); const role = await onLoadRole(roleId); loadedRoleIdRef.current = roleId; const rolePermissions = role.permissions || []; setSelectedPermissions(rolePermissions); setValue("permissions", rolePermissions); reset({ name: role.name, code: role.code, description: role.description || "", permissions: rolePermissions, }); } catch (err: any) { setLoadError( err?.response?.data?.error?.message || "Failed to load role details", ); } finally { setIsLoadingRole(false); } }; loadRoleData(); } } else if (!isOpen) { loadedRoleIdRef.current = null; setSelectedPermissions([]); reset({ name: "", code: "", description: "", permissions: [], }); setLoadError(null); clearErrors(); } }, [isOpen, roleId, onLoadRole, reset, clearErrors, setValue]); const handleFormSubmit = async ( data: EditPlatformRoleFormData, ): Promise => { if (!roleId) return; clearErrors(); try { const { code, ...rest } = data; await onSubmit(roleId, rest as UpdateRoleRequest); } catch (error: any) { 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 EditPlatformRoleFormData, { type: "server", message: detail.message, }); } }, ); } else { 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 update role. Please try again."; setError("root", { type: "server", message: typeof errorMessage === "string" ? errorMessage : "Failed to update role. Please try again.", }); } throw error; } }; return ( Cancel {isLoading ? "Updating..." : "Update Role"} } > {isLoadingRole && (
)} {errors.root && (

{errors.root.message}

)} {loadError && (

{loadError}

)} {!isLoadingRole && (

Permissions

Select allowed actions for this role by platform service.

{errors.permissions && (

{errors.permissions.message}

)}
{["View", "Create", "Edit", "Delete"].map((action) => ( ))} {Array.from(availableResourcesAndActions.entries()).map( ([resource, actions]) => ( {["read", "create", "update", "delete"].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" />
)}
); }; export interface PlatformRolesTableRef { openNewModal: () => void; refresh: () => void; } export const PlatformRolesTable = forwardRef( (_, ref): ReactElement => { const tenantIdFromAuth = useAppSelector((state) => state.auth.tenantId); const { canCreate, canUpdate } = usePermissions(); const [roles, setRoles] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [isCreateOpen, setIsCreateOpen] = useState(false); const [isCreating, setIsCreating] = useState(false); // Pagination state const [currentPage, setCurrentPage] = useState(1); const [limit, setLimit] = useState(10); const [pagination, setPagination] = useState<{ page: number; limit: number; total: number; totalPages: number; hasMore: boolean; }>({ page: 1, limit: 10, total: 0, totalPages: 1, hasMore: false, }); // Filter and Search state const [orderBy, setOrderBy] = useState(null); const [search, setSearch] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(""); // Modal state const [editModalOpen, setEditModalOpen] = useState(false); // const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [selectedRoleId, setSelectedRoleId] = useState(null); // const [selectedRoleName, setSelectedRoleName] = useState(""); const [isUpdating, setIsUpdating] = useState(false); // const [isDeleting, setIsDeleting] = useState(false); // Expose methods to parent useImperativeHandle(ref, () => ({ openNewModal: () => setIsCreateOpen(true), refresh: () => fetchPlatformRoles(currentPage, limit, orderBy, debouncedSearch), })); const fetchPlatformRoles = async ( page: number, itemsPerPage: number, sortBy: string[] | null = null, searchQuery: string | null = null, ) => { try { setIsLoading(true); setError(null); // Load roles filtered by scope=platform const response = await roleService.getAll( page, itemsPerPage, sortBy, searchQuery, "platform", ); if (response.success) { setRoles(response.data); setPagination(response.pagination); } else { setError("Failed to load platform roles"); } } catch (err: any) { setError( err?.response?.data?.error?.message || "Failed to load platform roles", ); } finally { setIsLoading(false); } }; // Debounce search useEffect(() => { const timer = setTimeout(() => { setDebouncedSearch(search); if (search) setCurrentPage(1); }, 500); return () => clearTimeout(timer); }, [search]); useEffect(() => { fetchPlatformRoles(currentPage, limit, orderBy, debouncedSearch); }, [currentPage, limit, orderBy, debouncedSearch]); const handleCreateRole = async (data: CreateRoleRequest) => { try { setIsCreating(true); const payload = { ...data, scope: "platform", tenant_id: tenantIdFromAuth || "00000000-0000-0000-0000-000000000001", }; const response = await roleService.create(payload); showToast.success( response.message || "Platform role created successfully", ); setIsCreateOpen(false); fetchPlatformRoles(currentPage, limit, orderBy, debouncedSearch); } catch (err: any) { throw err; } finally { setIsCreating(false); } }; const handleUpdateRole = async (id: string, data: UpdateRoleRequest) => { try { setIsUpdating(true); const payload = { ...data, scope: "platform", tenant_id: tenantIdFromAuth || "00000000-0000-0000-0000-000000000001", }; const response = await roleService.update(id, payload); showToast.success( response.message || "Platform role updated successfully", ); setEditModalOpen(false); setSelectedRoleId(null); fetchPlatformRoles(currentPage, limit, orderBy, debouncedSearch); } catch (err: any) { throw err; } finally { setIsUpdating(false); } }; // const handleConfirmDelete = async () => { // if (!selectedRoleId) return; // try { // setIsDeleting(true); // await roleService.delete(selectedRoleId); // showToast.success("Platform role deleted successfully"); // // setDeleteModalOpen(false); // setSelectedRoleId(null); // setSelectedRoleName(""); // fetchPlatformRoles(currentPage, limit, orderBy, debouncedSearch); // } catch (err: any) { // showToast.error( // err?.response?.data?.error?.message || // "Failed to delete platform role", // ); // } finally { // setIsDeleting(false); // } // }; const loadRole = async (id: string): Promise => { const response = await roleService.getById(id); return response.data; }; // Columns config const columns: Column[] = [ { key: "name", label: "Role Name", render: (role) => ( {role.name} ), }, { key: "code", label: "Role Code", render: (role) => , }, { key: "description", label: "Description", render: (role) => ( {role.description || "N/A"} ), }, { key: "user_count", label: "Assigned Users", render: (role) => ( {role.user_count || 0} ), }, { key: "created_at", label: "Created Date", render: (role) => ( {formatDate(role.created_at)} ), }, { key: "actions", label: "Actions", align: "right", render: (role) => (
{ setSelectedRoleId(role.id); setEditModalOpen(true); } : undefined } // onDelete={ // canDelete("roles") // ? () => { // setSelectedRoleId(role.id); // setSelectedRoleName(role.name); // setDeleteModalOpen(true); // } // : undefined // } />
), }, ]; return ( <>
{ setOrderBy(value as string[] | null); setCurrentPage(1); }} placeholder="Default" showIcon icon={} />
{canCreate("roles") && ( setIsCreateOpen(true)} > New Platform Role )}
role.id} emptyMessage="No platform roles found" isLoading={isLoading} error={error} /> {pagination.total > 0 && ( { setCurrentPage(page); }} onLimitChange={(newLimit: number) => { setLimit(newLimit); setCurrentPage(1); }} /> )} setIsCreateOpen(false)} onSubmit={handleCreateRole} isLoading={isCreating} /> { setEditModalOpen(false); setSelectedRoleId(null); }} roleId={selectedRoleId} onLoadRole={loadRole} onSubmit={handleUpdateRole} isLoading={isUpdating} /> {/* { setDeleteModalOpen(false); setSelectedRoleId(null); setSelectedRoleName(""); }} onConfirm={handleConfirmDelete} title="Delete Platform Role" message="Are you sure you want to delete this platform role? This action cannot be undone." itemName={selectedRoleName} isLoading={isDeleting} /> */} ); }, ); PlatformRolesTable.displayName = "PlatformRolesTable";