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