From d9d573256c63743b67db2cf495df6ec2b2dae9fa Mon Sep 17 00:00:00 2001 From: madresebanoo Date: Wed, 2 Sep 2026 15:16:04 +0430 Subject: [PATCH] feat(admin/users): support multiple roles in user table and form UsersTable now shows all role labels joined (not just the first role), and its CSS badge class is based on the primary role. UserFormPage uses a multi-select for roles instead of a single-select, and the form validation requires at least one role. TermItem layout fix for inactive badge alignment. Co-Authored-By: Claude Sonnet 4.6 --- .../admin/terms/components/TermItem.vue | 18 +++---- .../admin/users/components/UsersTable.vue | 52 ++++++++++++------- .../admin/users/pages/UserFormPage.vue | 22 ++++---- src/features/admin/users/schema/index.js | 4 +- 4 files changed, 54 insertions(+), 42 deletions(-) diff --git a/src/features/admin/terms/components/TermItem.vue b/src/features/admin/terms/components/TermItem.vue index 7e1715f..f717c34 100644 --- a/src/features/admin/terms/components/TermItem.vue +++ b/src/features/admin/terms/components/TermItem.vue @@ -17,14 +17,13 @@ -
- - - غیرفعال - -
-
+
+ + + غیرفعال + +
formatJalaaliDate(props.term.createdAt) || '' font-size: 0.75rem; } - &__status { - flex: 0 0 auto; - } - &__status-badge { display: inline-flex; align-items: center; @@ -191,6 +186,7 @@ const createdDate = computed(() => formatJalaaliDate(props.term.createdAt) || '' &__actions { display: flex; + flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: 0.375rem; diff --git a/src/features/admin/users/components/UsersTable.vue b/src/features/admin/users/components/UsersTable.vue index a525b4b..3b9089d 100644 --- a/src/features/admin/users/components/UsersTable.vue +++ b/src/features/admin/users/components/UsersTable.vue @@ -25,12 +25,12 @@ @@ -96,15 +96,13 @@ const queryClient = useQueryClient() const emit = defineEmits(['edit', 'delete', 'block', 'unblock', 'show-details']) -const roleName = (user) => { - const first = user.roles?.[0] - return typeof first === 'string' ? first : first?.name -} +const roleNames = (user) => + (user.roles || []).map((r) => (typeof r === 'string' ? r : r?.name)).filter(Boolean) -const roleLabel = (user) => { - const name = roleName(user) - if (!name) return '' - return ROLES.find((r) => r.value === name)?.label || name +const roleLabelsText = (user) => { + const names = roleNames(user) + if (names.length === 0) return '' + return names.map((name) => ROLES.find((r) => r.value === name)?.label || name).join('، ') } const menuOpen = ref(false) @@ -140,23 +138,39 @@ const onRoleClick = (event, user) => { roleMenuOpen.value = true } -const onChangeRole = (targetRoleName) => { +// Toggles one role's membership without touching the user's other roles — +// access must stay additive, so this can never collapse into a single-role +// replace the way the old "switch to X" action used to. +const onToggleRole = (targetRoleName, isAssigned) => { const user = activeUser.value if (!user || !targetRoleName) return + const current = roleNames(user) + const nextRoles = isAssigned + ? current.filter((r) => r !== targetRoleName) + : [...current, targetRoleName] + if (nextRoles.length === 0) return updateRoleMutation.mutate( - { id: user.id, payload: { roles: [targetRoleName] } }, + { id: user.id, payload: { roles: nextRoles } }, { onSuccess: invalidateUsers } ) } const roleMenuItems = computed(() => { - const current = activeUser.value ? roleName(activeUser.value) : null - return ROLES.filter((r) => r.changeable && r.value !== current).map((r) => ({ - key: `role-${r.value}`, - label: `تغییر به ${r.label}`, - icon: 'arrows-clockwise', - onClick: () => onChangeRole(r.value), - })) + const user = activeUser.value + if (!user) return [] + const current = roleNames(user) + return ROLES.filter((r) => r.changeable).map((r) => { + const isAssigned = current.includes(r.value) + const isLastRole = isAssigned && current.length <= 1 + return { + key: `role-${r.value}`, + label: isAssigned ? `حذف نقش ${r.label}` : `افزودن نقش ${r.label}`, + icon: isAssigned ? 'close' : 'check', + danger: isAssigned, + disabled: isLastRole, + onClick: () => onToggleRole(r.value, isAssigned), + } + }) }) const menuItems = computed(() => { diff --git a/src/features/admin/users/pages/UserFormPage.vue b/src/features/admin/users/pages/UserFormPage.vue index 46b1f96..70eada8 100644 --- a/src/features/admin/users/pages/UserFormPage.vue +++ b/src/features/admin/users/pages/UserFormPage.vue @@ -19,14 +19,15 @@
@@ -265,7 +266,7 @@ const { data: cities = ref([]) } = useGetCitiesOfProvinceQuery(provinceIdRef, { const initialNationalCode = ref('') const form = ref({ - role: '', + roles: [], name: '', birthday: '', nationalCode: '', @@ -307,11 +308,12 @@ watch( existingUser, (user) => { if (!user) return - const firstRole = Array.isArray(user.roles) ? user.roles[0] : null - const roleValue = typeof firstRole === 'object' ? firstRole?.name : firstRole + const roleValues = Array.isArray(user.roles) + ? user.roles.map((r) => (typeof r === 'object' ? r?.name : r)).filter(Boolean) + : [] form.value = { ...form.value, - role: roleValue || '', + roles: roleValues, name: user.name || `${user.firstName || ''} ${user.lastName || ''}`.trim(), birthday: user.birthday || '', nationalCode: user.nationalCode || '', @@ -360,7 +362,7 @@ const submitting = computed(() => addMutation.isPending.value || updateMutation. const buildUpdatePayload = () => { return { name: form.value.name, - roles: form.value.role ? [form.value.role] : [], + roles: form.value.roles, provinceId: form.value.provinceId || null, cityId: form.value.cityId || null, marriageStatus: form.value.maritalStatus || null, diff --git a/src/features/admin/users/schema/index.js b/src/features/admin/users/schema/index.js index 59c0834..6ec5475 100644 --- a/src/features/admin/users/schema/index.js +++ b/src/features/admin/users/schema/index.js @@ -1,4 +1,4 @@ -import { number, object, string } from 'yup' +import { array, number, object, string } from 'yup' import { passwordRule } from '@/constants/rules/passwordRule' import { phoneNumberRule } from '@/constants/rules/phoneNumberRule' import { nationalCodeRule } from '@/constants/rules/nationalCodeRule' @@ -9,7 +9,7 @@ export const rejectionReasonSchema = object().shape({ }) const baseUserShape = { - role: string().required(), + roles: array().of(string().required()).min(1, 'حداقل یک نقش را انتخاب کنید').required(), name: string().required().min(3), phone: phoneNumberRule, nationalCode: nationalCodeRule,