Deploy/batch 2 medium risk #2
@@ -17,14 +17,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!term.isActive" class="term-item__status">
|
|
||||||
<span class="term-item__status-badge">
|
|
||||||
<span class="term-item__status-dot" />
|
|
||||||
غیرفعال
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="term-item__actions">
|
<div class="term-item__actions">
|
||||||
|
<div v-if="!term.isActive" class="term-item__status">
|
||||||
|
<span class="term-item__status-badge">
|
||||||
|
<span class="term-item__status-dot" />
|
||||||
|
غیرفعال
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
:model-value="!!term.isActive"
|
:model-value="!!term.isActive"
|
||||||
@update:model-value="emit('change-status', { id: term.id, isActive: !term.isActive })"
|
@update:model-value="emit('change-status', { id: term.id, isActive: !term.isActive })"
|
||||||
@@ -166,10 +165,6 @@ const createdDate = computed(() => formatJalaaliDate(props.term.createdAt) || ''
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__status {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__status-badge {
|
&__status-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -191,6 +186,7 @@ const createdDate = computed(() => formatJalaaliDate(props.term.createdAt) || ''
|
|||||||
|
|
||||||
&__actions {
|
&__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.375rem;
|
gap: 0.375rem;
|
||||||
|
|||||||
@@ -25,12 +25,12 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="users-table__role-badge"
|
class="users-table__role-badge"
|
||||||
:class="`users-table__role-badge--${roleName(user) || 'unknown'}`"
|
:class="`users-table__role-badge--${roleNames(user)[0] || 'unknown'}`"
|
||||||
:disabled="updateRoleMutation.isPending.value && activeUser?.id === user.id"
|
:disabled="updateRoleMutation.isPending.value && activeUser?.id === user.id"
|
||||||
@click="onRoleClick($event, user)"
|
@click="onRoleClick($event, user)"
|
||||||
>
|
>
|
||||||
<span class="users-table__role-dot" />
|
<span class="users-table__role-dot" />
|
||||||
<span>{{ roleLabel(user) || 'بدون نقش' }}</span>
|
<span>{{ roleLabelsText(user) || 'بدون نقش' }}</span>
|
||||||
<SvgIcon name="caret-down" :size="12" color="currentColor" />
|
<SvgIcon name="caret-down" :size="12" color="currentColor" />
|
||||||
</button>
|
</button>
|
||||||
<Badge v-if="user.status === 'blocked'" variant="danger" size="sm" dot>
|
<Badge v-if="user.status === 'blocked'" variant="danger" size="sm" dot>
|
||||||
@@ -96,15 +96,13 @@ const queryClient = useQueryClient()
|
|||||||
|
|
||||||
const emit = defineEmits(['edit', 'delete', 'block', 'unblock', 'show-details'])
|
const emit = defineEmits(['edit', 'delete', 'block', 'unblock', 'show-details'])
|
||||||
|
|
||||||
const roleName = (user) => {
|
const roleNames = (user) =>
|
||||||
const first = user.roles?.[0]
|
(user.roles || []).map((r) => (typeof r === 'string' ? r : r?.name)).filter(Boolean)
|
||||||
return typeof first === 'string' ? first : first?.name
|
|
||||||
}
|
|
||||||
|
|
||||||
const roleLabel = (user) => {
|
const roleLabelsText = (user) => {
|
||||||
const name = roleName(user)
|
const names = roleNames(user)
|
||||||
if (!name) return ''
|
if (names.length === 0) return ''
|
||||||
return ROLES.find((r) => r.value === name)?.label || name
|
return names.map((name) => ROLES.find((r) => r.value === name)?.label || name).join('، ')
|
||||||
}
|
}
|
||||||
|
|
||||||
const menuOpen = ref(false)
|
const menuOpen = ref(false)
|
||||||
@@ -140,23 +138,39 @@ const onRoleClick = (event, user) => {
|
|||||||
roleMenuOpen.value = true
|
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
|
const user = activeUser.value
|
||||||
if (!user || !targetRoleName) return
|
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(
|
updateRoleMutation.mutate(
|
||||||
{ id: user.id, payload: { roles: [targetRoleName] } },
|
{ id: user.id, payload: { roles: nextRoles } },
|
||||||
{ onSuccess: invalidateUsers }
|
{ onSuccess: invalidateUsers }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleMenuItems = computed(() => {
|
const roleMenuItems = computed(() => {
|
||||||
const current = activeUser.value ? roleName(activeUser.value) : null
|
const user = activeUser.value
|
||||||
return ROLES.filter((r) => r.changeable && r.value !== current).map((r) => ({
|
if (!user) return []
|
||||||
key: `role-${r.value}`,
|
const current = roleNames(user)
|
||||||
label: `تغییر به ${r.label}`,
|
return ROLES.filter((r) => r.changeable).map((r) => {
|
||||||
icon: 'arrows-clockwise',
|
const isAssigned = current.includes(r.value)
|
||||||
onClick: () => onChangeRole(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(() => {
|
const menuItems = computed(() => {
|
||||||
|
|||||||
@@ -19,14 +19,15 @@
|
|||||||
<div class="user-form__row">
|
<div class="user-form__row">
|
||||||
<div class="user-form__cell user-form__cell--third">
|
<div class="user-form__cell user-form__cell--third">
|
||||||
<SelectField
|
<SelectField
|
||||||
v-model="form.role"
|
v-model="form.roles"
|
||||||
name="role"
|
name="roles"
|
||||||
label="نقش"
|
label="نقشها"
|
||||||
|
:multiple="true"
|
||||||
:options="ROLES"
|
:options="ROLES"
|
||||||
option-label="label"
|
option-label="label"
|
||||||
option-value="value"
|
option-value="value"
|
||||||
:error="errors.role"
|
:error="errors.roles"
|
||||||
@change="validateAt('role', form.role)"
|
@change="validateAt('roles', form.roles)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-form__cell user-form__cell--third">
|
<div class="user-form__cell user-form__cell--third">
|
||||||
@@ -265,7 +266,7 @@ const { data: cities = ref([]) } = useGetCitiesOfProvinceQuery(provinceIdRef, {
|
|||||||
const initialNationalCode = ref('')
|
const initialNationalCode = ref('')
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
role: '',
|
roles: [],
|
||||||
name: '',
|
name: '',
|
||||||
birthday: '',
|
birthday: '',
|
||||||
nationalCode: '',
|
nationalCode: '',
|
||||||
@@ -307,11 +308,12 @@ watch(
|
|||||||
existingUser,
|
existingUser,
|
||||||
(user) => {
|
(user) => {
|
||||||
if (!user) return
|
if (!user) return
|
||||||
const firstRole = Array.isArray(user.roles) ? user.roles[0] : null
|
const roleValues = Array.isArray(user.roles)
|
||||||
const roleValue = typeof firstRole === 'object' ? firstRole?.name : firstRole
|
? user.roles.map((r) => (typeof r === 'object' ? r?.name : r)).filter(Boolean)
|
||||||
|
: []
|
||||||
form.value = {
|
form.value = {
|
||||||
...form.value,
|
...form.value,
|
||||||
role: roleValue || '',
|
roles: roleValues,
|
||||||
name: user.name || `${user.firstName || ''} ${user.lastName || ''}`.trim(),
|
name: user.name || `${user.firstName || ''} ${user.lastName || ''}`.trim(),
|
||||||
birthday: user.birthday || '',
|
birthday: user.birthday || '',
|
||||||
nationalCode: user.nationalCode || '',
|
nationalCode: user.nationalCode || '',
|
||||||
@@ -360,7 +362,7 @@ const submitting = computed(() => addMutation.isPending.value || updateMutation.
|
|||||||
const buildUpdatePayload = () => {
|
const buildUpdatePayload = () => {
|
||||||
return {
|
return {
|
||||||
name: form.value.name,
|
name: form.value.name,
|
||||||
roles: form.value.role ? [form.value.role] : [],
|
roles: form.value.roles,
|
||||||
provinceId: form.value.provinceId || null,
|
provinceId: form.value.provinceId || null,
|
||||||
cityId: form.value.cityId || null,
|
cityId: form.value.cityId || null,
|
||||||
marriageStatus: form.value.maritalStatus || null,
|
marriageStatus: form.value.maritalStatus || null,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { number, object, string } from 'yup'
|
import { array, number, object, string } from 'yup'
|
||||||
import { passwordRule } from '@/constants/rules/passwordRule'
|
import { passwordRule } from '@/constants/rules/passwordRule'
|
||||||
import { phoneNumberRule } from '@/constants/rules/phoneNumberRule'
|
import { phoneNumberRule } from '@/constants/rules/phoneNumberRule'
|
||||||
import { nationalCodeRule } from '@/constants/rules/nationalCodeRule'
|
import { nationalCodeRule } from '@/constants/rules/nationalCodeRule'
|
||||||
@@ -9,7 +9,7 @@ export const rejectionReasonSchema = object().shape({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const baseUserShape = {
|
const baseUserShape = {
|
||||||
role: string().required(),
|
roles: array().of(string().required()).min(1, 'حداقل یک نقش را انتخاب کنید').required(),
|
||||||
name: string().required().min(3),
|
name: string().required().min(3),
|
||||||
phone: phoneNumberRule,
|
phone: phoneNumberRule,
|
||||||
nationalCode: nationalCodeRule,
|
nationalCode: nationalCodeRule,
|
||||||
|
|||||||
Reference in New Issue
Block a user