import { useEffect } from "react"; import type { ReactElement } from "react"; import { useForm, useFieldArray } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Plus, Trash2 } from "lucide-react"; import { Modal, FormField, FormSelect, PaginatedSelect, PrimaryButton, SecondaryButton, } from "@/components/shared"; import { roleService } from "@/services/role-service"; import { departmentService } from "@/services/department-service"; import { designationService } from "@/services/designation-service"; import { moduleService } from "@/services/module-service"; import { supplierService } from "@/services/supplier-service"; import { useAppSelector } from "@/hooks/redux-hooks"; // Validation schema const newUserSchema = z .object({ email: z.email({ message: "Please enter a valid email address" }), password: z .string() .min(1, "Password is required") .min(6, "Password must be at least 6 characters"), confirmPassword: z.string().min(1, "Confirm password is required"), first_name: z.string().min(1, "First name is required"), last_name: z.string().min(1, "Last name is required"), status: z.enum(["active", "suspended", "deleted"], { message: "Status is required", }), auth_provider: z.enum(["local"], { message: "Auth provider is required", }), role_module_combinations: z.array( z.object({ role_id: z.string().min(1, "Role is required"), module_id: z.string().nullable().optional(), }) ).min(1, "At least one role assignment is required") .refine( (combinations) => { const seen = new Set(); for (const combo of combinations) { const key = `${combo.role_id}-${combo.module_id || "global"}`; if (seen.has(key)) return false; seen.add(key); } return true; }, { message: "Duplicate role-module combinations are not allowed" } ), department_id: z.string().optional(), designation_id: z.string().optional(), category: z.enum(["tenant_user", "supplier_user"]).default("tenant_user"), supplier_id: z.string().optional().nullable(), }) .refine((data) => data.password === data.confirmPassword, { message: "Passwords don't match", path: ["confirmPassword"], }); type NewUserFormData = z.infer; interface NewUserModalProps { isOpen: boolean; onClose: () => void; onSubmit: (data: Omit) => Promise; isLoading?: boolean; defaultTenantId?: string; // If provided, filter roles by tenant_id } const statusOptions = [ { value: "active", label: "Active" }, { value: "suspended", label: "Suspended" }, { value: "deleted", label: "Deleted" }, ]; export const NewUserModal = ({ isOpen, onClose, onSubmit, isLoading = false, defaultTenantId, }: NewUserModalProps): ReactElement | null => { const { control, register, handleSubmit, setValue, watch, reset, setError, clearErrors, formState: { errors }, } = useForm({ // eslint-disable-next-line @typescript-eslint/no-explicit-any resolver: zodResolver(newUserSchema) as any, defaultValues: { role_module_combinations: [{ role_id: "", module_id: null }], department_id: "", designation_id: "", category: "tenant_user" as const, supplier_id: null, }, }); const { fields, append, remove } = useFieldArray({ control, name: "role_module_combinations", }); const statusValue = watch("status"); const departmentIdValue = watch("department_id"); const designationIdValue = watch("designation_id"); const categoryValue = watch("category"); const supplierIdValue = watch("supplier_id"); const roles = useAppSelector((state) => state.auth.roles); const isSuperAdmin = roles.includes("super_admin"); // Reset form when modal closes useEffect(() => { if (!isOpen) { reset({ email: "", password: "", confirmPassword: "", first_name: "", last_name: "", status: "active", auth_provider: "local", role_module_combinations: [{ role_id: "", module_id: null }], department_id: "", designation_id: "", category: "tenant_user", supplier_id: null, }); clearErrors(); } }, [isOpen, reset, clearErrors]); // Load roles for dropdown const loadRoles = async (page: number, limit: number) => { const response = defaultTenantId ? await roleService.getByTenant(defaultTenantId, page, limit) : await roleService.getAll(page, limit); return { options: response.data.map((role) => ({ value: role.id, label: role.name, })), pagination: response.pagination, }; }; const loadDepartments = async () => { const response = await departmentService.list(defaultTenantId, { active_only: true, }); return { options: response.data.map((dept) => ({ value: dept.id, label: dept.name, })), pagination: { page: 1, limit: response.data.length, total: response.data.length, totalPages: 1, hasMore: false, }, }; }; const loadDesignations = async () => { const response = await designationService.list(defaultTenantId, { active_only: true, }); return { options: response.data.map((desig) => ({ value: desig.id, label: desig.name, })), pagination: { page: 1, limit: response.data.length, total: response.data.length, totalPages: 1, hasMore: false, }, }; }; const loadModules = async (page: number, limit: number) => { const tenantId = isSuperAdmin ? defaultTenantId : undefined; const response = await moduleService.getAvailable(page, limit, tenantId); return { options: response.data.map((module) => ({ value: module.id, label: module.name, })), pagination: response.pagination, }; }; const loadSuppliers = async (page: number, limit: number) => { const response = await supplierService.list({ tenantId: defaultTenantId, limit, offset: (page - 1) * limit, }); const total = response.pagination?.total ?? response.data?.length ?? 0; return { options: (response.data || []).map((supplier: any) => ({ value: supplier.id, label: supplier.name, })), pagination: { page, limit, total, totalPages: Math.ceil(total / limit) || 1, hasMore: page * limit < total, }, }; }; const handleFormSubmit = async (data: NewUserFormData): Promise => { clearErrors(); try { const { confirmPassword, ...submitData } = data; // Normalize empty strings to null for optional UUID fields if (!submitData.department_id) submitData.department_id = undefined; if (!submitData.designation_id) submitData.designation_id = undefined; // If category is tenant_user, supplier_id should always be null if (submitData.category === "tenant_user") { submitData.supplier_id = null; } else if (!submitData.supplier_id) { submitData.supplier_id = null; } await onSubmit(submitData); } 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 === "email" || detail.path === "password" || detail.path === "first_name" || detail.path === "last_name" || detail.path === "status" || detail.path === "auth_provider" || detail.path === "role_module_combinations" ) { setError(detail.path as keyof NewUserFormData, { type: "server", message: detail.message, }); } }, ); } else { // Handle general errors // Check for nested error object with message property 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 user. Please try again."; setError("root", { type: "server", message: typeof errorMessage === "string" ? errorMessage : "Failed to create user. Please try again.", }); } } }; return ( Cancel {isLoading ? "Creating..." : "Create User"} } >
{/* General Error Display */} {errors.root && (

{errors.root.message}

)}
{/* Email */} {/* First Name and Last Name Row */}
{/* Password and Confirm Password Row */}
setValue("department_id", value)} onLoadOptions={loadDepartments} error={errors.department_id?.message} /> setValue("designation_id", value)} onLoadOptions={loadDesignations} error={errors.designation_id?.message} />
{/* User Category */}
{ const category = value as "tenant_user" | "supplier_user"; setValue("category", category, { shouldValidate: true }); if (category === "tenant_user") { setValue("supplier_id", null); } }} error={errors.category?.message} /> {categoryValue === "supplier_user" && ( setValue("supplier_id", value)} onLoadOptions={loadSuppliers} error={errors.supplier_id?.message} /> )}
setValue("status", value as "active" | "suspended" | "deleted") } error={errors.status?.message} />
{(errors.role_module_combinations as any)?.message && (

{(errors.role_module_combinations as any).message}

)}
{fields.map((field, index) => { const roleIdValue = watch(`role_module_combinations.${index}.role_id`); const moduleIdValue = watch(`role_module_combinations.${index}.module_id`); return (
setValue(`role_module_combinations.${index}.role_id`, value, { shouldValidate: true })} onLoadOptions={loadRoles} error={errors.role_module_combinations?.[index]?.role_id?.message} /> setValue(`role_module_combinations.${index}.module_id`, value, { shouldValidate: true })} onLoadOptions={loadModules} error={errors.role_module_combinations?.[index]?.module_id?.message} />
{fields.length > 1 && ( )}
); })}
); };