feat: Refactor user role assignments in modals to use role_module_assignments with improved validation and module association handling.
This commit is contained in:
parent
083d10fdff
commit
320277b536
@ -9,6 +9,7 @@ import {
|
||||
FormField,
|
||||
FormSelect,
|
||||
PaginatedSelect,
|
||||
MultiselectPaginatedSelect,
|
||||
PrimaryButton,
|
||||
SecondaryButton,
|
||||
} from "@/components/shared";
|
||||
@ -20,6 +21,11 @@ import { supplierService } from "@/services/supplier-service";
|
||||
import { useAppSelector } from "@/hooks/redux-hooks";
|
||||
import type { User } from "@/types/user";
|
||||
|
||||
const assignmentSchema = z.object({
|
||||
role_id: z.string().min(1, "Role is required"),
|
||||
module_ids: z.array(z.string().min(1)).min(1, "At least one module is required"),
|
||||
});
|
||||
|
||||
// Validation schema
|
||||
const editUserSchema = z.object({
|
||||
email: z.email({ message: "Please enter a valid email address" }),
|
||||
@ -29,24 +35,22 @@ const editUserSchema = z.object({
|
||||
message: "Status is required",
|
||||
}),
|
||||
tenant_id: z.string().min(1, "Tenant 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" }
|
||||
),
|
||||
role_module_assignments: z.array(assignmentSchema).min(1, "At least one role assignment is required")
|
||||
.superRefine((assignments, ctx) => {
|
||||
const seenModules = new Set<string>();
|
||||
assignments.forEach((assignment, rowIndex) => {
|
||||
assignment.module_ids.forEach((moduleId) => {
|
||||
if (seenModules.has(moduleId)) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
path: [rowIndex, "module_ids"],
|
||||
message: "A module can only be assigned to one role",
|
||||
});
|
||||
}
|
||||
seenModules.add(moduleId);
|
||||
});
|
||||
});
|
||||
}),
|
||||
department_id: z.string().optional(),
|
||||
designation_id: z.string().optional(),
|
||||
category: z.enum(["tenant_user", "supplier_user"]).default("tenant_user"),
|
||||
@ -54,13 +58,16 @@ const editUserSchema = z.object({
|
||||
});
|
||||
|
||||
type EditUserFormData = z.infer<typeof editUserSchema>;
|
||||
type UpdateUserPayload = Omit<EditUserFormData, "role_module_assignments"> & {
|
||||
role_module_combinations: { role_id: string; module_id: string }[];
|
||||
};
|
||||
|
||||
interface EditUserModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
userId: string | null;
|
||||
onLoadUser: (id: string) => Promise<User>;
|
||||
onSubmit: (id: string, data: EditUserFormData) => Promise<void>;
|
||||
onSubmit: (id: string, data: UpdateUserPayload) => Promise<void>;
|
||||
isLoading?: boolean;
|
||||
defaultTenantId?: string; // If provided, automatically set tenant_id and hide tenant field
|
||||
}
|
||||
@ -101,7 +108,7 @@ export const EditUserModal = ({
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: "role_module_combinations",
|
||||
name: "role_module_assignments",
|
||||
});
|
||||
|
||||
const statusValue = watch("status");
|
||||
@ -236,23 +243,32 @@ export const EditUserModal = ({
|
||||
const roleOptions: { value: string; label: string }[] = [];
|
||||
const moduleOptions: { value: string; label: string }[] = [];
|
||||
|
||||
let initialCombinations = [{ role_id: "", module_id: null as string | null }];
|
||||
let initialAssignments = [{ role_id: "", module_ids: [] as string[] }];
|
||||
|
||||
if (user.role_module_combinations && user.role_module_combinations.length > 0) {
|
||||
initialCombinations = user.role_module_combinations.map(c => {
|
||||
const grouped = new Map<string, string[]>();
|
||||
user.role_module_combinations.forEach((c) => {
|
||||
if (c.role_id && c.role_name && !roleOptions.some(o => o.value === c.role_id)) {
|
||||
roleOptions.push({ value: c.role_id, label: c.role_name });
|
||||
}
|
||||
if (c.module_id && c.module_name && !moduleOptions.some(o => o.value === c.module_id)) {
|
||||
moduleOptions.push({ value: c.module_id, label: c.module_name });
|
||||
}
|
||||
return { role_id: c.role_id, module_id: c.module_id || null };
|
||||
if (c.module_id) {
|
||||
const existing = grouped.get(c.role_id) || [];
|
||||
existing.push(c.module_id);
|
||||
grouped.set(c.role_id, existing);
|
||||
}
|
||||
});
|
||||
initialAssignments = Array.from(grouped.entries()).map(([role_id, module_ids]) => ({
|
||||
role_id,
|
||||
module_ids,
|
||||
}));
|
||||
} else {
|
||||
// Fallback for older format
|
||||
const r_ids = user.roles?.map(r => r.id) || (user.role_id ? [user.role_id] : []);
|
||||
if (r_ids.length > 0) {
|
||||
initialCombinations = r_ids.map(id => ({ role_id: id, module_id: null }));
|
||||
initialAssignments = r_ids.map(id => ({ role_id: id, module_ids: [] }));
|
||||
}
|
||||
if (user.roles?.length) {
|
||||
user.roles.forEach(r => roleOptions.push({ value: r.id, label: r.name }));
|
||||
@ -294,7 +310,7 @@ export const EditUserModal = ({
|
||||
last_name: user.last_name,
|
||||
status: user.status,
|
||||
tenant_id: defaultTenantId || tenantId,
|
||||
role_module_combinations: initialCombinations,
|
||||
role_module_assignments: initialAssignments,
|
||||
department_id: departmentId,
|
||||
designation_id: designationId,
|
||||
category:
|
||||
@ -356,7 +372,7 @@ export const EditUserModal = ({
|
||||
last_name: "",
|
||||
status: "active",
|
||||
tenant_id: defaultTenantId || "",
|
||||
role_module_combinations: [{ role_id: "", module_id: null }],
|
||||
role_module_assignments: [{ role_id: "", module_ids: [] }],
|
||||
department_id: "",
|
||||
designation_id: "",
|
||||
category: "tenant_user",
|
||||
@ -394,13 +410,21 @@ export const EditUserModal = ({
|
||||
data.supplier_id = null;
|
||||
}
|
||||
|
||||
await onSubmit(userId, data);
|
||||
const { role_module_assignments, ...submitData } = data;
|
||||
const role_module_combinations = role_module_assignments.flatMap((row) =>
|
||||
row.module_ids.map((module_id) => ({ role_id: row.role_id, module_id })),
|
||||
);
|
||||
await onSubmit(userId, { ...submitData, role_module_combinations });
|
||||
} catch (error: any) {
|
||||
if (
|
||||
error?.response?.data?.details &&
|
||||
Array.isArray(error.response.data.details)
|
||||
) {
|
||||
const validationErrors = error.response.data.details;
|
||||
setError("root", {
|
||||
type: "server",
|
||||
message: validationErrors[0]?.message || "Validation failed",
|
||||
});
|
||||
validationErrors.forEach(
|
||||
(detail: { path: string; message: string }) => {
|
||||
if (
|
||||
@ -409,6 +433,7 @@ export const EditUserModal = ({
|
||||
detail.path === "last_name" ||
|
||||
detail.path === "status" ||
|
||||
detail.path === "tenant_id" ||
|
||||
detail.path === "role_module_assignments" ||
|
||||
detail.path === "role_module_combinations"
|
||||
) {
|
||||
setError(detail.path as keyof EditUserFormData, {
|
||||
@ -441,6 +466,17 @@ export const EditUserModal = ({
|
||||
}
|
||||
};
|
||||
|
||||
const getModuleError = (index: number): string | undefined => {
|
||||
const fieldError: any = errors.role_module_assignments?.[index]?.module_ids;
|
||||
if (!fieldError) return undefined;
|
||||
if (typeof fieldError.message === "string") return fieldError.message;
|
||||
if (Array.isArray(fieldError)) {
|
||||
const firstChild = fieldError.find((e: any) => e?.message);
|
||||
return firstChild?.message;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
@ -595,23 +631,23 @@ export const EditUserModal = ({
|
||||
<label className="text-sm font-medium text-gray-700">Role & Module Assignments</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => append({ role_id: "", module_id: null })}
|
||||
onClick={() => append({ role_id: "", module_ids: [] })}
|
||||
className="text-sm font-medium text-[#112868] hover:text-[#0b1c4a] flex items-center gap-1"
|
||||
>
|
||||
<Plus className="w-4 h-4" /> Add Assignment
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{(errors.role_module_combinations as any)?.message && (
|
||||
{(errors.role_module_assignments as any)?.message && (
|
||||
<p className="text-sm text-red-500 mb-2">
|
||||
{(errors.role_module_combinations as any).message}
|
||||
{(errors.role_module_assignments as any).message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
{fields.map((field, index) => {
|
||||
const roleIdValue = watch(`role_module_combinations.${index}.role_id`);
|
||||
const moduleIdValue = watch(`role_module_combinations.${index}.module_id`);
|
||||
const roleIdValue = watch(`role_module_assignments.${index}.role_id`);
|
||||
const moduleIdsValue = watch(`role_module_assignments.${index}.module_ids`) || [];
|
||||
|
||||
// Extract specific label if available from initial options
|
||||
const getRoleLabel = (val: string) => {
|
||||
@ -619,32 +655,35 @@ export const EditUserModal = ({
|
||||
return opt ? opt.label : undefined;
|
||||
};
|
||||
|
||||
const getModuleLabel = (val: string) => {
|
||||
const opt = initialModuleOptions.find(o => o.value === val);
|
||||
return opt ? opt.label : undefined;
|
||||
};
|
||||
const initialSelectedModules = moduleIdsValue
|
||||
.map((val) => {
|
||||
const opt = initialModuleOptions.find((o) => o.value === val);
|
||||
return opt ? { value: opt.value, label: opt.label } : null;
|
||||
})
|
||||
.filter((opt): opt is { value: string; label: string } => Boolean(opt));
|
||||
|
||||
return (
|
||||
<div key={field.id} className="flex gap-2 items-start border p-3 rounded-md bg-gray-50 relative">
|
||||
<div className="flex-1 grid grid-cols-2 gap-3">
|
||||
<PaginatedSelect
|
||||
label={`Role ${index + 1}`}
|
||||
label="Select Role"
|
||||
required
|
||||
placeholder="Select Role"
|
||||
value={roleIdValue || ""}
|
||||
onValueChange={(value) => setValue(`role_module_combinations.${index}.role_id`, value, { shouldValidate: true })}
|
||||
onValueChange={(value) => setValue(`role_module_assignments.${index}.role_id`, value, { shouldValidate: true })}
|
||||
onLoadOptions={loadRoles}
|
||||
initialOption={roleIdValue ? { value: roleIdValue, label: getRoleLabel(roleIdValue) || roleIdValue } : undefined}
|
||||
error={errors.role_module_combinations?.[index]?.role_id?.message}
|
||||
error={errors.role_module_assignments?.[index]?.role_id?.message}
|
||||
/>
|
||||
<PaginatedSelect
|
||||
label="Module (Optional)"
|
||||
placeholder="Select Module"
|
||||
value={moduleIdValue || ""}
|
||||
onValueChange={(value) => setValue(`role_module_combinations.${index}.module_id`, value, { shouldValidate: true })}
|
||||
<MultiselectPaginatedSelect
|
||||
label="Select Modules"
|
||||
required
|
||||
placeholder="Select Modules"
|
||||
value={moduleIdsValue}
|
||||
onValueChange={(value) => setValue(`role_module_assignments.${index}.module_ids`, value, { shouldValidate: true })}
|
||||
onLoadOptions={loadModules}
|
||||
initialOption={moduleIdValue ? { value: moduleIdValue, label: getModuleLabel(moduleIdValue) || moduleIdValue } : undefined}
|
||||
error={errors.role_module_combinations?.[index]?.module_id?.message}
|
||||
initialOptions={initialSelectedModules}
|
||||
error={getModuleError(index)}
|
||||
/>
|
||||
</div>
|
||||
{fields.length > 1 && (
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
FormField,
|
||||
FormSelect,
|
||||
PaginatedSelect,
|
||||
MultiselectPaginatedSelect,
|
||||
PrimaryButton,
|
||||
SecondaryButton,
|
||||
} from "@/components/shared";
|
||||
@ -19,41 +20,36 @@ import { moduleService } from "@/services/module-service";
|
||||
import { supplierService } from "@/services/supplier-service";
|
||||
import { useAppSelector } from "@/hooks/redux-hooks";
|
||||
|
||||
// Validation schema
|
||||
const assignmentSchema = z.object({
|
||||
role_id: z.string().min(1, "Role is required"),
|
||||
module_ids: z.array(z.string().min(1)).min(1, "At least one module is required"),
|
||||
});
|
||||
|
||||
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"),
|
||||
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" }
|
||||
),
|
||||
status: z.enum(["active", "suspended", "deleted"], { message: "Status is required" }),
|
||||
auth_provider: z.enum(["local"], { message: "Auth provider is required" }),
|
||||
role_module_assignments: z.array(assignmentSchema).min(1, "At least one role assignment is required")
|
||||
.superRefine((assignments, ctx) => {
|
||||
const used = new Set<string>();
|
||||
assignments.forEach((assignment, rowIndex) => {
|
||||
assignment.module_ids.forEach((moduleId) => {
|
||||
if (used.has(moduleId)) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
path: [rowIndex, "module_ids"],
|
||||
message: "A module can only be assigned to one role",
|
||||
});
|
||||
}
|
||||
used.add(moduleId);
|
||||
});
|
||||
});
|
||||
}),
|
||||
department_id: z.string().optional(),
|
||||
designation_id: z.string().optional(),
|
||||
category: z.enum(["tenant_user", "supplier_user"]).default("tenant_user"),
|
||||
@ -65,13 +61,16 @@ const newUserSchema = z
|
||||
});
|
||||
|
||||
type NewUserFormData = z.infer<typeof newUserSchema>;
|
||||
type CreateUserPayload = Omit<NewUserFormData, "confirmPassword" | "role_module_assignments"> & {
|
||||
role_module_combinations: { role_id: string; module_id: string }[];
|
||||
};
|
||||
|
||||
interface NewUserModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: Omit<NewUserFormData, "confirmPassword">) => Promise<void>;
|
||||
onSubmit: (data: CreateUserPayload) => Promise<void>;
|
||||
isLoading?: boolean;
|
||||
defaultTenantId?: string; // If provided, filter roles by tenant_id
|
||||
defaultTenantId?: string;
|
||||
}
|
||||
|
||||
const statusOptions = [
|
||||
@ -87,31 +86,21 @@ export const NewUserModal = ({
|
||||
isLoading = false,
|
||||
defaultTenantId,
|
||||
}: NewUserModalProps): ReactElement | null => {
|
||||
const {
|
||||
control,
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch,
|
||||
reset,
|
||||
setError,
|
||||
clearErrors,
|
||||
formState: { errors },
|
||||
} = useForm<NewUserFormData>({
|
||||
// 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 { control, register, handleSubmit, setValue, watch, reset, setError, clearErrors, formState: { errors } } =
|
||||
useForm<NewUserFormData>({
|
||||
resolver: zodResolver(newUserSchema) as any,
|
||||
defaultValues: {
|
||||
role_module_assignments: [{ role_id: "", module_ids: [] }],
|
||||
department_id: "",
|
||||
designation_id: "",
|
||||
category: "tenant_user",
|
||||
supplier_id: null,
|
||||
},
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: "role_module_combinations",
|
||||
name: "role_module_assignments",
|
||||
});
|
||||
|
||||
const statusValue = watch("status");
|
||||
@ -123,7 +112,6 @@ export const NewUserModal = ({
|
||||
const roles = useAppSelector((state) => state.auth.roles);
|
||||
const isSuperAdmin = roles.includes("super_admin");
|
||||
|
||||
// Reset form when modal closes
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
reset({
|
||||
@ -134,7 +122,7 @@ export const NewUserModal = ({
|
||||
last_name: "",
|
||||
status: "active",
|
||||
auth_provider: "local",
|
||||
role_module_combinations: [{ role_id: "", module_id: null }],
|
||||
role_module_assignments: [{ role_id: "", module_ids: [] }],
|
||||
department_id: "",
|
||||
designation_id: "",
|
||||
category: "tenant_user",
|
||||
@ -144,158 +132,90 @@ export const NewUserModal = ({
|
||||
}
|
||||
}, [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,
|
||||
})),
|
||||
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,
|
||||
})),
|
||||
options: response.data.map((module) => ({ value: module.id, label: module.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 loadSuppliers = async (page: number, limit: number) => {
|
||||
const response = await supplierService.list({
|
||||
tenantId: defaultTenantId,
|
||||
limit,
|
||||
offset: (page - 1) * limit,
|
||||
});
|
||||
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,
|
||||
},
|
||||
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<void> => {
|
||||
clearErrors();
|
||||
try {
|
||||
const { confirmPassword, ...submitData } = data;
|
||||
// Normalize empty strings to null for optional UUID fields
|
||||
const { confirmPassword, role_module_assignments, ...submitData } = data;
|
||||
const role_module_combinations = role_module_assignments.flatMap((row) =>
|
||||
row.module_ids.map((module_id) => ({ role_id: row.role_id, module_id })),
|
||||
);
|
||||
|
||||
if (!submitData.department_id) submitData.department_id = undefined;
|
||||
if (!submitData.designation_id) submitData.designation_id = undefined;
|
||||
if (submitData.category === "tenant_user") submitData.supplier_id = null;
|
||||
else if (!submitData.supplier_id) submitData.supplier_id = null;
|
||||
|
||||
// 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);
|
||||
await onSubmit({ ...submitData, role_module_combinations });
|
||||
} 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.",
|
||||
});
|
||||
if (error?.response?.data?.details && Array.isArray(error.response.data.details)) {
|
||||
const first = error.response.data.details[0];
|
||||
setError("root", { type: "server", message: first?.message || "Validation failed" });
|
||||
return;
|
||||
}
|
||||
const message =
|
||||
error?.response?.data?.error?.message ||
|
||||
error?.response?.data?.message ||
|
||||
error?.message ||
|
||||
"Failed to create user. Please try again.";
|
||||
setError("root", { type: "server", message });
|
||||
}
|
||||
};
|
||||
|
||||
const getModuleError = (index: number): string | undefined => {
|
||||
const fieldError: any = errors.role_module_assignments?.[index]?.module_ids;
|
||||
if (!fieldError) return undefined;
|
||||
if (typeof fieldError.message === "string") return fieldError.message;
|
||||
if (Array.isArray(fieldError)) {
|
||||
const firstChild = fieldError.find((e: any) => e?.message);
|
||||
return firstChild?.message;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
@ -305,12 +225,7 @@ export const NewUserModal = ({
|
||||
maxWidth="md"
|
||||
footer={
|
||||
<>
|
||||
<SecondaryButton
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
disabled={isLoading}
|
||||
className="px-4 py-2.5 text-sm"
|
||||
>
|
||||
<SecondaryButton type="button" onClick={onClose} disabled={isLoading} className="px-4 py-2.5 text-sm">
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
<PrimaryButton
|
||||
@ -326,190 +241,116 @@ export const NewUserModal = ({
|
||||
}
|
||||
>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit as any)} className="p-5">
|
||||
{/* General Error Display */}
|
||||
{errors.root && (
|
||||
<div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md mb-4">
|
||||
<p className="text-sm text-[#ef4444]">{errors.root.message}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-0">
|
||||
{/* Email */}
|
||||
<FormField
|
||||
label="Email"
|
||||
type="email"
|
||||
<FormField label="Email" type="email" required placeholder="Enter email address" error={errors.email?.message} {...register("email")} />
|
||||
<div className="grid grid-cols-2 gap-5 pb-4">
|
||||
<FormField label="First Name" required placeholder="Enter first name" error={errors.first_name?.message} {...register("first_name")} />
|
||||
<FormField label="Last Name" required placeholder="Enter last name" error={errors.last_name?.message} {...register("last_name")} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-5 pb-4">
|
||||
<FormField label="Password" type="password" required placeholder="Enter password" error={errors.password?.message} {...register("password")} />
|
||||
<FormField label="Confirm Password" type="password" required placeholder="Confirm password" error={errors.confirmPassword?.message} {...register("confirmPassword")} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-5 pb-4">
|
||||
<PaginatedSelect label="Department" placeholder="Select Department" value={departmentIdValue || ""} onValueChange={(value) => setValue("department_id", value)} onLoadOptions={loadDepartments} error={errors.department_id?.message} />
|
||||
<PaginatedSelect label="Designation" placeholder="Select Designation" value={designationIdValue || ""} onValueChange={(value) => setValue("designation_id", value)} onLoadOptions={loadDesignations} error={errors.designation_id?.message} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-5 pb-4">
|
||||
<FormSelect
|
||||
label="User Category"
|
||||
required
|
||||
placeholder="Enter email address"
|
||||
error={errors.email?.message}
|
||||
{...register("email")}
|
||||
placeholder="Select Category"
|
||||
options={[{ value: "tenant_user", label: "Tenant User" }, { value: "supplier_user", label: "Supplier User" }]}
|
||||
value={categoryValue || "tenant_user"}
|
||||
onValueChange={(value) => {
|
||||
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}
|
||||
/>
|
||||
|
||||
{/* First Name and Last Name Row */}
|
||||
<div className="grid grid-cols-2 gap-5 pb-4">
|
||||
<FormField
|
||||
label="First Name"
|
||||
required
|
||||
placeholder="Enter first name"
|
||||
error={errors.first_name?.message}
|
||||
{...register("first_name")}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
label="Last Name"
|
||||
required
|
||||
placeholder="Enter last name"
|
||||
error={errors.last_name?.message}
|
||||
{...register("last_name")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Password and Confirm Password Row */}
|
||||
<div className="grid grid-cols-2 gap-5 pb-4">
|
||||
<FormField
|
||||
label="Password"
|
||||
type="password"
|
||||
required
|
||||
placeholder="Enter password"
|
||||
error={errors.password?.message}
|
||||
{...register("password")}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
label="Confirm Password"
|
||||
type="password"
|
||||
required
|
||||
placeholder="Confirm password"
|
||||
error={errors.confirmPassword?.message}
|
||||
{...register("confirmPassword")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-5 pb-4">
|
||||
{categoryValue === "supplier_user" && (
|
||||
<PaginatedSelect
|
||||
label="Department"
|
||||
placeholder="Select Department"
|
||||
value={departmentIdValue || ""}
|
||||
onValueChange={(value) => setValue("department_id", value)}
|
||||
onLoadOptions={loadDepartments}
|
||||
error={errors.department_id?.message}
|
||||
/>
|
||||
|
||||
<PaginatedSelect
|
||||
label="Designation"
|
||||
placeholder="Select Designation"
|
||||
value={designationIdValue || ""}
|
||||
onValueChange={(value) => setValue("designation_id", value)}
|
||||
onLoadOptions={loadDesignations}
|
||||
error={errors.designation_id?.message}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* User Category */}
|
||||
<div className="grid grid-cols-2 gap-5 pb-4">
|
||||
<FormSelect
|
||||
label="User Category"
|
||||
label="Linked Supplier"
|
||||
required
|
||||
placeholder="Select Category"
|
||||
options={[
|
||||
{ value: "tenant_user", label: "Tenant User" },
|
||||
{ value: "supplier_user", label: "Supplier User" },
|
||||
]}
|
||||
value={categoryValue || "tenant_user"}
|
||||
onValueChange={(value) => {
|
||||
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}
|
||||
placeholder="Select Supplier"
|
||||
value={supplierIdValue || ""}
|
||||
onValueChange={(value) => setValue("supplier_id", value)}
|
||||
onLoadOptions={loadSuppliers}
|
||||
error={errors.supplier_id?.message}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="pb-4">
|
||||
<FormSelect
|
||||
label="Status"
|
||||
required
|
||||
placeholder="Select Status"
|
||||
options={statusOptions}
|
||||
value={statusValue}
|
||||
onValueChange={(value) => setValue("status", value as "active" | "suspended" | "deleted")}
|
||||
error={errors.status?.message}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{categoryValue === "supplier_user" && (
|
||||
<PaginatedSelect
|
||||
label="Linked Supplier"
|
||||
required
|
||||
placeholder="Select Supplier"
|
||||
value={supplierIdValue || ""}
|
||||
onValueChange={(value) => setValue("supplier_id", value)}
|
||||
onLoadOptions={loadSuppliers}
|
||||
error={errors.supplier_id?.message}
|
||||
/>
|
||||
)}
|
||||
<div className="mt-2 mb-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<label className="text-sm font-medium text-gray-700">Role & Module Assignments</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => append({ role_id: "", module_ids: [] })}
|
||||
className="text-sm font-medium text-[#112868] hover:text-[#0b1c4a] flex items-center gap-1"
|
||||
>
|
||||
<Plus className="w-4 h-4" /> Add Assignment
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="pb-4">
|
||||
<FormSelect
|
||||
label="Status"
|
||||
required
|
||||
placeholder="Select Status"
|
||||
options={statusOptions}
|
||||
value={statusValue}
|
||||
onValueChange={(value) =>
|
||||
setValue("status", value as "active" | "suspended" | "deleted")
|
||||
}
|
||||
error={errors.status?.message}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 mb-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<label className="text-sm font-medium text-gray-700">Role & Module Assignments</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => append({ role_id: "", module_id: null })}
|
||||
className="text-sm font-medium text-[#112868] hover:text-[#0b1c4a] flex items-center gap-1"
|
||||
>
|
||||
<Plus className="w-4 h-4" /> Add Assignment
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{(errors.role_module_combinations as any)?.message && (
|
||||
<p className="text-sm text-red-500 mb-2">
|
||||
{(errors.role_module_combinations as any).message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
{fields.map((field, index) => {
|
||||
const roleIdValue = watch(`role_module_combinations.${index}.role_id`);
|
||||
const moduleIdValue = watch(`role_module_combinations.${index}.module_id`);
|
||||
|
||||
return (
|
||||
<div key={field.id} className="flex gap-2 items-start border p-3 rounded-md bg-gray-50 relative">
|
||||
<div className="flex-1 grid grid-cols-2 gap-3">
|
||||
<PaginatedSelect
|
||||
label={`Role ${index + 1}`}
|
||||
required
|
||||
placeholder="Select Role"
|
||||
value={roleIdValue || ""}
|
||||
onValueChange={(value) => setValue(`role_module_combinations.${index}.role_id`, value, { shouldValidate: true })}
|
||||
onLoadOptions={loadRoles}
|
||||
error={errors.role_module_combinations?.[index]?.role_id?.message}
|
||||
/>
|
||||
<PaginatedSelect
|
||||
label="Module (Optional)"
|
||||
placeholder="Select Module"
|
||||
value={moduleIdValue || ""}
|
||||
onValueChange={(value) => setValue(`role_module_combinations.${index}.module_id`, value, { shouldValidate: true })}
|
||||
onLoadOptions={loadModules}
|
||||
error={errors.role_module_combinations?.[index]?.module_id?.message}
|
||||
/>
|
||||
</div>
|
||||
{fields.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => remove(index)}
|
||||
className="mt-8 p-2 text-red-500 hover:bg-red-50 rounded-md transition-colors"
|
||||
title="Remove Assignment"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
{(errors.role_module_assignments as any)?.message && (
|
||||
<p className="text-sm text-red-500 mb-2">{(errors.role_module_assignments as any).message}</p>
|
||||
)}
|
||||
<div className="space-y-3">
|
||||
{fields.map((field, index) => {
|
||||
const roleIdValue = watch(`role_module_assignments.${index}.role_id`);
|
||||
const moduleIdsValue = watch(`role_module_assignments.${index}.module_ids`) || [];
|
||||
return (
|
||||
<div key={field.id} className="flex gap-2 items-start border p-3 rounded-md bg-gray-50 relative">
|
||||
<div className="flex-1 grid grid-cols-2 gap-3">
|
||||
<PaginatedSelect
|
||||
label="Select Role"
|
||||
required
|
||||
placeholder="Select Role"
|
||||
value={roleIdValue || ""}
|
||||
onValueChange={(value) => setValue(`role_module_assignments.${index}.role_id`, value, { shouldValidate: true })}
|
||||
onLoadOptions={loadRoles}
|
||||
error={errors.role_module_assignments?.[index]?.role_id?.message}
|
||||
/>
|
||||
<MultiselectPaginatedSelect
|
||||
label="Select Modules"
|
||||
required
|
||||
placeholder="Select Modules"
|
||||
value={moduleIdsValue}
|
||||
onValueChange={(value) => setValue(`role_module_assignments.${index}.module_ids`, value, { shouldValidate: true })}
|
||||
onLoadOptions={loadModules}
|
||||
error={getModuleError(index)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{fields.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => remove(index)}
|
||||
className="mt-8 p-2 text-red-500 hover:bg-red-50 rounded-md transition-colors"
|
||||
title="Remove Assignment"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user