From f7eb25f87435a529fcfcdaac9fab74dd60383f06 Mon Sep 17 00:00:00 2001 From: Yashwin Date: Thu, 9 Jul 2026 15:58:25 +0530 Subject: [PATCH] feat: add tenant admin support to EditUserModal and dynamic form validation --- src/components/shared/EditUserModal.tsx | 277 +++++++++++++---------- src/components/shared/ViewUserModal.tsx | 15 ++ src/components/superadmin/UsersTable.tsx | 2 +- src/types/user.ts | 4 + 4 files changed, 174 insertions(+), 124 deletions(-) diff --git a/src/components/shared/EditUserModal.tsx b/src/components/shared/EditUserModal.tsx index e7f003e..8a82bbb 100644 --- a/src/components/shared/EditUserModal.tsx +++ b/src/components/shared/EditUserModal.tsx @@ -69,7 +69,7 @@ const editUserSchema = z.object({ type EditUserFormData = z.infer; type UpdateUserPayload = Omit & { - role_module_combinations: { role_id: string; module_id: string }[]; + role_module_combinations?: { role_id: string; module_id?: string | null }[]; }; interface EditUserModalProps { @@ -100,6 +100,8 @@ export const EditUserModal = ({ const [isLoadingUser, setIsLoadingUser] = useState(false); const [loadError, setLoadError] = useState(null); const loadedUserIdRef = useRef(null); + const [isTenantAdmin, setIsTenantAdmin] = useState(false); + const [selectedUserTenantId, setSelectedUserTenantId] = useState(""); const { control, @@ -113,7 +115,20 @@ export const EditUserModal = ({ formState: { errors }, } = useForm({ // @ts-ignore - resolver: zodResolver(editUserSchema), + resolver: zodResolver( + isTenantAdmin + ? editUserSchema.extend({ + role_module_assignments: z + .array( + z.object({ + role_id: z.string().optional(), + module_ids: z.array(z.string()).optional(), + }) + ) + .optional(), + }) + : editUserSchema + ), }); const { fields, append, remove } = useFieldArray({ @@ -127,8 +142,6 @@ export const EditUserModal = ({ const categoryValue = watch("category"); const supplierIdValue = watch("supplier_id"); - const rolesFromAuth = useAppSelector((state) => state.auth.roles); - const isSuperAdmin = rolesFromAuth.includes("super_admin"); const tenantIdFromAuth = useAppSelector((state) => state.auth.tenantId); const [initialRoleOptions, setInitialRoleOptions] = useState< @@ -152,8 +165,9 @@ export const EditUserModal = ({ // Load roles for dropdown - ensure selected role is included const loadRoles = async (page: number, limit: number) => { - const response = defaultTenantId - ? await roleService.getByTenant(defaultTenantId, page, limit, undefined, undefined, true) + const effectiveTenantId = selectedUserTenantId || defaultTenantId || tenantIdFromAuth; + const response = effectiveTenantId + ? await roleService.getByTenant(effectiveTenantId, page, limit, undefined, undefined, true) : await roleService.getAll(page, limit, undefined, undefined, undefined, true); return { options: response.data.map((role) => ({ @@ -165,7 +179,8 @@ export const EditUserModal = ({ }; const loadDepartments = async () => { - const response = await departmentService.list(defaultTenantId, { + const effectiveTenantId = selectedUserTenantId || defaultTenantId || tenantIdFromAuth; + const response = await departmentService.list(effectiveTenantId, { active_only: true, }); return { @@ -184,7 +199,8 @@ export const EditUserModal = ({ }; const loadDesignations = async () => { - const response = await designationService.list(defaultTenantId, { + const effectiveTenantId = selectedUserTenantId || defaultTenantId || tenantIdFromAuth; + const response = await designationService.list(effectiveTenantId, { active_only: true, }); return { @@ -203,8 +219,8 @@ export const EditUserModal = ({ }; const loadModules = async (page: number, limit: number) => { - const tenantId = isSuperAdmin ? defaultTenantId : tenantIdFromAuth; - const response = await moduleService.getAvailable(page, limit, tenantId); + const effectiveTenantId = selectedUserTenantId || defaultTenantId || tenantIdFromAuth; + const response = await moduleService.getAvailable(page, limit, effectiveTenantId); return { options: response.data.map((module) => ({ value: module.id, @@ -215,8 +231,9 @@ export const EditUserModal = ({ }; const loadSuppliers = async (page: number, limit: number) => { + const effectiveTenantId = selectedUserTenantId || defaultTenantId || tenantIdFromAuth; const response = await supplierService.list({ - tenantId: defaultTenantId, + tenantId: effectiveTenantId, limit, offset: (page - 1) * limit, }); @@ -344,6 +361,14 @@ export const EditUserModal = ({ }); } + const isUserTenantAdmin = + user.roles?.some((r) => r.code === "tenant_admin") || + user.role_module_combinations?.some((c) => c.role_code === "tenant_admin") || + user.role?.code === "tenant_admin" || + user.role_id === "tenant_admin"; + setIsTenantAdmin(!!isUserTenantAdmin); + setSelectedUserTenantId(tenantId); + reset({ email: user.email, first_name: user.first_name, @@ -406,6 +431,8 @@ export const EditUserModal = ({ setInitialDepartmentOption(null); setInitialDesignationOption(null); setInitialSupplierOption(null); + setIsTenantAdmin(false); + setSelectedUserTenantId(""); reset({ email: "", first_name: "", @@ -448,12 +475,14 @@ export const EditUserModal = ({ } 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, - })), - ); + const role_module_combinations = isTenantAdmin + ? undefined + : (role_module_assignments || []).flatMap((row) => + (row.module_ids || []).map((module_id) => ({ + role_id: row.role_id, + module_id, + })), + ); await onSubmit(userId, { ...submitData, role_module_combinations }); } catch (error: any) { if ( @@ -672,118 +701,120 @@ export const EditUserModal = ({ /> -
-
- - -
+ {!isTenantAdmin && ( +
+
+ + +
- {(errors.role_module_assignments as any)?.message && ( -

- {(errors.role_module_assignments as any).message} -

- )} + {(errors.role_module_assignments as any)?.message && ( +

+ {(errors.role_module_assignments as any).message} +

+ )} -
- {fields.map((field, index) => { - const roleIdValue = watch( - `role_module_assignments.${index}.role_id`, - ); - const moduleIdsValue = - watch(`role_module_assignments.${index}.module_ids`) || []; - - // Extract specific label if available from initial options - const getRoleLabel = (val: string) => { - const opt = initialRoleOptions.find((o) => o.value === val); - return opt ? opt.label : undefined; - }; - - const initialSelectedModules = moduleIdsValue - .map((val) => { - const opt = initialModuleOptions.find( - (o) => o.value === val, - ); - return opt - ? { value: opt.value, label: opt.label } - : null; - }) - .filter((opt): opt is { value: string; label: string } => - Boolean(opt), +
+ {fields.map((field, index) => { + const roleIdValue = watch( + `role_module_assignments.${index}.role_id`, ); + const moduleIdsValue = + watch(`role_module_assignments.${index}.module_ids`) || []; - return ( -
-
- - setValue( - `role_module_assignments.${index}.role_id`, - value, - { shouldValidate: true }, - ) - } - onLoadOptions={loadRoles} - initialOption={ - roleIdValue - ? { - value: roleIdValue, - label: - getRoleLabel(roleIdValue) || roleIdValue, - } - : undefined - } - error={ - errors.role_module_assignments?.[index]?.role_id - ?.message - } - /> - - setValue( - `role_module_assignments.${index}.module_ids`, - value, - { shouldValidate: true }, - ) - } - onLoadOptions={loadModules} - initialOptions={initialSelectedModules} - error={getModuleError(index)} - /> + // 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 initialSelectedModules = moduleIdsValue + .map((val) => { + const opt = initialModuleOptions.find( + (o) => o.value === val, + ); + return opt + ? { value: opt.value, label: opt.label } + : null; + }) + .filter((opt): opt is { value: string; label: string } => + Boolean(opt), + ); + + return ( +
+
+ + setValue( + `role_module_assignments.${index}.role_id`, + value, + { shouldValidate: true }, + ) + } + onLoadOptions={loadRoles} + initialOption={ + roleIdValue + ? { + value: roleIdValue, + label: + getRoleLabel(roleIdValue) || roleIdValue, + } + : undefined + } + error={ + errors.role_module_assignments?.[index]?.role_id + ?.message + } + /> + + setValue( + `role_module_assignments.${index}.module_ids`, + value, + { shouldValidate: true }, + ) + } + onLoadOptions={loadModules} + initialOptions={initialSelectedModules} + error={getModuleError(index)} + /> +
+ {fields.length > 1 && ( + + )}
- {fields.length > 1 && ( - - )} -
- ); - })} + ); + })} +
-
+ )}
)} diff --git a/src/components/shared/ViewUserModal.tsx b/src/components/shared/ViewUserModal.tsx index 6f65df2..ef1ec74 100644 --- a/src/components/shared/ViewUserModal.tsx +++ b/src/components/shared/ViewUserModal.tsx @@ -136,6 +136,21 @@ export const ViewUserModal = ({
+ {user.status === "suspended" && user.preferences?.security?.suspended_reason && ( +
+ +

+ {user.preferences.security.suspended_reason} +

+ {user.preferences.security.suspended_at && ( +

+ Suspended on: {formatDate(user.preferences.security.suspended_at)} +

+ )} +
+ )}