feat: Enable granular user role assignments with optional module association using role_module_combinations.
This commit is contained in:
parent
a6ef7e6bee
commit
20d802555e
@ -1,15 +1,14 @@
|
|||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm, useFieldArray } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2, Plus, Trash2 } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
FormField,
|
FormField,
|
||||||
FormSelect,
|
FormSelect,
|
||||||
PaginatedSelect,
|
PaginatedSelect,
|
||||||
MultiselectPaginatedSelect,
|
|
||||||
PrimaryButton,
|
PrimaryButton,
|
||||||
SecondaryButton,
|
SecondaryButton,
|
||||||
} from "@/components/shared";
|
} from "@/components/shared";
|
||||||
@ -30,10 +29,26 @@ 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_ids: z.array(z.string()).min(1, "At least one role 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(),
|
department_id: z.string().optional(),
|
||||||
designation_id: z.string().optional(),
|
designation_id: z.string().optional(),
|
||||||
module_ids: z.array(z.string()).optional(),
|
|
||||||
category: z.enum(["tenant_user", "supplier_user"]).default("tenant_user"),
|
category: z.enum(["tenant_user", "supplier_user"]).default("tenant_user"),
|
||||||
supplier_id: z.string().optional().nullable(),
|
supplier_id: z.string().optional().nullable(),
|
||||||
});
|
});
|
||||||
@ -70,6 +85,7 @@ export const EditUserModal = ({
|
|||||||
const loadedUserIdRef = useRef<string | null>(null);
|
const loadedUserIdRef = useRef<string | null>(null);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
control,
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
setValue,
|
setValue,
|
||||||
@ -79,15 +95,18 @@ export const EditUserModal = ({
|
|||||||
clearErrors,
|
clearErrors,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<EditUserFormData>({
|
} = useForm<EditUserFormData>({
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// @ts-ignore
|
||||||
resolver: zodResolver(editUserSchema) as any,
|
resolver: zodResolver(editUserSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: "role_module_combinations",
|
||||||
});
|
});
|
||||||
|
|
||||||
const statusValue = watch("status");
|
const statusValue = watch("status");
|
||||||
const roleIdsValue = watch("role_ids");
|
|
||||||
const departmentIdValue = watch("department_id");
|
const departmentIdValue = watch("department_id");
|
||||||
const designationIdValue = watch("designation_id");
|
const designationIdValue = watch("designation_id");
|
||||||
const moduleIdsValue = watch("module_ids");
|
|
||||||
const categoryValue = watch("category");
|
const categoryValue = watch("category");
|
||||||
const supplierIdValue = watch("supplier_id");
|
const supplierIdValue = watch("supplier_id");
|
||||||
|
|
||||||
@ -213,14 +232,37 @@ export const EditUserModal = ({
|
|||||||
loadedUserIdRef.current = userId;
|
loadedUserIdRef.current = userId;
|
||||||
|
|
||||||
const tenantId = user.tenant?.id || user.tenant_id || "";
|
const tenantId = user.tenant?.id || user.tenant_id || "";
|
||||||
const roleIds =
|
|
||||||
user.roles?.map((r) => r.id) ||
|
const roleOptions: { value: string; label: string }[] = [];
|
||||||
(user.role_id ? [user.role_id] : []);
|
const moduleOptions: { value: string; label: string }[] = [];
|
||||||
const roleOptions =
|
|
||||||
user.roles?.map((r) => ({ value: r.id, label: r.name })) ||
|
let initialCombinations = [{ role_id: "", module_id: null as string | null }];
|
||||||
(user.role?.id
|
|
||||||
? [{ value: user.role.id, label: user.role.name }]
|
if (user.role_module_combinations && user.role_module_combinations.length > 0) {
|
||||||
: []);
|
initialCombinations = user.role_module_combinations.map(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 };
|
||||||
|
});
|
||||||
|
} 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 }));
|
||||||
|
}
|
||||||
|
if (user.roles?.length) {
|
||||||
|
user.roles.forEach(r => roleOptions.push({ value: r.id, label: r.name }));
|
||||||
|
} else if (user.role?.id) {
|
||||||
|
roleOptions.push({ value: user.role.id, label: user.role.name });
|
||||||
|
}
|
||||||
|
if (user.modules?.length) {
|
||||||
|
user.modules.forEach(m => moduleOptions.push({ value: m.id, label: m.name }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const departmentId =
|
const departmentId =
|
||||||
user.department?.id || user.department_id || "";
|
user.department?.id || user.department_id || "";
|
||||||
@ -252,10 +294,9 @@ 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_ids: roleIds,
|
role_module_combinations: initialCombinations,
|
||||||
department_id: departmentId,
|
department_id: departmentId,
|
||||||
designation_id: designationId,
|
designation_id: designationId,
|
||||||
module_ids: user.modules?.map((m) => m.id) || [],
|
|
||||||
category:
|
category:
|
||||||
(user.category as "tenant_user" | "supplier_user") ||
|
(user.category as "tenant_user" | "supplier_user") ||
|
||||||
"tenant_user",
|
"tenant_user",
|
||||||
@ -285,10 +326,8 @@ export const EditUserModal = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.modules) {
|
if (moduleOptions.length > 0) {
|
||||||
setInitialModuleOptions(
|
setInitialModuleOptions(moduleOptions);
|
||||||
user.modules.map((m) => ({ value: m.id, label: m.name })),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (defaultTenantId) {
|
if (defaultTenantId) {
|
||||||
@ -317,10 +356,9 @@ export const EditUserModal = ({
|
|||||||
last_name: "",
|
last_name: "",
|
||||||
status: "active",
|
status: "active",
|
||||||
tenant_id: defaultTenantId || "",
|
tenant_id: defaultTenantId || "",
|
||||||
role_ids: [],
|
role_module_combinations: [{ role_id: "", module_id: null }],
|
||||||
department_id: "",
|
department_id: "",
|
||||||
designation_id: "",
|
designation_id: "",
|
||||||
module_ids: [],
|
|
||||||
category: "tenant_user",
|
category: "tenant_user",
|
||||||
supplier_id: null,
|
supplier_id: null,
|
||||||
});
|
});
|
||||||
@ -365,10 +403,19 @@ export const EditUserModal = ({
|
|||||||
const validationErrors = error.response.data.details;
|
const validationErrors = error.response.data.details;
|
||||||
validationErrors.forEach(
|
validationErrors.forEach(
|
||||||
(detail: { path: string; message: string }) => {
|
(detail: { path: string; message: string }) => {
|
||||||
setError(detail.path as keyof EditUserFormData, {
|
if (
|
||||||
type: "server",
|
detail.path === "email" ||
|
||||||
message: detail.message,
|
detail.path === "first_name" ||
|
||||||
});
|
detail.path === "last_name" ||
|
||||||
|
detail.path === "status" ||
|
||||||
|
detail.path === "tenant_id" ||
|
||||||
|
detail.path === "role_module_combinations"
|
||||||
|
) {
|
||||||
|
setError(detail.path as keyof EditUserFormData, {
|
||||||
|
type: "server",
|
||||||
|
message: detail.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -526,17 +573,7 @@ export const EditUserModal = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-5 pb-4">
|
<div className="pb-4">
|
||||||
<MultiselectPaginatedSelect
|
|
||||||
label="Assign Role"
|
|
||||||
required
|
|
||||||
placeholder="Select Roles"
|
|
||||||
value={roleIdsValue || []}
|
|
||||||
onValueChange={(value) => setValue("role_ids", value)}
|
|
||||||
onLoadOptions={loadRoles}
|
|
||||||
initialOptions={initialRoleOptions}
|
|
||||||
error={errors.role_ids?.message}
|
|
||||||
/>
|
|
||||||
<FormSelect
|
<FormSelect
|
||||||
label="Status"
|
label="Status"
|
||||||
required
|
required
|
||||||
@ -553,16 +590,77 @@ export const EditUserModal = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pb-4">
|
<div className="mt-2 mb-4">
|
||||||
<MultiselectPaginatedSelect
|
<div className="flex justify-between items-center mb-2">
|
||||||
label="Assign Modules"
|
<label className="text-sm font-medium text-gray-700">Role & Module Assignments</label>
|
||||||
placeholder="Select Modules"
|
<button
|
||||||
value={moduleIdsValue || []}
|
type="button"
|
||||||
onValueChange={(value) => setValue("module_ids", value)}
|
onClick={() => append({ role_id: "", module_id: null })}
|
||||||
onLoadOptions={loadModules}
|
className="text-sm font-medium text-[#112868] hover:text-[#0b1c4a] flex items-center gap-1"
|
||||||
initialOptions={initialModuleOptions}
|
>
|
||||||
error={errors.module_ids?.message}
|
<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`);
|
||||||
|
|
||||||
|
// Extract specific label if available from initial options
|
||||||
|
const getRoleLabel = (val: string) => {
|
||||||
|
const opt = initialRoleOptions.find(o => o.value === val);
|
||||||
|
return opt ? opt.label : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getModuleLabel = (val: string) => {
|
||||||
|
const opt = initialModuleOptions.find(o => o.value === val);
|
||||||
|
return opt ? opt.label : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
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}
|
||||||
|
initialOption={roleIdValue ? { value: roleIdValue, label: getRoleLabel(roleIdValue) || roleIdValue } : undefined}
|
||||||
|
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}
|
||||||
|
initialOption={moduleIdValue ? { value: moduleIdValue, label: getModuleLabel(moduleIdValue) || moduleIdValue } : undefined}
|
||||||
|
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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm, useFieldArray } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { Plus, Trash2 } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
FormField,
|
FormField,
|
||||||
FormSelect,
|
FormSelect,
|
||||||
PaginatedSelect,
|
PaginatedSelect,
|
||||||
MultiselectPaginatedSelect,
|
|
||||||
PrimaryButton,
|
PrimaryButton,
|
||||||
SecondaryButton,
|
SecondaryButton,
|
||||||
} from "@/components/shared";
|
} from "@/components/shared";
|
||||||
@ -36,10 +36,26 @@ const newUserSchema = z
|
|||||||
auth_provider: z.enum(["local"], {
|
auth_provider: z.enum(["local"], {
|
||||||
message: "Auth provider is required",
|
message: "Auth provider is required",
|
||||||
}),
|
}),
|
||||||
role_ids: z.array(z.string()).min(1, "At least one role 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(),
|
department_id: z.string().optional(),
|
||||||
designation_id: z.string().optional(),
|
designation_id: z.string().optional(),
|
||||||
module_ids: z.array(z.string()).optional(),
|
|
||||||
category: z.enum(["tenant_user", "supplier_user"]).default("tenant_user"),
|
category: z.enum(["tenant_user", "supplier_user"]).default("tenant_user"),
|
||||||
supplier_id: z.string().optional().nullable(),
|
supplier_id: z.string().optional().nullable(),
|
||||||
})
|
})
|
||||||
@ -72,6 +88,7 @@ export const NewUserModal = ({
|
|||||||
defaultTenantId,
|
defaultTenantId,
|
||||||
}: NewUserModalProps): ReactElement | null => {
|
}: NewUserModalProps): ReactElement | null => {
|
||||||
const {
|
const {
|
||||||
|
control,
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
setValue,
|
setValue,
|
||||||
@ -84,20 +101,22 @@ export const NewUserModal = ({
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
resolver: zodResolver(newUserSchema) as any,
|
resolver: zodResolver(newUserSchema) as any,
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
role_ids: [],
|
role_module_combinations: [{ role_id: "", module_id: null }],
|
||||||
department_id: "",
|
department_id: "",
|
||||||
designation_id: "",
|
designation_id: "",
|
||||||
module_ids: [],
|
|
||||||
category: "tenant_user" as const,
|
category: "tenant_user" as const,
|
||||||
supplier_id: null,
|
supplier_id: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: "role_module_combinations",
|
||||||
|
});
|
||||||
|
|
||||||
const statusValue = watch("status");
|
const statusValue = watch("status");
|
||||||
const roleIdsValue = watch("role_ids");
|
|
||||||
const departmentIdValue = watch("department_id");
|
const departmentIdValue = watch("department_id");
|
||||||
const designationIdValue = watch("designation_id");
|
const designationIdValue = watch("designation_id");
|
||||||
const moduleIdsValue = watch("module_ids");
|
|
||||||
const categoryValue = watch("category");
|
const categoryValue = watch("category");
|
||||||
const supplierIdValue = watch("supplier_id");
|
const supplierIdValue = watch("supplier_id");
|
||||||
|
|
||||||
@ -115,10 +134,9 @@ export const NewUserModal = ({
|
|||||||
last_name: "",
|
last_name: "",
|
||||||
status: "active",
|
status: "active",
|
||||||
auth_provider: "local",
|
auth_provider: "local",
|
||||||
role_ids: [],
|
role_module_combinations: [{ role_id: "", module_id: null }],
|
||||||
department_id: "",
|
department_id: "",
|
||||||
designation_id: "",
|
designation_id: "",
|
||||||
module_ids: [],
|
|
||||||
category: "tenant_user",
|
category: "tenant_user",
|
||||||
supplier_id: null,
|
supplier_id: null,
|
||||||
});
|
});
|
||||||
@ -244,8 +262,7 @@ export const NewUserModal = ({
|
|||||||
detail.path === "last_name" ||
|
detail.path === "last_name" ||
|
||||||
detail.path === "status" ||
|
detail.path === "status" ||
|
||||||
detail.path === "auth_provider" ||
|
detail.path === "auth_provider" ||
|
||||||
detail.path === "role_ids" ||
|
detail.path === "role_module_combinations"
|
||||||
detail.path === "module_ids"
|
|
||||||
) {
|
) {
|
||||||
setError(detail.path as keyof NewUserFormData, {
|
setError(detail.path as keyof NewUserFormData, {
|
||||||
type: "server",
|
type: "server",
|
||||||
@ -421,18 +438,7 @@ export const NewUserModal = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Role and Status Row */}
|
<div className="pb-4">
|
||||||
<div className="grid grid-cols-2 gap-5 pb-4">
|
|
||||||
<MultiselectPaginatedSelect
|
|
||||||
label="Assign Role"
|
|
||||||
required
|
|
||||||
placeholder="Select Roles"
|
|
||||||
value={roleIdsValue || []}
|
|
||||||
onValueChange={(value) => setValue("role_ids", value)}
|
|
||||||
onLoadOptions={loadRoles}
|
|
||||||
error={errors.role_ids?.message}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormSelect
|
<FormSelect
|
||||||
label="Status"
|
label="Status"
|
||||||
required
|
required
|
||||||
@ -446,15 +452,64 @@ export const NewUserModal = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pb-4">
|
<div className="mt-2 mb-4">
|
||||||
<MultiselectPaginatedSelect
|
<div className="flex justify-between items-center mb-2">
|
||||||
label="Assign Modules"
|
<label className="text-sm font-medium text-gray-700">Role & Module Assignments</label>
|
||||||
placeholder="Select Modules"
|
<button
|
||||||
value={moduleIdsValue || []}
|
type="button"
|
||||||
onValueChange={(value) => setValue("module_ids", value)}
|
onClick={() => append({ role_id: "", module_id: null })}
|
||||||
onLoadOptions={loadModules}
|
className="text-sm font-medium text-[#112868] hover:text-[#0b1c4a] flex items-center gap-1"
|
||||||
error={errors.module_ids?.message}
|
>
|
||||||
/>
|
<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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -141,7 +141,17 @@ export const ViewUserModal = ({
|
|||||||
Roles
|
Roles
|
||||||
</label>
|
</label>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{user.roles && user.roles.length > 0 ? (
|
{user.role_module_combinations && user.role_module_combinations.length > 0 ? (
|
||||||
|
user.role_module_combinations.map((combo, idx) => (
|
||||||
|
<span
|
||||||
|
key={`${combo.role_id}-${combo.module_id || 'global'}-${idx}`}
|
||||||
|
className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-medium bg-[#112868]/10 text-[#112868]"
|
||||||
|
title={combo.module_name ? `Role for ${combo.module_name}` : "Global Role"}
|
||||||
|
>
|
||||||
|
{combo.role_name} {combo.module_name && `(${combo.module_name})`}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
) : user.roles && user.roles.length > 0 ? (
|
||||||
user.roles.map((role) => (
|
user.roles.map((role) => (
|
||||||
<span
|
<span
|
||||||
key={role.id}
|
key={role.id}
|
||||||
|
|||||||
@ -132,7 +132,7 @@ export const UsersTable = ({
|
|||||||
last_name: string;
|
last_name: string;
|
||||||
status: "active" | "suspended" | "deleted";
|
status: "active" | "suspended" | "deleted";
|
||||||
auth_provider: "local";
|
auth_provider: "local";
|
||||||
role_ids: string[];
|
role_module_combinations: { role_id: string; module_id?: string | null }[];
|
||||||
department_id?: string;
|
department_id?: string;
|
||||||
designation_id?: string;
|
designation_id?: string;
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
@ -181,7 +181,7 @@ export const UsersTable = ({
|
|||||||
status: "active" | "suspended" | "deleted";
|
status: "active" | "suspended" | "deleted";
|
||||||
auth_provider?: string;
|
auth_provider?: string;
|
||||||
tenant_id: string;
|
tenant_id: string;
|
||||||
role_ids: string[];
|
role_module_combinations: { role_id: string; module_id?: string | null }[];
|
||||||
department_id?: string;
|
department_id?: string;
|
||||||
designation_id?: string;
|
designation_id?: string;
|
||||||
},
|
},
|
||||||
@ -270,7 +270,17 @@ export const UsersTable = ({
|
|||||||
label: "Role",
|
label: "Role",
|
||||||
render: (user) => (
|
render: (user) => (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{user.roles && user.roles.length > 0 ? (
|
{user.role_module_combinations && user.role_module_combinations.length > 0 ? (
|
||||||
|
user.role_module_combinations.map((combo, idx) => (
|
||||||
|
<span
|
||||||
|
key={`${combo.role_id}-${combo.module_id || 'global'}-${idx}`}
|
||||||
|
className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-medium bg-[#112868]/10 text-[#112868]"
|
||||||
|
title={combo.module_name ? `Role for ${combo.module_name}` : "Global Role"}
|
||||||
|
>
|
||||||
|
{combo.role_name} {combo.module_name && `(${combo.module_name})`}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
) : user.roles && user.roles.length > 0 ? (
|
||||||
user.roles.map((role) => (
|
user.roles.map((role) => (
|
||||||
<span
|
<span
|
||||||
key={role.id}
|
key={role.id}
|
||||||
|
|||||||
@ -134,7 +134,7 @@ const Users = (): ReactElement => {
|
|||||||
last_name: string;
|
last_name: string;
|
||||||
status: "active" | "suspended" | "deleted";
|
status: "active" | "suspended" | "deleted";
|
||||||
auth_provider: "local";
|
auth_provider: "local";
|
||||||
role_ids: string[];
|
role_module_combinations: { role_id: string; module_id?: string | null }[];
|
||||||
department_id?: string;
|
department_id?: string;
|
||||||
designation_id?: string;
|
designation_id?: string;
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
@ -179,7 +179,7 @@ const Users = (): ReactElement => {
|
|||||||
last_name: string;
|
last_name: string;
|
||||||
status: "active" | "suspended" | "deleted";
|
status: "active" | "suspended" | "deleted";
|
||||||
tenant_id: string;
|
tenant_id: string;
|
||||||
role_ids: string[];
|
role_module_combinations: { role_id: string; module_id?: string | null }[];
|
||||||
department_id?: string;
|
department_id?: string;
|
||||||
designation_id?: string;
|
designation_id?: string;
|
||||||
},
|
},
|
||||||
@ -264,7 +264,17 @@ const Users = (): ReactElement => {
|
|||||||
label: "Role",
|
label: "Role",
|
||||||
render: (user) => (
|
render: (user) => (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{user.roles && user.roles.length > 0 ? (
|
{user.role_module_combinations && user.role_module_combinations.length > 0 ? (
|
||||||
|
user.role_module_combinations.map((combo, idx) => (
|
||||||
|
<span
|
||||||
|
key={`${combo.role_id}-${combo.module_id || 'global'}-${idx}`}
|
||||||
|
className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-medium bg-[#112868]/10 text-[#112868]"
|
||||||
|
title={combo.module_name ? `Role for ${combo.module_name}` : "Global Role"}
|
||||||
|
>
|
||||||
|
{combo.role_name} {combo.module_name && `(${combo.module_name})`}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
) : user.roles && user.roles.length > 0 ? (
|
||||||
user.roles.map((role) => (
|
user.roles.map((role) => (
|
||||||
<span
|
<span
|
||||||
key={role.id}
|
key={role.id}
|
||||||
|
|||||||
@ -1,3 +1,15 @@
|
|||||||
|
export interface RoleModuleCombination {
|
||||||
|
role_id: string;
|
||||||
|
module_id?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserRoleModuleCombination {
|
||||||
|
role_id: string;
|
||||||
|
role_name: string;
|
||||||
|
module_id: string | null;
|
||||||
|
module_name: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
@ -33,6 +45,7 @@ export interface User {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
}[];
|
}[];
|
||||||
|
role_module_combinations?: UserRoleModuleCombination[];
|
||||||
category?: 'tenant_user' | 'supplier_user';
|
category?: 'tenant_user' | 'supplier_user';
|
||||||
supplier_id?: string | null;
|
supplier_id?: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
@ -63,6 +76,7 @@ export interface CreateUserRequest {
|
|||||||
tenant_id?: string;
|
tenant_id?: string;
|
||||||
role_id?: string;
|
role_id?: string;
|
||||||
role_ids?: string[];
|
role_ids?: string[];
|
||||||
|
role_module_combinations?: RoleModuleCombination[];
|
||||||
department_id?: string;
|
department_id?: string;
|
||||||
designation_id?: string;
|
designation_id?: string;
|
||||||
module_ids?: string[];
|
module_ids?: string[];
|
||||||
@ -90,6 +104,7 @@ export interface UpdateUserRequest {
|
|||||||
tenant_id: string;
|
tenant_id: string;
|
||||||
role_id?: string;
|
role_id?: string;
|
||||||
role_ids?: string[];
|
role_ids?: string[];
|
||||||
|
role_module_combinations?: RoleModuleCombination[];
|
||||||
department_id?: string;
|
department_id?: string;
|
||||||
designation_id?: string;
|
designation_id?: string;
|
||||||
module_ids?: string[];
|
module_ids?: string[];
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user