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 type { ReactElement } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useForm, useFieldArray } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Loader2, Plus, Trash2 } from "lucide-react";
|
||||
import {
|
||||
Modal,
|
||||
FormField,
|
||||
FormSelect,
|
||||
PaginatedSelect,
|
||||
MultiselectPaginatedSelect,
|
||||
PrimaryButton,
|
||||
SecondaryButton,
|
||||
} from "@/components/shared";
|
||||
@ -30,10 +29,26 @@ const editUserSchema = z.object({
|
||||
message: "Status 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(),
|
||||
designation_id: z.string().optional(),
|
||||
module_ids: z.array(z.string()).optional(),
|
||||
category: z.enum(["tenant_user", "supplier_user"]).default("tenant_user"),
|
||||
supplier_id: z.string().optional().nullable(),
|
||||
});
|
||||
@ -70,6 +85,7 @@ export const EditUserModal = ({
|
||||
const loadedUserIdRef = useRef<string | null>(null);
|
||||
|
||||
const {
|
||||
control,
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
@ -79,15 +95,18 @@ export const EditUserModal = ({
|
||||
clearErrors,
|
||||
formState: { errors },
|
||||
} = useForm<EditUserFormData>({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
resolver: zodResolver(editUserSchema) as any,
|
||||
// @ts-ignore
|
||||
resolver: zodResolver(editUserSchema),
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: "role_module_combinations",
|
||||
});
|
||||
|
||||
const statusValue = watch("status");
|
||||
const roleIdsValue = watch("role_ids");
|
||||
const departmentIdValue = watch("department_id");
|
||||
const designationIdValue = watch("designation_id");
|
||||
const moduleIdsValue = watch("module_ids");
|
||||
const categoryValue = watch("category");
|
||||
const supplierIdValue = watch("supplier_id");
|
||||
|
||||
@ -213,14 +232,37 @@ export const EditUserModal = ({
|
||||
loadedUserIdRef.current = userId;
|
||||
|
||||
const tenantId = user.tenant?.id || user.tenant_id || "";
|
||||
const roleIds =
|
||||
user.roles?.map((r) => r.id) ||
|
||||
(user.role_id ? [user.role_id] : []);
|
||||
const roleOptions =
|
||||
user.roles?.map((r) => ({ value: r.id, label: r.name })) ||
|
||||
(user.role?.id
|
||||
? [{ value: user.role.id, label: user.role.name }]
|
||||
: []);
|
||||
|
||||
const roleOptions: { value: string; label: string }[] = [];
|
||||
const moduleOptions: { value: string; label: string }[] = [];
|
||||
|
||||
let initialCombinations = [{ role_id: "", module_id: null as string | null }];
|
||||
|
||||
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 =
|
||||
user.department?.id || user.department_id || "";
|
||||
@ -252,10 +294,9 @@ export const EditUserModal = ({
|
||||
last_name: user.last_name,
|
||||
status: user.status,
|
||||
tenant_id: defaultTenantId || tenantId,
|
||||
role_ids: roleIds,
|
||||
role_module_combinations: initialCombinations,
|
||||
department_id: departmentId,
|
||||
designation_id: designationId,
|
||||
module_ids: user.modules?.map((m) => m.id) || [],
|
||||
category:
|
||||
(user.category as "tenant_user" | "supplier_user") ||
|
||||
"tenant_user",
|
||||
@ -285,10 +326,8 @@ export const EditUserModal = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (user.modules) {
|
||||
setInitialModuleOptions(
|
||||
user.modules.map((m) => ({ value: m.id, label: m.name })),
|
||||
);
|
||||
if (moduleOptions.length > 0) {
|
||||
setInitialModuleOptions(moduleOptions);
|
||||
}
|
||||
|
||||
if (defaultTenantId) {
|
||||
@ -317,10 +356,9 @@ export const EditUserModal = ({
|
||||
last_name: "",
|
||||
status: "active",
|
||||
tenant_id: defaultTenantId || "",
|
||||
role_ids: [],
|
||||
role_module_combinations: [{ role_id: "", module_id: null }],
|
||||
department_id: "",
|
||||
designation_id: "",
|
||||
module_ids: [],
|
||||
category: "tenant_user",
|
||||
supplier_id: null,
|
||||
});
|
||||
@ -365,10 +403,19 @@ export const EditUserModal = ({
|
||||
const validationErrors = error.response.data.details;
|
||||
validationErrors.forEach(
|
||||
(detail: { path: string; message: string }) => {
|
||||
setError(detail.path as keyof EditUserFormData, {
|
||||
type: "server",
|
||||
message: detail.message,
|
||||
});
|
||||
if (
|
||||
detail.path === "email" ||
|
||||
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 {
|
||||
@ -526,17 +573,7 @@ export const EditUserModal = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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}
|
||||
initialOptions={initialRoleOptions}
|
||||
error={errors.role_ids?.message}
|
||||
/>
|
||||
<div className="pb-4">
|
||||
<FormSelect
|
||||
label="Status"
|
||||
required
|
||||
@ -553,16 +590,77 @@ export const EditUserModal = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pb-4">
|
||||
<MultiselectPaginatedSelect
|
||||
label="Assign Modules"
|
||||
placeholder="Select Modules"
|
||||
value={moduleIdsValue || []}
|
||||
onValueChange={(value) => setValue("module_ids", value)}
|
||||
onLoadOptions={loadModules}
|
||||
initialOptions={initialModuleOptions}
|
||||
error={errors.module_ids?.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_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`);
|
||||
|
||||
// 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>
|
||||
)}
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { useEffect } 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 { z } from "zod";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import {
|
||||
Modal,
|
||||
FormField,
|
||||
FormSelect,
|
||||
PaginatedSelect,
|
||||
MultiselectPaginatedSelect,
|
||||
PrimaryButton,
|
||||
SecondaryButton,
|
||||
} from "@/components/shared";
|
||||
@ -36,10 +36,26 @@ const newUserSchema = z
|
||||
auth_provider: z.enum(["local"], {
|
||||
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(),
|
||||
designation_id: z.string().optional(),
|
||||
module_ids: z.array(z.string()).optional(),
|
||||
category: z.enum(["tenant_user", "supplier_user"]).default("tenant_user"),
|
||||
supplier_id: z.string().optional().nullable(),
|
||||
})
|
||||
@ -72,6 +88,7 @@ export const NewUserModal = ({
|
||||
defaultTenantId,
|
||||
}: NewUserModalProps): ReactElement | null => {
|
||||
const {
|
||||
control,
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
@ -84,20 +101,22 @@ export const NewUserModal = ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
resolver: zodResolver(newUserSchema) as any,
|
||||
defaultValues: {
|
||||
role_ids: [],
|
||||
role_module_combinations: [{ role_id: "", module_id: null }],
|
||||
department_id: "",
|
||||
designation_id: "",
|
||||
module_ids: [],
|
||||
category: "tenant_user" as const,
|
||||
supplier_id: null,
|
||||
},
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: "role_module_combinations",
|
||||
});
|
||||
|
||||
const statusValue = watch("status");
|
||||
const roleIdsValue = watch("role_ids");
|
||||
const departmentIdValue = watch("department_id");
|
||||
const designationIdValue = watch("designation_id");
|
||||
const moduleIdsValue = watch("module_ids");
|
||||
const categoryValue = watch("category");
|
||||
const supplierIdValue = watch("supplier_id");
|
||||
|
||||
@ -115,10 +134,9 @@ export const NewUserModal = ({
|
||||
last_name: "",
|
||||
status: "active",
|
||||
auth_provider: "local",
|
||||
role_ids: [],
|
||||
role_module_combinations: [{ role_id: "", module_id: null }],
|
||||
department_id: "",
|
||||
designation_id: "",
|
||||
module_ids: [],
|
||||
category: "tenant_user",
|
||||
supplier_id: null,
|
||||
});
|
||||
@ -244,8 +262,7 @@ export const NewUserModal = ({
|
||||
detail.path === "last_name" ||
|
||||
detail.path === "status" ||
|
||||
detail.path === "auth_provider" ||
|
||||
detail.path === "role_ids" ||
|
||||
detail.path === "module_ids"
|
||||
detail.path === "role_module_combinations"
|
||||
) {
|
||||
setError(detail.path as keyof NewUserFormData, {
|
||||
type: "server",
|
||||
@ -421,18 +438,7 @@ export const NewUserModal = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Role and Status Row */}
|
||||
<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}
|
||||
/>
|
||||
|
||||
<div className="pb-4">
|
||||
<FormSelect
|
||||
label="Status"
|
||||
required
|
||||
@ -446,15 +452,64 @@ export const NewUserModal = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pb-4">
|
||||
<MultiselectPaginatedSelect
|
||||
label="Assign Modules"
|
||||
placeholder="Select Modules"
|
||||
value={moduleIdsValue || []}
|
||||
onValueChange={(value) => setValue("module_ids", value)}
|
||||
onLoadOptions={loadModules}
|
||||
error={errors.module_ids?.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_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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -141,7 +141,17 @@ export const ViewUserModal = ({
|
||||
Roles
|
||||
</label>
|
||||
<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) => (
|
||||
<span
|
||||
key={role.id}
|
||||
|
||||
@ -132,7 +132,7 @@ export const UsersTable = ({
|
||||
last_name: string;
|
||||
status: "active" | "suspended" | "deleted";
|
||||
auth_provider: "local";
|
||||
role_ids: string[];
|
||||
role_module_combinations: { role_id: string; module_id?: string | null }[];
|
||||
department_id?: string;
|
||||
designation_id?: string;
|
||||
}): Promise<void> => {
|
||||
@ -181,7 +181,7 @@ export const UsersTable = ({
|
||||
status: "active" | "suspended" | "deleted";
|
||||
auth_provider?: string;
|
||||
tenant_id: string;
|
||||
role_ids: string[];
|
||||
role_module_combinations: { role_id: string; module_id?: string | null }[];
|
||||
department_id?: string;
|
||||
designation_id?: string;
|
||||
},
|
||||
@ -270,7 +270,17 @@ export const UsersTable = ({
|
||||
label: "Role",
|
||||
render: (user) => (
|
||||
<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) => (
|
||||
<span
|
||||
key={role.id}
|
||||
|
||||
@ -134,7 +134,7 @@ const Users = (): ReactElement => {
|
||||
last_name: string;
|
||||
status: "active" | "suspended" | "deleted";
|
||||
auth_provider: "local";
|
||||
role_ids: string[];
|
||||
role_module_combinations: { role_id: string; module_id?: string | null }[];
|
||||
department_id?: string;
|
||||
designation_id?: string;
|
||||
}): Promise<void> => {
|
||||
@ -179,7 +179,7 @@ const Users = (): ReactElement => {
|
||||
last_name: string;
|
||||
status: "active" | "suspended" | "deleted";
|
||||
tenant_id: string;
|
||||
role_ids: string[];
|
||||
role_module_combinations: { role_id: string; module_id?: string | null }[];
|
||||
department_id?: string;
|
||||
designation_id?: string;
|
||||
},
|
||||
@ -264,7 +264,17 @@ const Users = (): ReactElement => {
|
||||
label: "Role",
|
||||
render: (user) => (
|
||||
<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) => (
|
||||
<span
|
||||
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 {
|
||||
id: string;
|
||||
email: string;
|
||||
@ -33,6 +45,7 @@ export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
}[];
|
||||
role_module_combinations?: UserRoleModuleCombination[];
|
||||
category?: 'tenant_user' | 'supplier_user';
|
||||
supplier_id?: string | null;
|
||||
created_at: string;
|
||||
@ -63,6 +76,7 @@ export interface CreateUserRequest {
|
||||
tenant_id?: string;
|
||||
role_id?: string;
|
||||
role_ids?: string[];
|
||||
role_module_combinations?: RoleModuleCombination[];
|
||||
department_id?: string;
|
||||
designation_id?: string;
|
||||
module_ids?: string[];
|
||||
@ -90,6 +104,7 @@ export interface UpdateUserRequest {
|
||||
tenant_id: string;
|
||||
role_id?: string;
|
||||
role_ids?: string[];
|
||||
role_module_combinations?: RoleModuleCombination[];
|
||||
department_id?: string;
|
||||
designation_id?: string;
|
||||
module_ids?: string[];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user