fix: admin users
Deploy banu-front / deploy (push) Failing after 16s

This commit is contained in:
sajjadtalkhabi
2026-06-10 20:55:54 +03:30
parent b8ccbcc030
commit b9fa65b84f
51 changed files with 373 additions and 255 deletions
+6 -5
View File
@@ -23,7 +23,11 @@
:loading="!!data?.confirmLoading" :loading="!!data?.confirmLoading"
custom-class="confirm-modal__btn confirm-modal__btn--confirm" custom-class="confirm-modal__btn confirm-modal__btn--confirm"
@click="confirm(data)" @click="confirm(data)"
/> >
<template #appendIcon>
<SvgIcon name="arrow-left" :size="18" color="#fff" />
</template>
</BaseButton>
</div> </div>
</div> </div>
</template> </template>
@@ -31,6 +35,7 @@
</template> </template>
<script setup> <script setup>
import SvgIcon from './icons/SvgIcon.vue'
import { useModalStore } from '@/store/modal' import { useModalStore } from '@/store/modal'
import BasicModal from '@/components/BasicModal.vue' import BasicModal from '@/components/BasicModal.vue'
import BaseButton from '@/components/BaseButton.vue' import BaseButton from '@/components/BaseButton.vue'
@@ -82,10 +87,6 @@ const cancel = (data) => {
&__btn { &__btn {
min-width: 100px; min-width: 100px;
padding: 0.5rem 1.5rem; padding: 0.5rem 1.5rem;
&--confirm {
padding-inline: 1.5rem 2.5rem;
}
} }
} }
</style> </style>
+4 -3
View File
@@ -85,7 +85,7 @@ const props = defineProps({
name: { type: String, default: '' }, name: { type: String, default: '' },
label: { type: String, default: '' }, label: { type: String, default: '' },
placeholder: { type: String, default: 'انتخاب کنید' }, placeholder: { type: String, default: 'انتخاب کنید' },
options: { type: Array, required: true }, options: { type: Array, default: () => [] },
optionLabel: { type: String, default: 'name' }, optionLabel: { type: String, default: 'name' },
optionValue: { type: String, default: 'id' }, optionValue: { type: String, default: 'id' },
multiple: { type: Boolean, default: false }, multiple: { type: Boolean, default: false },
@@ -118,13 +118,14 @@ const isSelected = (opt) => {
const selectedLabel = computed(() => { const selectedLabel = computed(() => {
const value = props.modelValue const value = props.modelValue
if (value === undefined || value === null || value === '') return props.placeholder if (value === undefined || value === null || value === '') return props.placeholder
const options = props.options ?? []
if (props.multiple && Array.isArray(value)) { if (props.multiple && Array.isArray(value)) {
const selected = props.options.filter((o) => value.includes(o[props.optionValue])) const selected = options.filter((o) => value.includes(o[props.optionValue]))
if (selected.length === 0) return props.placeholder if (selected.length === 0) return props.placeholder
if (selected.length <= 2) return selected.map((s) => s[props.optionLabel]).join('، ') if (selected.length <= 2) return selected.map((s) => s[props.optionLabel]).join('، ')
return `${selected.length} مورد انتخاب شده` return `${selected.length} مورد انتخاب شده`
} }
const found = props.options.find((o) => o[props.optionValue] === value) const found = options.find((o) => o[props.optionValue] === value)
return found ? found[props.optionLabel] : props.placeholder return found ? found[props.optionLabel] : props.placeholder
}) })
+1 -1
View File
@@ -88,9 +88,9 @@
<script setup> <script setup>
import { toast } from 'vue3-toastify' import { toast } from 'vue3-toastify'
import useModal from '@/composables/useModal' import useModal from '@/composables/useModal'
import { PermissionModal } from '@/features/auth'
import SvgIcon from '@/components/icons/SvgIcon.vue' import SvgIcon from '@/components/icons/SvgIcon.vue'
import { computed, onBeforeUnmount, ref, watch } from 'vue' import { computed, onBeforeUnmount, ref, watch } from 'vue'
import PermissionModal from '@/features/auth/components/studentRegister/PermissionModal.vue'
const props = defineProps({ const props = defineProps({
modelValue: { type: [Object, File, String, null], default: null }, modelValue: { type: [Object, File, String, null], default: null },
-3
View File
@@ -13,9 +13,6 @@ export type IconName =
| 'caret-right' | 'caret-right'
| 'chat' | 'chat'
| 'chat-centered-dots' | 'chat-centered-dots'
| 'chat-slash'
| 'chat-text'
| 'chats'
| 'check' | 'check'
| 'check-square' | 'check-square'
| 'close' | 'close'
+8 -2
View File
@@ -38,11 +38,17 @@ export const STUDENT_REGISTRATION = Object.freeze({
export const STUDENT_REGISTRATION_ORDER = Object.freeze(Object.values(STUDENT_REGISTRATION)) export const STUDENT_REGISTRATION_ORDER = Object.freeze(Object.values(STUDENT_REGISTRATION))
// User lifecycle states (Set User Status endpoint):
// pending just registered
// verified phone OTP-confirmed
// approved full access
// blocked no access (existing tokens revoked)
export const USER_STATUS = Object.freeze({ export const USER_STATUS = Object.freeze({
approved: 'تأیید شده',
pending: 'در انتظار', pending: 'در انتظار',
rejected: 'رد شده', verified: 'تأیید تلفن',
approved: 'تأیید شده',
blocked: 'مسدود', blocked: 'مسدود',
rejected: 'رد شده',
}) })
export const MARITAL_STATUS = Object.freeze({ export const MARITAL_STATUS = Object.freeze({
+8
View File
@@ -0,0 +1,8 @@
export { default as AddAssignmentModal } from './components/modals/AddAssignmentModal.vue'
export { default as AssignmentDetailsModal } from './components/modals/AssignmentDetailsModal.vue'
export { default as AssignmentItem } from './components/AssignmentItem.vue'
export { default as AssignmentSubmissionDetailsModal } from './components/modals/AssignmentSubmissionDetailsModal.vue'
export { default as AssignmentSubmissionsModal } from './components/modals/AssignmentSubmissionsModal.vue'
export { default as AssignmentsFilters } from './components/AssignmentsFilters.vue'
export * from './schema'
@@ -0,0 +1,3 @@
export { default as ConsultationDetailsModal } from './components/modals/ConsultationDetailsModal.vue'
export { default as ConsultationItem } from './components/ConsultationItem.vue'
export { default as ConsultationsFilters } from './components/ConsultationsFilters.vue'
+8
View File
@@ -0,0 +1,8 @@
export { default as AddOfferedCourseModal } from './components/modals/AddOfferedCourseModal.vue'
export { default as AddSessionToCourseModal } from './components/modals/AddSessionToCourseModal.vue'
export { default as CourseDetailsModal } from './components/modals/CourseDetailsModal.vue'
export { default as CourseItem } from './components/CourseItem.vue'
export { default as CourseSessionItem } from './components/CourseSessionItem.vue'
export { default as CoursesFilters } from './components/CoursesFilters.vue'
export * from './schema'
+8
View File
@@ -0,0 +1,8 @@
export { default as ExamDetailsModal } from './components/modals/ExamDetailsModal.vue'
export { default as ExamItem } from './components/ExamItem.vue'
export { default as ExamParticipantDetailsModal } from './components/modals/ExamParticipantDetailsModal.vue'
export { default as ExamParticipantsModal } from './components/modals/ExamParticipantsModal.vue'
export { default as ExamQuestionBuilder } from './components/ExamQuestionBuilder.vue'
export { default as ExamsFilters } from './components/ExamsFilters.vue'
export * from './schema'
+3
View File
@@ -0,0 +1,3 @@
export { default as TicketDetailsModal } from './components/modals/TicketDetailsModal.vue'
export { default as TicketItem } from './components/TicketItem.vue'
export { default as TicketsFilters } from './components/TicketsFilters.vue'
+4
View File
@@ -0,0 +1,4 @@
export { default as SendMessageModal } from './components/modals/SendMessageModal.vue'
export { default as ServiceDetailsModal } from './components/modals/ServiceDetailsModal.vue'
export { default as ServiceItem } from './components/ServiceItem.vue'
export { default as ServicesFilters } from './components/ServicesFilters.vue'
+6
View File
@@ -0,0 +1,6 @@
export { default as SessionAttendanceModal } from './components/modals/SessionAttendanceModal.vue'
export { default as SessionDetailsModal } from './components/modals/SessionDetailsModal.vue'
export { default as SessionItem } from './components/SessionItem.vue'
export { default as SessionsFilters } from './components/SessionsFilters.vue'
export * from './schema'
+8
View File
@@ -0,0 +1,8 @@
export { default as AddTermStudentModal } from './components/modals/AddTermStudentModal.vue'
export { default as AttachCourseToTermModal } from './components/modals/AttachCourseToTermModal.vue'
export { default as CourseItem } from './components/CourseItem.vue'
export { default as TermDetailsModal } from './components/modals/TermDetailsModal.vue'
export { default as TermItem } from './components/TermItem.vue'
export { default as TermsFilters } from './components/TermsFilters.vue'
export * from './schema'
@@ -65,10 +65,9 @@ import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue' import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import TermsFilters from '@/features/admin/terms/components/TermsFilters.vue' import TermsFilters from '@/features/admin/terms/components/TermsFilters.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue' import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import { CourseDetailsModal, AddOfferedCourseModal } from '@/features/admin/courses'
import TermDetailsModal from '@/features/admin/terms/components/modals/TermDetailsModal.vue' import TermDetailsModal from '@/features/admin/terms/components/modals/TermDetailsModal.vue'
import AddTermStudentModal from '@/features/admin/terms/components/modals/AddTermStudentModal.vue' import AddTermStudentModal from '@/features/admin/terms/components/modals/AddTermStudentModal.vue'
import CourseDetailsModal from '@/features/admin/courses/components/modals/CourseDetailsModal.vue'
import AddOfferedCourseModal from '@/features/admin/courses/components/modals/AddOfferedCourseModal.vue'
import AttachCourseToTermModal from '@/features/admin/terms/components/modals/AttachCourseToTermModal.vue' import AttachCourseToTermModal from '@/features/admin/terms/components/modals/AttachCourseToTermModal.vue'
import { import {
adminTermsKeys, adminTermsKeys,
@@ -26,8 +26,8 @@
</template> </template>
</TextField> </TextField>
<TextField <TextField
v-model="form.phoneNumber" v-model="form.phone"
name="phoneNumber" name="phone"
label="شماره همراه کاربر" label="شماره همراه کاربر"
inputmode="numeric" inputmode="numeric"
:convert-digits="true" :convert-digits="true"
@@ -76,7 +76,7 @@ import SelectField from '@/components/form/SelectField.vue'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: Object, type: Object,
default: () => ({ name: '', nationalCode: '', phoneNumber: '', role: '' }), default: () => ({ name: '', nationalCode: '', phone: '', role: '' }),
}, },
}) })
@@ -101,7 +101,7 @@ const onSubmit = () => {
} }
const onReset = () => { const onReset = () => {
form.value = { name: '', nationalCode: '', phoneNumber: '', role: '' } form.value = { name: '', nationalCode: '', phone: '', role: '' }
emit('update:modelValue', { ...form.value }) emit('update:modelValue', { ...form.value })
emit('reset') emit('reset')
} }
@@ -2,18 +2,22 @@
<div class="users-table"> <div class="users-table">
<div v-for="user in users" :key="user.id" class="users-table__row"> <div v-for="user in users" :key="user.id" class="users-table__row">
<div class="users-table__avatar"> <div class="users-table__avatar">
<img v-if="user.avatarUrl" :src="user.avatarUrl" :alt="fullName(user)" /> <img v-if="user.avatarUrl" :src="user.avatarUrl" :alt="user?.name" />
<SvgIcon v-else name="user" :size="36" color="#cbd5e1" /> <SvgIcon v-else name="user" :size="36" color="#cbd5e1" />
</div> </div>
<div class="users-table__info"> <div class="users-table__info">
<p class="users-table__name">{{ fullName(user) || '—' }}</p> <p class="users-table__name">{{ user?.name || '—' }}</p>
<div class="users-table__meta"> <div class="users-table__meta">
<span v-if="user.phoneNumber">{{ user.phoneNumber }}</span> <span v-if="user.phone">{{ user.phone }}</span>
<span v-if="user.nationalCode" class="users-table__sep">|</span> <span v-if="user.nationalCode" class="users-table__sep">|</span>
<span v-if="user.nationalCode">کد ملی: {{ user.nationalCode }}</span> <span v-if="user.nationalCode">کد ملی: {{ user.nationalCode }}</span>
</div> </div>
</div> </div>
<Badge v-if="user.status === 'blocked'" variant="danger" size="sm" dot>
{{ USER_STATUS.blocked }}
</Badge>
<button <button
type="button" type="button"
class="users-table__role-badge" class="users-table__role-badge"
@@ -66,8 +70,9 @@
</template> </template>
<script setup> <script setup>
import { ROLES } from '@/enums'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import Badge from '@/components/Badge.vue'
import { ROLES, USER_STATUS } from '@/enums'
import { useQueryClient } from '@tanstack/vue-query' import { useQueryClient } from '@tanstack/vue-query'
import SvgIcon from '@/components/icons/SvgIcon.vue' import SvgIcon from '@/components/icons/SvgIcon.vue'
import BaseButton from '@/components/BaseButton.vue' import BaseButton from '@/components/BaseButton.vue'
@@ -80,17 +85,7 @@ defineProps({
const queryClient = useQueryClient() const queryClient = useQueryClient()
const emit = defineEmits([ const emit = defineEmits(['edit', 'delete', 'block', 'unblock', 'show-details'])
'edit',
'delete',
'block',
'show-details',
'show-assignments',
'show-exams',
'export-pdf',
])
const fullName = (user) => `${user.firstName || ''} ${user.lastName || ''}`.trim()
const roleName = (user) => { const roleName = (user) => {
const first = user.roles?.[0] const first = user.roles?.[0]
@@ -155,45 +150,38 @@ const roleMenuItems = computed(() => {
})) }))
}) })
const menuItems = computed(() => [ const menuItems = computed(() => {
{ const user = activeUser.value
key: 'edit', const isBlocked = user?.status === 'blocked'
label: 'ویرایش', return [
icon: 'pencil', {
onClick: () => emit('edit', activeUser.value), key: 'edit',
}, label: 'ویرایش',
{ icon: 'pencil',
key: 'delete', onClick: () => emit('edit', user),
label: 'حذف', },
icon: 'trash', {
onClick: () => emit('delete', activeUser.value), key: 'delete',
}, label: 'حذف',
{ icon: 'trash',
key: 'block', onClick: () => emit('delete', user),
label: 'مسدود کردن کاربر', },
icon: 'close', isBlocked
danger: true, ? {
onClick: () => emit('block', activeUser.value), key: 'unblock',
}, label: 'رفع مسدودیت کاربر',
{ icon: 'check',
key: 'show-assignments', onClick: () => emit('unblock', user),
label: 'جزئیات تکلیف', }
icon: 'file', : {
onClick: () => emit('show-assignments', activeUser.value), key: 'block',
}, label: 'مسدود کردن کاربر',
{ icon: 'close',
key: 'show-exams', danger: true,
label: 'جزئیات آزمون', onClick: () => emit('block', user),
icon: 'file', },
onClick: () => emit('show-exams', activeUser.value), ]
}, })
{
key: 'export-pdf',
label: 'خروجی PDF',
icon: 'upload',
onClick: () => emit('export-pdf', activeUser.value),
},
])
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -40,11 +40,14 @@
<div class="user-details__grid"> <div class="user-details__grid">
<LineInfoBlock <LineInfoBlock
title="تاریخ تولد" title="تاریخ تولد"
:numeric-desc="user.faBirthDate || formatJalaaliDate(user.birthDate) || ''" :numeric-desc="formatJalaaliDate(user.birthday) || ''"
/> />
<LineInfoBlock title="شماره تماس" :numeric-desc="user.phoneNumber || ''" /> <LineInfoBlock title="شماره تماس" :numeric-desc="user.phone || ''" />
<LineInfoBlock title="کد ملی" :numeric-desc="user.nationalCode || ''" /> <LineInfoBlock title="کد ملی" :numeric-desc="user.nationalCode || ''" />
<LineInfoBlock title="وضعیت تاهل" :desc="MARITAL_STATUS[user.maritalStatus] || '—'" /> <LineInfoBlock
title="وضعیت تاهل"
:desc="MARITAL_STATUS[user.marriageStatus] || '—'"
/>
<LineInfoBlock title="جنسیت" :desc="GENDER[user.gender] || '—'" /> <LineInfoBlock title="جنسیت" :desc="GENDER[user.gender] || '—'" />
<LineInfoBlock title="استان" :desc="user?.province?.name || '—'" /> <LineInfoBlock title="استان" :desc="user?.province?.name || '—'" />
<LineInfoBlock title="شهر" :desc="user?.city?.name || '—'" /> <LineInfoBlock title="شهر" :desc="user?.city?.name || '—'" />
@@ -122,9 +125,7 @@ const { data: user, isLoading } = useAdminUserQuery(userId, {
enabled: () => !!userId.value, enabled: () => !!userId.value,
}) })
const fullName = computed(() => const fullName = computed(() => `${user.value?.name || ''}`.trim())
`${user.value?.firstName || ''} ${user.value?.lastName || ''}`.trim()
)
const roleName = computed(() => { const roleName = computed(() => {
const first = user.value?.roles?.[0] const first = user.value?.roles?.[0]
+12
View File
@@ -0,0 +1,12 @@
export { default as RejectReasonDetailsModal } from './components/verifications/modals/RejectReasonDetailsModal.vue'
export { default as RejectReasonModal } from './components/verifications/modals/RejectReasonModal.vue'
export { default as UserAssignmentsModal } from './components/modals/UserAssignmentsModal.vue'
export { default as UserDetailsModal } from './components/modals/UserDetailsModal.vue'
export { default as UserExamsModal } from './components/modals/UserExamsModal.vue'
export { default as UserVerificationDetailsModal } from './components/verifications/modals/UserVerificationDetailsModal.vue'
export { default as UsersFilters } from './components/UsersFilters.vue'
export { default as UsersTable } from './components/UsersTable.vue'
export { default as VerificationItem } from './components/verifications/VerificationItem.vue'
export { default as VerificationsFilters } from './components/verifications/VerificationsFilters.vue'
export * from './schema'
+73 -68
View File
@@ -29,36 +29,23 @@
<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.roleId" v-model="form.role"
name="roleId" name="role"
label="نقش" label="نقش"
:options="roleOptions" :options="ROLES"
option-label="label" option-label="label"
option-value="id" option-value="value"
:error="errors.roleId" :error="errors.role"
@change="validateAt('roleId', form.roleId)" @change="validateAt('role', form.role)"
/> />
</div> </div>
<div class="user-form__cell user-form__cell--third"> <div class="user-form__cell user-form__cell--third">
<TextField <TextField
v-model="form.firstName" v-model="form.name"
name="firstName" name="name"
label="نام" label="نام و نام خانوادگی"
:error="errors.firstName" :error="errors.name"
@blur="validateAt('firstName', form.firstName)" @blur="validateAt('name', form.name)"
>
<template #appendIcon>
<SvgIcon name="user" :size="20" color="var(--color-thd-gray)" />
</template>
</TextField>
</div>
<div class="user-form__cell user-form__cell--third">
<TextField
v-model="form.lastName"
name="lastName"
label="نام خانوادگی"
:error="errors.lastName"
@blur="validateAt('lastName', form.lastName)"
> >
<template #appendIcon> <template #appendIcon>
<SvgIcon name="user" :size="20" color="var(--color-thd-gray)" /> <SvgIcon name="user" :size="20" color="var(--color-thd-gray)" />
@@ -67,11 +54,11 @@
</div> </div>
<div class="user-form__cell user-form__cell--third"> <div class="user-form__cell user-form__cell--third">
<DatePickerField <DatePickerField
v-model="form.birthDate" v-model="form.birthday"
name="birthDate" name="birthday"
label="تاریخ تولد" label="تاریخ تولد"
:max="todayIso" :max="todayIso"
:error="errors.birthDate" :error="errors.birthday"
/> />
</div> </div>
<div class="user-form__cell user-form__cell--third"> <div class="user-form__cell user-form__cell--third">
@@ -125,13 +112,13 @@
<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">
<TextField <TextField
v-model="form.phoneNumber" v-model="form.phone"
name="phoneNumber" name="phone"
label="شماره تلفن همراه" label="شماره تلفن همراه"
inputmode="numeric" inputmode="numeric"
:convert-digits="true" :convert-digits="true"
:error="errors.phoneNumber" :error="errors.phone"
@blur="validateAt('phoneNumber', form.phoneNumber)" @blur="validateAt('phone', form.phone)"
> >
<template #appendIcon> <template #appendIcon>
<SvgIcon name="phone" :size="20" color="var(--color-thd-gray)" /> <SvgIcon name="phone" :size="20" color="var(--color-thd-gray)" />
@@ -226,10 +213,10 @@ import { useQueryClient } from '@tanstack/vue-query'
import BaseButton from '@/components/BaseButton.vue' import BaseButton from '@/components/BaseButton.vue'
import SvgIcon from '@/components/icons/SvgIcon.vue' import SvgIcon from '@/components/icons/SvgIcon.vue'
import TextField from '@/components/form/TextField.vue' import TextField from '@/components/form/TextField.vue'
import { GENDER, MARITAL_STATUS, ROLES } from '@/enums'
import SelectField from '@/components/form/SelectField.vue' import SelectField from '@/components/form/SelectField.vue'
import LineTitleBlock from '@/components/LineTitleBlock.vue' import LineTitleBlock from '@/components/LineTitleBlock.vue'
import ImageCropper from '@/components/form/ImageCropper.vue' import ImageCropper from '@/components/form/ImageCropper.vue'
import { GENDER, MARITAL_STATUS, ROLE_LABELS } from '@/enums'
import { objectToFormData } from '@/utils/object-to-formdata' import { objectToFormData } from '@/utils/object-to-formdata'
import { useUploadMediaMutation } from '@/services/query/auth' import { useUploadMediaMutation } from '@/services/query/auth'
import TextareaField from '@/components/form/TextareaField.vue' import TextareaField from '@/components/form/TextareaField.vue'
@@ -242,7 +229,6 @@ import { useGetCitiesOfProvinceQuery, useGetProvincesQuery } from '@/services/qu
import { import {
adminUsersKeys, adminUsersKeys,
useAddAdminUserMutation, useAddAdminUserMutation,
useAdminRolesListQuery,
useAdminUserQuery, useAdminUserQuery,
useUpdateAdminUserMutation, useUpdateAdminUserMutation,
} from '@/services/query/admin-users' } from '@/services/query/admin-users'
@@ -269,10 +255,6 @@ const maritalStatusOptions = Object.entries(MARITAL_STATUS).map(([value, label])
})) }))
const genderOptions = Object.entries(GENDER).map(([value, label]) => ({ value, label })) const genderOptions = Object.entries(GENDER).map(([value, label]) => ({ value, label }))
const { data: roles = ref([]) } = useAdminRolesListQuery()
const roleOptions = computed(() =>
(roles.value ?? []).map((r) => ({ ...r, label: ROLE_LABELS[r.name] || r.name }))
)
const { data: provinces = ref([]) } = useGetProvincesQuery() const { data: provinces = ref([]) } = useGetProvincesQuery()
const provinceIdRef = ref(null) const provinceIdRef = ref(null)
@@ -283,15 +265,14 @@ const { data: cities = ref([]) } = useGetCitiesOfProvinceQuery(provinceIdRef, {
const initialNationalCode = ref('') const initialNationalCode = ref('')
const form = ref({ const form = ref({
roleId: '', role: '',
firstName: '', name: '',
lastName: '', birthday: '',
birthDate: '',
nationalCode: '', nationalCode: '',
maritalStatus: '', maritalStatus: '',
gender: '', gender: '',
bio: '', bio: '',
phoneNumber: '', phone: '',
provinceId: '', provinceId: '',
cityId: '', cityId: '',
address: '', address: '',
@@ -322,31 +303,34 @@ const { data: existingUser } = useAdminUserQuery(userId, {
enabled: () => !!userId.value, enabled: () => !!userId.value,
}) })
watch(existingUser, (user) => { watch(
if (!user) return existingUser,
const firstRole = Array.isArray(user.roles) ? user.roles[0] : null (user) => {
const firstRoleId = typeof firstRole === 'object' ? firstRole?.id : null if (!user) return
form.value = { const firstRole = Array.isArray(user.roles) ? user.roles[0] : null
...form.value, const roleValue = typeof firstRole === 'object' ? firstRole?.name : firstRole
roleId: user.roleId || user.profile?.roleId || firstRoleId || '', form.value = {
firstName: user.firstName || '', ...form.value,
lastName: user.lastName || '', role: roleValue || '',
birthDate: user.profile?.birthDate || '', name: user.name || `${user.firstName || ''} ${user.lastName || ''}`.trim(),
nationalCode: user.nationalCode || '', birthday: user.birthday || '',
maritalStatus: user.profile?.maritalStatus || '', nationalCode: user.nationalCode || '',
gender: user.profile?.gender || '', maritalStatus: user.marriageStatus ?? user.profile?.maritalStatus ?? '',
bio: user.profile?.bio || '', gender: user.gender ?? user.profile?.gender ?? '',
phoneNumber: user.phoneNumber || '', bio: user.bio ?? user.profile?.bio ?? '',
provinceId: user?.province?.id || '', phone: user.phone || '',
cityId: user?.city?.id || '', provinceId: user?.province?.id || '',
address: user?.address || '', cityId: user?.city?.id || '',
avatarId: user.profile?.avatarId || null, address: typeof user.address === 'string' ? user.address : user.address?.address || '',
} avatarId: user.avatarId ?? user.profile?.avatarId ?? null,
initialNationalCode.value = user.nationalCode || '' }
if (user.avatarUrl) { initialNationalCode.value = user.nationalCode || ''
avatar.value = { url: user.avatarUrl } if (user.avatarUrl) {
} avatar.value = { url: user.avatarUrl }
}) }
},
{ immediate: true }
)
const uploadMutation = useUploadMediaMutation() const uploadMutation = useUploadMediaMutation()
@@ -369,11 +353,32 @@ const updateMutation = useUpdateAdminUserMutation()
const submitting = computed(() => addMutation.isPending.value || updateMutation.isPending.value) const submitting = computed(() => addMutation.isPending.value || updateMutation.isPending.value)
// Maps the form to the backend Update User contract:
// { name, roles, province_id, city_id, marriage_status, birthday, gender,
// address, national_code } (snake-case conversion happens in the http layer),
// plus phone/bio/avatar that the form also edits.
const buildUpdatePayload = () => {
return {
name: form.value.name,
roles: form.value.role ? [form.value.role] : [],
provinceId: form.value.provinceId || null,
cityId: form.value.cityId || null,
marriageStatus: form.value.maritalStatus || null,
birthday: form.value.birthday || null,
gender: form.value.gender || null,
address: form.value.address || '',
nationalCode: form.value.nationalCode,
phone: form.value.phone,
bio: form.value.bio || '',
avatarId: form.value.avatarId || null,
}
}
const onSubmit = async () => { const onSubmit = async () => {
const { isValid, payload } = await validate(form.value) const { isValid, payload } = await validate(form.value)
if (!isValid) return if (!isValid) return
if (isEditMode.value) { if (isEditMode.value) {
await updateMutation.mutateAsync({ id: userId.value, payload }) await updateMutation.mutateAsync({ id: userId.value, payload: buildUpdatePayload() })
} else { } else {
await addMutation.mutateAsync(payload) await addMutation.mutateAsync(payload)
} }
@@ -31,17 +31,15 @@
:users="users" :users="users"
@edit="onEdit" @edit="onEdit"
@delete="onAskDelete" @delete="onAskDelete"
@block="onAskBlock"
@unblock="onAskUnblock"
@show-details="onShowDetails" @show-details="onShowDetails"
@show-assignments="onShowAssignments"
@show-exams="onShowExams"
/> />
<NoItems v-else title="متاسفیم" desc="کاربری برای نمایش وجود ندارد." /> <NoItems v-else title="متاسفیم" desc="کاربری برای نمایش وجود ندارد." />
<PaginationBlock :pagination="paginationMeta" @update:page="setPage" /> <PaginationBlock :pagination="paginationMeta" @update:page="setPage" />
<UserDetailsModal v-if="isModal('UserDetailsModal')" /> <UserDetailsModal v-if="isModal('UserDetailsModal')" />
<UserAssignmentsModal v-if="isModal('UserAssignmentsModal')" />
<UserExamsModal v-if="isModal('UserExamsModal')" />
</div> </div>
</template> </template>
@@ -60,12 +58,11 @@ import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue' import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import UsersFilters from '@/features/admin/users/components/UsersFilters.vue' import UsersFilters from '@/features/admin/users/components/UsersFilters.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue' import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import UserExamsModal from '@/features/admin/users/components/modals/UserExamsModal.vue'
import UserDetailsModal from '@/features/admin/users/components/modals/UserDetailsModal.vue' import UserDetailsModal from '@/features/admin/users/components/modals/UserDetailsModal.vue'
import UserAssignmentsModal from '@/features/admin/users/components/modals/UserAssignmentsModal.vue'
import { import {
adminUsersKeys, adminUsersKeys,
useAdminUsersListQuery, useAdminUsersListQuery,
useChangeAdminUserStatusMutation,
useDeleteAdminUserMutation, useDeleteAdminUserMutation,
} from '@/services/query/admin-users' } from '@/services/query/admin-users'
@@ -73,7 +70,7 @@ const router = useRouter()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const { openModal, isModal } = useModal() const { openModal, isModal } = useModal()
const filters = ref({ name: '', nationalCode: '', phoneNumber: '', role: '' }) const filters = ref({ name: '', nationalCode: '', phone: '', role: '' })
const { pagination, setPage, reset: resetPagination } = usePagination({ page: 1, perPage: 10 }) const { pagination, setPage, reset: resetPagination } = usePagination({ page: 1, perPage: 10 })
const { data, isLoading } = useAdminUsersListQuery(filters, pagination, { const { data, isLoading } = useAdminUsersListQuery(filters, pagination, {
@@ -99,22 +96,38 @@ const onEdit = (user) => {
} }
const onShowDetails = (user) => openModal('UserDetailsModal', { id: user.id }) const onShowDetails = (user) => openModal('UserDetailsModal', { id: user.id })
const onShowAssignments = (user) => openModal('UserAssignmentsModal', { id: user.id })
const onShowExams = (user) => openModal('UserExamsModal', { id: user.id }) const invalidateUsers = () => queryClient.invalidateQueries({ queryKey: adminUsersKeys.all })
const userName = (user) =>
user.name || `${user.firstName || ''} ${user.lastName || ''}`.trim() || user.phone
const deleteMutation = useDeleteAdminUserMutation() const deleteMutation = useDeleteAdminUserMutation()
const onAskDelete = (user) => { const onAskDelete = (user) => {
const name = `${user.firstName || ''} ${user.lastName || ''}`.trim() || user.phoneNumber
openModal('ConfirmModal', { openModal('ConfirmModal', {
title: 'حذف کاربر', title: 'حذف کاربر',
message: `آیا از حذف <strong>${name}</strong> مطمئن هستید؟`, message: `آیا از حذف <strong>${userName(user)}</strong> مطمئن هستید؟`,
onConfirm: () => onConfirm: () => deleteMutation.mutate(user.id, { onSuccess: invalidateUsers }),
deleteMutation.mutate(user.id, {
onSuccess: () => queryClient.invalidateQueries({ queryKey: adminUsersKeys.all }),
}),
}) })
} }
const statusMutation = useChangeAdminUserStatusMutation()
const askStatusChange = (user, { status, title, verb }) => {
openModal('ConfirmModal', {
title,
message: `آیا از ${verb} <strong>${userName(user)}</strong> مطمئن هستید؟`,
onConfirm: () =>
statusMutation.mutate({ id: user.id, payload: { status } }, { onSuccess: invalidateUsers }),
})
}
const onAskBlock = (user) =>
askStatusChange(user, { status: 'blocked', title: 'مسدود کردن کاربر', verb: 'مسدود کردن' })
const onAskUnblock = (user) =>
askStatusChange(user, { status: 'approved', title: 'رفع مسدودیت کاربر', verb: 'رفع مسدودیت' })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
+4 -5
View File
@@ -16,12 +16,11 @@ export const rejectionReasonSchema = object().shape({
}) })
const baseUserShape = { const baseUserShape = {
roleId: string().required(), role: string().required(),
firstName: string().required().min(2), name: string().required().min(3),
lastName: string().required().min(2), phone: phoneNumberRule,
phoneNumber: phoneNumberRule,
nationalCode: nationalCodeRule, nationalCode: nationalCodeRule,
birthDate: string().nullable().notRequired(), birthday: string().nullable().notRequired(),
maritalStatus: string().nullable().notRequired(), maritalStatus: string().nullable().notRequired(),
gender: string().nullable().notRequired(), gender: string().nullable().notRequired(),
bio: string().nullable().notRequired(), bio: string().nullable().notRequired(),
+6 -6
View File
@@ -4,13 +4,13 @@
<AuthHeading title="ورود" subtitle="در این قسمت اطلاعات خود را وارد کنید." /> <AuthHeading title="ورود" subtitle="در این قسمت اطلاعات خود را وارد کنید." />
<TextField <TextField
v-model="form.phoneNumber" v-model="form.phone"
name="phoneNumber" name="phone"
label="شماره تلفن همراه" label="شماره تلفن همراه"
inputmode="numeric" inputmode="numeric"
:convert-digits="true" :convert-digits="true"
:error="errors.phoneNumber" :error="errors.phone"
@blur="validateAt('phoneNumber', form.phoneNumber)" @blur="validateAt('phone', form.phone)"
> >
<template #appendIcon> <template #appendIcon>
<SvgIcon name="phone" :size="22" color="var(--color-thd-gray)" /> <SvgIcon name="phone" :size="22" color="var(--color-thd-gray)" />
@@ -81,7 +81,7 @@ const emit = defineEmits(['next'])
const schema = loginSchema const schema = loginSchema
const form = ref({ phoneNumber: '', password: '' }) const form = ref({ phone: '', password: '' })
const { validate, validateAt, errors } = useYup(schema) const { validate, validateAt, errors } = useYup(schema)
const loginMutation = useLoginMutation() const loginMutation = useLoginMutation()
@@ -92,7 +92,7 @@ const onSubmit = async () => {
const { isValid, payload } = await validate(form.value) const { isValid, payload } = await validate(form.value)
if (!isValid) return if (!isValid) return
const response = await loginMutation.mutateAsync({ const response = await loginMutation.mutateAsync({
phone: payload.phoneNumber, phone: payload.phone,
password: payload.password, password: payload.password,
deviceName: 'web', deviceName: 'web',
}) })
@@ -11,13 +11,13 @@
/> />
<TextField <TextField
v-model="form.phoneNumber" v-model="form.phone"
name="phoneNumber" name="phone"
label="شماره تلفن همراه" label="شماره تلفن همراه"
inputmode="numeric" inputmode="numeric"
:convert-digits="true" :convert-digits="true"
:error="errors.phoneNumber" :error="errors.phone"
@blur="validateAt('phoneNumber', form.phoneNumber)" @blur="validateAt('phone', form.phone)"
> >
<template #appendIcon> <template #appendIcon>
<SvgIcon name="phone" :size="22" color="var(--color-thd-gray)" /> <SvgIcon name="phone" :size="22" color="var(--color-thd-gray)" />
@@ -67,7 +67,7 @@ const emit = defineEmits(['next', 'back'])
const isForgotPassword = computed(() => props.mode === 'forgotPassword') const isForgotPassword = computed(() => props.mode === 'forgotPassword')
const schema = loginWithCodeSchema const schema = loginWithCodeSchema
const form = ref({ phoneNumber: '' }) const form = ref({ phone: '' })
const { validate, validateAt, errors } = useYup(schema) const { validate, validateAt, errors } = useYup(schema)
const { savePhoneNumber } = useAuth() const { savePhoneNumber } = useAuth()
@@ -86,8 +86,8 @@ const onSubmit = async () => {
} else { } else {
await loginCodeMutation.mutateAsync(payload) await loginCodeMutation.mutateAsync(payload)
} }
savePhoneNumber(payload.phoneNumber) savePhoneNumber(payload.phone)
emit('next', { phoneNumber: payload.phoneNumber, mode: props.mode }) emit('next', { phone: payload.phone, mode: props.mode })
} }
</script> </script>
+8 -8
View File
@@ -28,13 +28,13 @@
</TextField> </TextField>
<TextField <TextField
v-model="form.phoneNumber" v-model="form.phone"
name="phoneNumber" name="phone"
label="شماره موبایل" label="شماره موبایل"
inputmode="numeric" inputmode="numeric"
:convert-digits="true" :convert-digits="true"
:error="errors.phoneNumber" :error="errors.phone"
@blur="validateAt('phoneNumber', form.phoneNumber)" @blur="validateAt('phone', form.phone)"
> >
<template #appendIcon> <template #appendIcon>
<SvgIcon name="phone" :size="22" color="var(--color-thd-gray)" /> <SvgIcon name="phone" :size="22" color="var(--color-thd-gray)" />
@@ -102,7 +102,7 @@ const schema = signupSchema
const form = ref({ const form = ref({
firstName: '', firstName: '',
lastName: '', lastName: '',
phoneNumber: '', phone: '',
password: '', password: '',
passwordConfirmation: '', passwordConfirmation: '',
}) })
@@ -117,12 +117,12 @@ const onSubmit = async () => {
const name = [payload.firstName, payload.lastName].filter(Boolean).join(' ').trim() const name = [payload.firstName, payload.lastName].filter(Boolean).join(' ').trim()
await registerMutation.mutateAsync({ await registerMutation.mutateAsync({
name, name,
phone: payload.phoneNumber, phone: payload.phone,
password: payload.password, password: payload.password,
passwordConfirmation: payload.passwordConfirmation, passwordConfirmation: payload.passwordConfirmation,
}) })
savePhoneNumber(payload.phoneNumber) savePhoneNumber(payload.phone)
emit('next', { phoneNumber: payload.phoneNumber }) emit('next', { phone: payload.phone })
} }
</script> </script>
@@ -103,7 +103,7 @@ const resending = computed(
const onSubmit = async () => { const onSubmit = async () => {
const { isValid, payload } = await validate(form.value) const { isValid, payload } = await validate(form.value)
if (!isValid) return if (!isValid) return
const body = { phoneNumber: props.phoneNumber, verifyCode: payload.verifyCode } const body = { phone: props.phoneNumber, verifyCode: payload.verifyCode }
if (props.mode === 'loginWithCode') { if (props.mode === 'loginWithCode') {
const response = await verifyLoginMutation.mutateAsync(body) const response = await verifyLoginMutation.mutateAsync(body)
@@ -93,14 +93,14 @@
<div class="personal-info__row"> <div class="personal-info__row">
<div class="personal-info__cell personal-info__cell--full-sm"> <div class="personal-info__cell personal-info__cell--full-sm">
<TextField <TextField
v-model="form.phoneNumber" v-model="form.phone"
name="phoneNumber" name="phone"
label="شماره تلفن همراه" label="شماره تلفن همراه"
inputmode="numeric" inputmode="numeric"
:convert-digits="true" :convert-digits="true"
:disabled="!!initialPhoneNumber" :disabled="!!initialPhone"
:error="errors.phoneNumber" :error="errors.phone"
@blur="validateAt('phoneNumber', form.phoneNumber)" @blur="validateAt('phone', form.phone)"
> >
<template #appendIcon> <template #appendIcon>
<SvgIcon name="phone" :size="20" color="var(--color-thd-gray)" /> <SvgIcon name="phone" :size="20" color="var(--color-thd-gray)" />
@@ -206,9 +206,7 @@ const maxBirthDate = computed(() => {
} }
}) })
const initialPhoneNumber = ref( const initialPhone = ref(user.value?.phone || store.personal.phone || '')
user.value?.phone || user.value?.phoneNumber || store.personal.phoneNumber || ''
)
const form = ref({ const form = ref({
firstName: store.personal.firstName || '', firstName: store.personal.firstName || '',
@@ -217,7 +215,7 @@ const form = ref({
nationalCode: store.personal.nationalCode || '', nationalCode: store.personal.nationalCode || '',
maritalStatus: store.personal.maritalStatus || '', maritalStatus: store.personal.maritalStatus || '',
gender: store.personal.gender || '', gender: store.personal.gender || '',
phoneNumber: initialPhoneNumber.value, phone: initialPhone.value,
provinceId: store.personal.provinceId || null, provinceId: store.personal.provinceId || null,
cityId: store.personal.cityId || null, cityId: store.personal.cityId || null,
address: store.personal.address || '', address: store.personal.address || '',
+26
View File
@@ -0,0 +1,26 @@
export { default as AuthCopyright } from './components/AuthCopyright.vue'
export { default as AuthHeading } from './components/AuthHeading.vue'
export { default as AuthShell } from './components/AuthShell.vue'
export { default as EducationalInformation } from './components/studentRegister/EducationalInformation.vue'
export { default as LoginForm } from './components/LoginForm.vue'
export { default as LoginWithVerifyCodeForm } from './components/LoginWithVerifyCodeForm.vue'
export { default as MediaRecorder } from './components/studentRegister/MediaRecorder.vue'
export { default as PatternSection } from './components/PatternSection.vue'
export { default as PermissionModal } from './components/studentRegister/PermissionModal.vue'
export { default as PersonalInformation } from './components/studentRegister/PersonalInformation.vue'
export { default as ProfessionalInformation } from './components/studentRegister/ProfessionalInformation.vue'
export { default as SideBarSteps } from './components/studentRegister/SideBarSteps.vue'
export { default as SignupForm } from './components/SignupForm.vue'
export { default as SkillAssessmentPart1 } from './components/studentRegister/SkillAssessmentPart1.vue'
export { default as SkillAssessmentPart2 } from './components/studentRegister/SkillAssessmentPart2.vue'
export { default as SkillAssessmentPart3 } from './components/studentRegister/SkillAssessmentPart3.vue'
export { default as SkillAssessmentPart4 } from './components/studentRegister/SkillAssessmentPart4.vue'
export { default as SkillAssessmentPart5 } from './components/studentRegister/SkillAssessmentPart5.vue'
export { default as SkillAssessmentPart6 } from './components/studentRegister/SkillAssessmentPart6.vue'
export { default as SkillAssessmentPart7 } from './components/studentRegister/SkillAssessmentPart7.vue'
export { default as SkillSelectStep } from './components/studentRegister/SkillSelectStep.vue'
export { default as StepActions } from './components/studentRegister/StepActions.vue'
export { default as SuccessModal } from './components/studentRegister/SuccessModal.vue'
export { default as VerifyCodeForm } from './components/VerifyCodeForm.vue'
export * from './schema'
@@ -5,10 +5,10 @@
<AuthHeading title="تغییر رمز عبور" subtitle="در این قسمت رمز عبور جدید خود را وارد کنید" /> <AuthHeading title="تغییر رمز عبور" subtitle="در این قسمت رمز عبور جدید خود را وارد کنید" />
<TextField <TextField
v-model="form.phoneNumber" v-model="form.phone"
name="phoneNumber" name="phone"
label="شماره موبایل" label="شماره موبایل"
:disabled="!!(user?.phone || user?.phoneNumber)" :disabled="!!(user?.phone || user?.phone)"
> >
<template #appendIcon> <template #appendIcon>
<SvgIcon name="phone" :size="22" color="var(--color-thd-gray)" /> <SvgIcon name="phone" :size="22" color="var(--color-thd-gray)" />
@@ -71,7 +71,7 @@ const redirectAfterLogin = useRedirectAfterLogin()
const schema = resetPasswordSchema const schema = resetPasswordSchema
const form = ref({ const form = ref({
phoneNumber: user.value?.phone || user.value?.phoneNumber || '', phone: user.value?.phone || '',
password: '', password: '',
passwordConfirmation: '', passwordConfirmation: '',
}) })
+3 -3
View File
@@ -4,18 +4,18 @@ import { phoneNumberRule } from '@/constants/rules/phoneNumberRule'
import { passwordConfirmationRule } from '@/constants/rules/passwordConfirmationRule' import { passwordConfirmationRule } from '@/constants/rules/passwordConfirmationRule'
export const loginSchema = object().shape({ export const loginSchema = object().shape({
phoneNumber: phoneNumberRule, phone: phoneNumberRule,
password: string().required(), password: string().required(),
}) })
export const loginWithCodeSchema = object().shape({ export const loginWithCodeSchema = object().shape({
phoneNumber: phoneNumberRule, phone: phoneNumberRule,
}) })
export const signupSchema = object().shape({ export const signupSchema = object().shape({
firstName: string().required().min(3), firstName: string().required().min(3),
lastName: string().required().min(3), lastName: string().required().min(3),
phoneNumber: phoneNumberRule, phone: phoneNumberRule,
password: passwordRule, password: passwordRule,
passwordConfirmation: passwordConfirmationRule, passwordConfirmation: passwordConfirmationRule,
}) })
+1
View File
@@ -0,0 +1 @@
export { default as CounselorConsultationDetailsModal } from './components/modals/CounselorConsultationDetailsModal.vue'
@@ -50,8 +50,7 @@ import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue' import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue' import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue' import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import ConsultationItem from '@/features/admin/consultations/components/ConsultationItem.vue' import { ConsultationItem, ConsultationsFilters } from '@/features/admin/consultations'
import ConsultationsFilters from '@/features/admin/consultations/components/ConsultationsFilters.vue'
import CounselorConsultationDetailsModal from '@/features/counselor/components/modals/CounselorConsultationDetailsModal.vue' import CounselorConsultationDetailsModal from '@/features/counselor/components/modals/CounselorConsultationDetailsModal.vue'
import { import {
counselorTicketsKeys, counselorTicketsKeys,
@@ -62,7 +61,7 @@ import {
const { openModal, isModal } = useModal() const { openModal, isModal } = useModal()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const filters = ref({ type: 'advise', userName: '', status: '', fromDate: '', toDate: '' }) const filters = ref({ userName: '', status: '', fromDate: '', toDate: '' })
const { pagination, setPage, reset: resetPagination } = usePagination({ page: 1, perPage: 10 }) const { pagination, setPage, reset: resetPagination } = usePagination({ page: 1, perPage: 10 })
const { data, isLoading } = useCounselorTicketsListQuery(filters, pagination, { const { data, isLoading } = useCounselorTicketsListQuery(filters, pagination, {
+4
View File
@@ -0,0 +1,4 @@
export { default as ItemDetailsModal } from './components/ItemDetailsModal.vue'
export { default as SampleModal } from './components/SampleModal.vue'
export * from './schema'
@@ -28,15 +28,15 @@
@blur="validateAt('fullName', form.fullName)" @blur="validateAt('fullName', form.fullName)"
/> />
<TextField <TextField
v-model="form.phoneNumber" v-model="form.phone"
name="phoneNumber" name="phone"
label="شماره موبایل" label="شماره موبایل"
placeholder="09xxxxxxxxx" placeholder="09xxxxxxxxx"
inputmode="numeric" inputmode="numeric"
:convert-digits="true" :convert-digits="true"
:error="errors.phoneNumber" :error="errors.phone"
:vibration="true" :vibration="true"
@blur="validateAt('phoneNumber', form.phoneNumber)" @blur="validateAt('phone', form.phone)"
/> />
<TextField <TextField
v-model="form.nationalCode" v-model="form.nationalCode"
@@ -295,7 +295,7 @@ const onCitySearch = (q) => {
const form = ref({ const form = ref({
fullName: '', fullName: '',
phoneNumber: '', phone: '',
nationalCode: '', nationalCode: '',
disabled: 'فقط برای نمایش', disabled: 'فقط برای نمایش',
bio: '', bio: '',
@@ -344,7 +344,7 @@ const onSubmit = async () => {
const reset = () => { const reset = () => {
form.value = { form.value = {
fullName: '', fullName: '',
phoneNumber: '', phone: '',
disabled: 'فقط برای نمایش', disabled: 'فقط برای نمایش',
bio: '', bio: '',
province: '', province: '',
@@ -0,0 +1 @@
export { default as CertificateItem } from './components/CertificateItem.vue'
@@ -0,0 +1,2 @@
export { default as ConsultantItem } from './components/ConsultantItem.vue'
export { default as RequestConsultantForm } from './components/RequestConsultantForm.vue'
+1
View File
@@ -0,0 +1 @@
export { default as StudentCourseItem } from './components/StudentCourseItem.vue'
+4
View File
@@ -0,0 +1,4 @@
export { default as StudentExamAttemptItem } from './components/StudentExamAttemptItem.vue'
export { default as StudentExamItem } from './components/StudentExamItem.vue'
export { default as StudentExamQuestion } from './components/StudentExamQuestion.vue'
export { default as StudentExamSummary } from './components/StudentExamSummary.vue'
+1
View File
@@ -0,0 +1 @@
export { default as StudentLessonItem } from './components/StudentLessonItem.vue'
@@ -98,13 +98,13 @@ import { useRoute, useRouter } from 'vue-router'
import SvgIcon from '@/components/icons/SvgIcon.vue' import SvgIcon from '@/components/icons/SvgIcon.vue'
import NoItems from '@/components/blocks/NoItems.vue' import NoItems from '@/components/blocks/NoItems.vue'
import TabsBlock from '@/components/blocks/TabsBlock.vue' import TabsBlock from '@/components/blocks/TabsBlock.vue'
import { StudentExamItem } from '@/features/student/exams'
import { StudentSessionItem } from '@/features/student/sessions'
import { useStudentCourseQuery } from '@/services/query/student-courses' import { useStudentCourseQuery } from '@/services/query/student-courses'
import { useStudentExamsListQuery } from '@/services/query/student-exams' import { useStudentExamsListQuery } from '@/services/query/student-exams'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue' import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue' import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import { useStudentSessionsListQuery } from '@/services/query/student-sessions' import { useStudentSessionsListQuery } from '@/services/query/student-sessions'
import StudentExamItem from '@/features/student/exams/components/StudentExamItem.vue'
import StudentSessionItem from '@/features/student/sessions/components/StudentSessionItem.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
+7
View File
@@ -0,0 +1,7 @@
export { default as DispatchDetailsModal } from './components/modals/DispatchDetailsModal.vue'
export { default as MissionaryDispatchItem } from './components/MissionaryDispatchItem.vue'
export { default as MissionaryNarrativeItem } from './components/MissionaryNarrativeItem.vue'
export { default as MissionaryRequestItem } from './components/MissionaryRequestItem.vue'
export { default as MissionaryUserCard } from './components/MissionaryUserCard.vue'
export { default as NarrativeDetailsModal } from './components/modals/NarrativeDetailsModal.vue'
export { default as RequestDetailsModal } from './components/modals/RequestDetailsModal.vue'
+1
View File
@@ -0,0 +1 @@
export * from './schema'
+3
View File
@@ -0,0 +1,3 @@
export { default as CreateServiceForm } from './components/CreateServiceForm.vue'
export { default as ServiceDetailsModal } from './components/modals/ServiceDetailsModal.vue'
export { default as ServiceItem } from './components/ServiceItem.vue'
+1
View File
@@ -0,0 +1 @@
export { default as StudentSessionItem } from './components/StudentSessionItem.vue'
+2
View File
@@ -0,0 +1,2 @@
export { default as StudentTermItem } from './components/StudentTermItem.vue'
export { default as StudentTermSummary } from './components/StudentTermSummary.vue'
@@ -41,13 +41,13 @@ import { computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import SvgIcon from '@/components/icons/SvgIcon.vue' import SvgIcon from '@/components/icons/SvgIcon.vue'
import NoItems from '@/components/blocks/NoItems.vue' import NoItems from '@/components/blocks/NoItems.vue'
import { StudentLessonItem } from '@/features/student/lessons'
import { useStudentTermQuery } from '@/services/query/student-terms' import { useStudentTermQuery } from '@/services/query/student-terms'
import { useStudentCoursesListQuery } from '@/services/query/student-courses' import { useStudentCoursesListQuery } from '@/services/query/student-courses'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue' import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue' import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue' import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import StudentTermSummary from '@/features/student/terms/components/StudentTermSummary.vue' import StudentTermSummary from '@/features/student/terms/components/StudentTermSummary.vue'
import StudentLessonItem from '@/features/student/lessons/components/StudentLessonItem.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
+4
View File
@@ -0,0 +1,4 @@
export { default as CreateTicketModal } from './components/modals/CreateTicketModal.vue'
export { default as StatusTile } from './components/StatusTile.vue'
export { default as TicketDetailsModal } from './components/modals/TicketDetailsModal.vue'
export { default as TicketItem } from './components/TicketItem.vue'
+1 -3
View File
@@ -14,8 +14,6 @@ export const apiUpdateAdminUserRole = (id, payload) =>
http.patch(buildUrl(endpoints.updateUserRole, { id }), payload) http.patch(buildUrl(endpoints.updateUserRole, { id }), payload)
export const apiChangeAdminUserStatus = (id, payload) => export const apiChangeAdminUserStatus = (id, payload) =>
http.post(buildUrl(endpoints.changeUserStatus, { id }), payload) http.patch(buildUrl(endpoints.changeUserStatus, { id }), payload)
export const apiDeleteAdminUser = (id) => http.delete(buildUrl(endpoints.deleteUser, { id })) export const apiDeleteAdminUser = (id) => http.delete(buildUrl(endpoints.deleteUser, { id }))
export const apiGetAdminRoles = () => http.get(endpoints.getRolesList)
-1
View File
@@ -56,7 +56,6 @@ export const endpoints = {
addNewUser: '/admin/users', addNewUser: '/admin/users',
updateUser: '/admin/users/:id', updateUser: '/admin/users/:id',
updateUserRole: '/admin/users/:id/role', updateUserRole: '/admin/users/:id/role',
getRolesList: '/admin/roles',
changeUserStatus: '/admin/users/:id/status', changeUserStatus: '/admin/users/:id/status',
deleteUser: '/admin/users/:id', deleteUser: '/admin/users/:id',
+1 -1
View File
@@ -78,6 +78,7 @@ const makeUser = (i, overrides = {}) => {
avatarUrl: '', avatarUrl: '',
avatarDownloadUrl: '', avatarDownloadUrl: '',
createdAt: new Date(Date.now() - i * 86_400_000).toISOString(), createdAt: new Date(Date.now() - i * 86_400_000).toISOString(),
birthday: new Date(1990, i % 12, ((i * 3) % 27) + 1).toISOString(),
// --- ui-only (pending backend) --- // --- ui-only (pending backend) ---
firstName, firstName,
@@ -94,7 +95,6 @@ const makeUser = (i, overrides = {}) => {
}, },
profile: { profile: {
bio: '', bio: '',
birthDate: new Date(1990, i % 12, ((i * 3) % 27) + 1).toISOString(),
maritalStatus: i % 2 ? 'single' : 'married', maritalStatus: i % 2 ? 'single' : 'married',
gender: i % 2 ? 'female' : 'male', gender: i % 2 ? 'female' : 'male',
}, },
+28 -22
View File
@@ -15,7 +15,9 @@ import {
register('GET', endpoints.getApprovedUsers, ({ query }) => { register('GET', endpoints.getApprovedUsers, ({ query }) => {
let list = filterItems(adminUsers, query, { let list = filterItems(adminUsers, query, {
name: (item, v) => name: (item, v) =>
`${item.firstName} ${item.lastName}`.toLowerCase().includes(String(v).toLowerCase()), (item.name || `${item.firstName} ${item.lastName}`)
.toLowerCase()
.includes(String(v).toLowerCase()),
nationalCode: 'includes', nationalCode: 'includes',
phoneNumber: 'includes', phoneNumber: 'includes',
roleId: (item, v) => String(item.roleId) === String(v), roleId: (item, v) => String(item.roleId) === String(v),
@@ -39,30 +41,32 @@ register('POST', endpoints.addNewUser, ({ data }) => {
const id = makeId() const id = makeId()
const roleByName = (n) => roles.find((r) => r.name === n) const roleByName = (n) => roles.find((r) => r.name === n)
const role = const role =
roles.find((r) => r.id === Number(data.roleId)) || (data.role ? roleByName(data.role) : null) ||
(Array.isArray(data.roles) && data.roles[0] ? roleByName(data.roles[0]) : null) || (Array.isArray(data.roles) && data.roles[0] ? roleByName(data.roles[0]) : null) ||
roles.find((r) => r.id === Number(data.roleId)) ||
null null
const province = data.provinceId ? { id: data.provinceId, name: '' } : null const province = data.provinceId ? { id: data.provinceId, name: '' } : null
const city = data.cityId ? { id: data.cityId, name: '' } : null const city = data.cityId ? { id: data.cityId, name: '' } : null
const firstName = data.firstName || '' const name = data.name || `${data.firstName || ''} ${data.lastName || ''}`.trim()
const lastName = data.lastName || '' const [firstName, ...rest] = name.split(' ')
const fullName = `${firstName} ${lastName}`.trim() const lastName = rest.join(' ')
const phoneNumber = data.phoneNumber || '' const phoneNumber = data.phoneNumber || ''
const user = { const user = {
// --- spec --- // --- spec ---
id, id,
name: data.name || fullName, name,
email: data.email || '', email: data.email || '',
phone: data.phone || (phoneNumber ? phoneNumber.replace(/^0/, '+98') : null), phone: data.phone || (phoneNumber ? phoneNumber.replace(/^0/, '+98') : null),
roles: Array.isArray(data.roles) ? data.roles : role ? [role.name] : [], roles: Array.isArray(data.roles) ? data.roles : role ? [role.name] : [],
avatarUrl: '', avatarUrl: '',
avatarDownloadUrl: null, avatarDownloadUrl: null,
createdAt: isoNow(), createdAt: isoNow(),
birthday: data.birthday || null,
// --- ui-only --- // --- ui-only ---
firstName, firstName,
lastName, lastName,
fullName, fullName: name,
phoneNumber, phoneNumber,
nationalCode: data.nationalCode || '', nationalCode: data.nationalCode || '',
status: 'approved', status: 'approved',
@@ -70,8 +74,7 @@ register('POST', endpoints.addNewUser, ({ data }) => {
address: { address: data.address || '', province, city }, address: { address: data.address || '', province, city },
profile: { profile: {
bio: data.bio || '', bio: data.bio || '',
birthDate: data.birthDate || '', maritalStatus: data.marriageStatus || data.maritalStatus || '',
maritalStatus: data.maritalStatus || '',
gender: data.gender || '', gender: data.gender || '',
avatarId: data.avatarId || null, avatarId: data.avatarId || null,
}, },
@@ -87,24 +90,25 @@ register('POST', endpoints.addNewUser, ({ data }) => {
register('PATCH', endpoints.updateUser, ({ params, data }) => { register('PATCH', endpoints.updateUser, ({ params, data }) => {
const roleByName = (n) => roles.find((r) => r.name === n) const roleByName = (n) => roles.find((r) => r.name === n)
const role = const role =
roles.find((r) => r.id === Number(data.roleId)) ||
(Array.isArray(data.roles) && data.roles[0] ? roleByName(data.roles[0]) : null) || (Array.isArray(data.roles) && data.roles[0] ? roleByName(data.roles[0]) : null) ||
roles.find((r) => r.id === Number(data.roleId)) ||
null null
const firstName = data.firstName ?? '' const name = data.name ?? `${data.firstName ?? ''} ${data.lastName ?? ''}`.trim()
const lastName = data.lastName ?? '' const [firstName, ...rest] = name.split(' ')
const fullName = `${firstName} ${lastName}`.trim() const lastName = rest.join(' ')
const phoneNumber = data.phoneNumber ?? '' const phoneNumber = data.phoneNumber ?? ''
const patch = { const patch = {
// --- spec --- // --- spec ---
name: data.name ?? fullName, name,
email: data.email, email: data.email,
phone: data.phone ?? (phoneNumber ? phoneNumber.replace(/^0/, '+98') : undefined), phone: data.phone ?? (phoneNumber ? phoneNumber.replace(/^0/, '+98') : undefined),
roles: Array.isArray(data.roles) ? data.roles : role ? [role.name] : [], roles: Array.isArray(data.roles) ? data.roles : role ? [role.name] : [],
birthday: data.birthday ?? null,
// --- ui-only --- // --- ui-only ---
firstName, firstName,
lastName, lastName,
fullName, fullName: name,
phoneNumber, phoneNumber,
nationalCode: data.nationalCode, nationalCode: data.nationalCode,
roleId: role?.id, roleId: role?.id,
@@ -115,8 +119,7 @@ register('PATCH', endpoints.updateUser, ({ params, data }) => {
}, },
profile: { profile: {
bio: data.bio || '', bio: data.bio || '',
birthDate: data.birthDate || '', maritalStatus: data.marriageStatus ?? data.maritalStatus ?? '',
maritalStatus: data.maritalStatus || '',
gender: data.gender || '', gender: data.gender || '',
avatarId: data.avatarId || null, avatarId: data.avatarId || null,
}, },
@@ -148,9 +151,14 @@ register('PATCH', endpoints.updateUserRole, ({ params, data }) => {
} }
}) })
register('POST', endpoints.changeUserStatus, ({ params, data }) => ({ register('PATCH', endpoints.changeUserStatus, ({ params, data }) => {
data: updateById(adminUsers, params.id, { status: data.status }), const updated = updateById(adminUsers, params.id, { status: data.status })
})) return {
success: true,
message: 'User status updated.',
data: { id: updated.id, name: updated.name, status: updated.status },
}
})
register('DELETE', endpoints.deleteUser, ({ params }) => { register('DELETE', endpoints.deleteUser, ({ params }) => {
removeById(adminUsers, params.id) removeById(adminUsers, params.id)
@@ -160,5 +168,3 @@ register('DELETE', endpoints.deleteUser, ({ params }) => {
data: null, data: null,
} }
}) })
register('GET', endpoints.getRolesList, () => ({ data: roles }))
-10
View File
@@ -4,7 +4,6 @@ import {
apiAddAdminUser, apiAddAdminUser,
apiChangeAdminUserStatus, apiChangeAdminUserStatus,
apiDeleteAdminUser, apiDeleteAdminUser,
apiGetAdminRoles,
apiGetAdminUsers, apiGetAdminUsers,
apiShowAdminUser, apiShowAdminUser,
apiUpdateAdminUser, apiUpdateAdminUser,
@@ -15,7 +14,6 @@ export const adminUsersKeys = {
all: ['admin', 'users'], all: ['admin', 'users'],
list: (filters, pagination) => ['admin', 'users', 'list', filters, pagination], list: (filters, pagination) => ['admin', 'users', 'list', filters, pagination],
detail: (id) => ['admin', 'users', 'detail', id], detail: (id) => ['admin', 'users', 'detail', id],
roles: () => ['admin', 'users', 'roles'],
} }
export const useAdminUsersListQuery = (filtersRef, paginationRef, options = {}) => export const useAdminUsersListQuery = (filtersRef, paginationRef, options = {}) =>
@@ -37,14 +35,6 @@ export const useAdminUserQuery = (idRef, options = {}) =>
...options, ...options,
}) })
export const useAdminRolesListQuery = (options = {}) =>
useQuery({
queryKey: adminUsersKeys.roles(),
queryFn: () => apiGetAdminRoles(),
select: (response) => response?.data ?? response,
...options,
})
export const useAddAdminUserMutation = () => export const useAddAdminUserMutation = () =>
useMutation({ mutationFn: (payload) => apiAddAdminUser(payload) }) useMutation({ mutationFn: (payload) => apiAddAdminUser(payload) })