@@ -23,7 +23,11 @@
|
||||
:loading="!!data?.confirmLoading"
|
||||
custom-class="confirm-modal__btn confirm-modal__btn--confirm"
|
||||
@click="confirm(data)"
|
||||
/>
|
||||
>
|
||||
<template #appendIcon>
|
||||
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
||||
</template>
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -31,6 +35,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SvgIcon from './icons/SvgIcon.vue'
|
||||
import { useModalStore } from '@/store/modal'
|
||||
import BasicModal from '@/components/BasicModal.vue'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
@@ -82,10 +87,6 @@ const cancel = (data) => {
|
||||
&__btn {
|
||||
min-width: 100px;
|
||||
padding: 0.5rem 1.5rem;
|
||||
|
||||
&--confirm {
|
||||
padding-inline: 1.5rem 2.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -85,7 +85,7 @@ const props = defineProps({
|
||||
name: { type: String, default: '' },
|
||||
label: { type: String, default: '' },
|
||||
placeholder: { type: String, default: 'انتخاب کنید' },
|
||||
options: { type: Array, required: true },
|
||||
options: { type: Array, default: () => [] },
|
||||
optionLabel: { type: String, default: 'name' },
|
||||
optionValue: { type: String, default: 'id' },
|
||||
multiple: { type: Boolean, default: false },
|
||||
@@ -118,13 +118,14 @@ const isSelected = (opt) => {
|
||||
const selectedLabel = computed(() => {
|
||||
const value = props.modelValue
|
||||
if (value === undefined || value === null || value === '') return props.placeholder
|
||||
const options = props.options ?? []
|
||||
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 <= 2) return selected.map((s) => s[props.optionLabel]).join('، ')
|
||||
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
|
||||
})
|
||||
|
||||
|
||||
@@ -88,9 +88,9 @@
|
||||
<script setup>
|
||||
import { toast } from 'vue3-toastify'
|
||||
import useModal from '@/composables/useModal'
|
||||
import { PermissionModal } from '@/features/auth'
|
||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import PermissionModal from '@/features/auth/components/studentRegister/PermissionModal.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [Object, File, String, null], default: null },
|
||||
|
||||
Vendored
-3
@@ -13,9 +13,6 @@ export type IconName =
|
||||
| 'caret-right'
|
||||
| 'chat'
|
||||
| 'chat-centered-dots'
|
||||
| 'chat-slash'
|
||||
| 'chat-text'
|
||||
| 'chats'
|
||||
| 'check'
|
||||
| 'check-square'
|
||||
| 'close'
|
||||
|
||||
+8
-2
@@ -38,11 +38,17 @@ export const STUDENT_REGISTRATION = Object.freeze({
|
||||
|
||||
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({
|
||||
approved: 'تأیید شده',
|
||||
pending: 'در انتظار',
|
||||
rejected: 'رد شده',
|
||||
verified: 'تأیید تلفن',
|
||||
approved: 'تأیید شده',
|
||||
blocked: 'مسدود',
|
||||
rejected: 'رد شده',
|
||||
})
|
||||
|
||||
export const MARITAL_STATUS = Object.freeze({
|
||||
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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 TermsFilters from '@/features/admin/terms/components/TermsFilters.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 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 {
|
||||
adminTermsKeys,
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
</template>
|
||||
</TextField>
|
||||
<TextField
|
||||
v-model="form.phoneNumber"
|
||||
name="phoneNumber"
|
||||
v-model="form.phone"
|
||||
name="phone"
|
||||
label="شماره همراه کاربر"
|
||||
inputmode="numeric"
|
||||
:convert-digits="true"
|
||||
@@ -76,7 +76,7 @@ import SelectField from '@/components/form/SelectField.vue'
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({ name: '', nationalCode: '', phoneNumber: '', role: '' }),
|
||||
default: () => ({ name: '', nationalCode: '', phone: '', role: '' }),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -101,7 +101,7 @@ const onSubmit = () => {
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
form.value = { name: '', nationalCode: '', phoneNumber: '', role: '' }
|
||||
form.value = { name: '', nationalCode: '', phone: '', role: '' }
|
||||
emit('update:modelValue', { ...form.value })
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
@@ -2,18 +2,22 @@
|
||||
<div class="users-table">
|
||||
<div v-for="user in users" :key="user.id" class="users-table__row">
|
||||
<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" />
|
||||
</div>
|
||||
<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">
|
||||
<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">کد ملی: {{ user.nationalCode }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Badge v-if="user.status === 'blocked'" variant="danger" size="sm" dot>
|
||||
{{ USER_STATUS.blocked }}
|
||||
</Badge>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="users-table__role-badge"
|
||||
@@ -66,8 +70,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ROLES } from '@/enums'
|
||||
import { computed, ref } from 'vue'
|
||||
import Badge from '@/components/Badge.vue'
|
||||
import { ROLES, USER_STATUS } from '@/enums'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
@@ -80,17 +85,7 @@ defineProps({
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const emit = defineEmits([
|
||||
'edit',
|
||||
'delete',
|
||||
'block',
|
||||
'show-details',
|
||||
'show-assignments',
|
||||
'show-exams',
|
||||
'export-pdf',
|
||||
])
|
||||
|
||||
const fullName = (user) => `${user.firstName || ''} ${user.lastName || ''}`.trim()
|
||||
const emit = defineEmits(['edit', 'delete', 'block', 'unblock', 'show-details'])
|
||||
|
||||
const roleName = (user) => {
|
||||
const first = user.roles?.[0]
|
||||
@@ -155,45 +150,38 @@ const roleMenuItems = computed(() => {
|
||||
}))
|
||||
})
|
||||
|
||||
const menuItems = computed(() => [
|
||||
const menuItems = computed(() => {
|
||||
const user = activeUser.value
|
||||
const isBlocked = user?.status === 'blocked'
|
||||
return [
|
||||
{
|
||||
key: 'edit',
|
||||
label: 'ویرایش',
|
||||
icon: 'pencil',
|
||||
onClick: () => emit('edit', activeUser.value),
|
||||
onClick: () => emit('edit', user),
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'حذف',
|
||||
icon: 'trash',
|
||||
onClick: () => emit('delete', activeUser.value),
|
||||
onClick: () => emit('delete', user),
|
||||
},
|
||||
{
|
||||
isBlocked
|
||||
? {
|
||||
key: 'unblock',
|
||||
label: 'رفع مسدودیت کاربر',
|
||||
icon: 'check',
|
||||
onClick: () => emit('unblock', user),
|
||||
}
|
||||
: {
|
||||
key: 'block',
|
||||
label: 'مسدود کردن کاربر',
|
||||
icon: 'close',
|
||||
danger: true,
|
||||
onClick: () => emit('block', activeUser.value),
|
||||
onClick: () => emit('block', user),
|
||||
},
|
||||
{
|
||||
key: 'show-assignments',
|
||||
label: 'جزئیات تکلیف',
|
||||
icon: 'file',
|
||||
onClick: () => emit('show-assignments', activeUser.value),
|
||||
},
|
||||
{
|
||||
key: 'show-exams',
|
||||
label: 'جزئیات آزمون',
|
||||
icon: 'file',
|
||||
onClick: () => emit('show-exams', activeUser.value),
|
||||
},
|
||||
{
|
||||
key: 'export-pdf',
|
||||
label: 'خروجی PDF',
|
||||
icon: 'upload',
|
||||
onClick: () => emit('export-pdf', activeUser.value),
|
||||
},
|
||||
])
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -40,11 +40,14 @@
|
||||
<div class="user-details__grid">
|
||||
<LineInfoBlock
|
||||
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="وضعیت تاهل" :desc="MARITAL_STATUS[user.maritalStatus] || '—'" />
|
||||
<LineInfoBlock
|
||||
title="وضعیت تاهل"
|
||||
:desc="MARITAL_STATUS[user.marriageStatus] || '—'"
|
||||
/>
|
||||
<LineInfoBlock title="جنسیت" :desc="GENDER[user.gender] || '—'" />
|
||||
<LineInfoBlock title="استان" :desc="user?.province?.name || '—'" />
|
||||
<LineInfoBlock title="شهر" :desc="user?.city?.name || '—'" />
|
||||
@@ -122,9 +125,7 @@ const { data: user, isLoading } = useAdminUserQuery(userId, {
|
||||
enabled: () => !!userId.value,
|
||||
})
|
||||
|
||||
const fullName = computed(() =>
|
||||
`${user.value?.firstName || ''} ${user.value?.lastName || ''}`.trim()
|
||||
)
|
||||
const fullName = computed(() => `${user.value?.name || ''}`.trim())
|
||||
|
||||
const roleName = computed(() => {
|
||||
const first = user.value?.roles?.[0]
|
||||
|
||||
@@ -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'
|
||||
@@ -29,36 +29,23 @@
|
||||
<div class="user-form__row">
|
||||
<div class="user-form__cell user-form__cell--third">
|
||||
<SelectField
|
||||
v-model="form.roleId"
|
||||
name="roleId"
|
||||
v-model="form.role"
|
||||
name="role"
|
||||
label="نقش"
|
||||
:options="roleOptions"
|
||||
:options="ROLES"
|
||||
option-label="label"
|
||||
option-value="id"
|
||||
:error="errors.roleId"
|
||||
@change="validateAt('roleId', form.roleId)"
|
||||
option-value="value"
|
||||
:error="errors.role"
|
||||
@change="validateAt('role', form.role)"
|
||||
/>
|
||||
</div>
|
||||
<div class="user-form__cell user-form__cell--third">
|
||||
<TextField
|
||||
v-model="form.firstName"
|
||||
name="firstName"
|
||||
label="نام"
|
||||
:error="errors.firstName"
|
||||
@blur="validateAt('firstName', form.firstName)"
|
||||
>
|
||||
<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)"
|
||||
v-model="form.name"
|
||||
name="name"
|
||||
label="نام و نام خانوادگی"
|
||||
:error="errors.name"
|
||||
@blur="validateAt('name', form.name)"
|
||||
>
|
||||
<template #appendIcon>
|
||||
<SvgIcon name="user" :size="20" color="var(--color-thd-gray)" />
|
||||
@@ -67,11 +54,11 @@
|
||||
</div>
|
||||
<div class="user-form__cell user-form__cell--third">
|
||||
<DatePickerField
|
||||
v-model="form.birthDate"
|
||||
name="birthDate"
|
||||
v-model="form.birthday"
|
||||
name="birthday"
|
||||
label="تاریخ تولد"
|
||||
:max="todayIso"
|
||||
:error="errors.birthDate"
|
||||
:error="errors.birthday"
|
||||
/>
|
||||
</div>
|
||||
<div class="user-form__cell user-form__cell--third">
|
||||
@@ -125,13 +112,13 @@
|
||||
<div class="user-form__row">
|
||||
<div class="user-form__cell user-form__cell--third">
|
||||
<TextField
|
||||
v-model="form.phoneNumber"
|
||||
name="phoneNumber"
|
||||
v-model="form.phone"
|
||||
name="phone"
|
||||
label="شماره تلفن همراه"
|
||||
inputmode="numeric"
|
||||
:convert-digits="true"
|
||||
:error="errors.phoneNumber"
|
||||
@blur="validateAt('phoneNumber', form.phoneNumber)"
|
||||
:error="errors.phone"
|
||||
@blur="validateAt('phone', form.phone)"
|
||||
>
|
||||
<template #appendIcon>
|
||||
<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 SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||
import TextField from '@/components/form/TextField.vue'
|
||||
import { GENDER, MARITAL_STATUS, ROLES } from '@/enums'
|
||||
import SelectField from '@/components/form/SelectField.vue'
|
||||
import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
||||
import ImageCropper from '@/components/form/ImageCropper.vue'
|
||||
import { GENDER, MARITAL_STATUS, ROLE_LABELS } from '@/enums'
|
||||
import { objectToFormData } from '@/utils/object-to-formdata'
|
||||
import { useUploadMediaMutation } from '@/services/query/auth'
|
||||
import TextareaField from '@/components/form/TextareaField.vue'
|
||||
@@ -242,7 +229,6 @@ import { useGetCitiesOfProvinceQuery, useGetProvincesQuery } from '@/services/qu
|
||||
import {
|
||||
adminUsersKeys,
|
||||
useAddAdminUserMutation,
|
||||
useAdminRolesListQuery,
|
||||
useAdminUserQuery,
|
||||
useUpdateAdminUserMutation,
|
||||
} 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 { 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 provinceIdRef = ref(null)
|
||||
@@ -283,15 +265,14 @@ const { data: cities = ref([]) } = useGetCitiesOfProvinceQuery(provinceIdRef, {
|
||||
const initialNationalCode = ref('')
|
||||
|
||||
const form = ref({
|
||||
roleId: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
birthDate: '',
|
||||
role: '',
|
||||
name: '',
|
||||
birthday: '',
|
||||
nationalCode: '',
|
||||
maritalStatus: '',
|
||||
gender: '',
|
||||
bio: '',
|
||||
phoneNumber: '',
|
||||
phone: '',
|
||||
provinceId: '',
|
||||
cityId: '',
|
||||
address: '',
|
||||
@@ -322,31 +303,34 @@ const { data: existingUser } = useAdminUserQuery(userId, {
|
||||
enabled: () => !!userId.value,
|
||||
})
|
||||
|
||||
watch(existingUser, (user) => {
|
||||
watch(
|
||||
existingUser,
|
||||
(user) => {
|
||||
if (!user) return
|
||||
const firstRole = Array.isArray(user.roles) ? user.roles[0] : null
|
||||
const firstRoleId = typeof firstRole === 'object' ? firstRole?.id : null
|
||||
const roleValue = typeof firstRole === 'object' ? firstRole?.name : firstRole
|
||||
form.value = {
|
||||
...form.value,
|
||||
roleId: user.roleId || user.profile?.roleId || firstRoleId || '',
|
||||
firstName: user.firstName || '',
|
||||
lastName: user.lastName || '',
|
||||
birthDate: user.profile?.birthDate || '',
|
||||
role: roleValue || '',
|
||||
name: user.name || `${user.firstName || ''} ${user.lastName || ''}`.trim(),
|
||||
birthday: user.birthday || '',
|
||||
nationalCode: user.nationalCode || '',
|
||||
maritalStatus: user.profile?.maritalStatus || '',
|
||||
gender: user.profile?.gender || '',
|
||||
bio: user.profile?.bio || '',
|
||||
phoneNumber: user.phoneNumber || '',
|
||||
maritalStatus: user.marriageStatus ?? user.profile?.maritalStatus ?? '',
|
||||
gender: user.gender ?? user.profile?.gender ?? '',
|
||||
bio: user.bio ?? user.profile?.bio ?? '',
|
||||
phone: user.phone || '',
|
||||
provinceId: user?.province?.id || '',
|
||||
cityId: user?.city?.id || '',
|
||||
address: user?.address || '',
|
||||
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) {
|
||||
avatar.value = { url: user.avatarUrl }
|
||||
}
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const uploadMutation = useUploadMediaMutation()
|
||||
|
||||
@@ -369,11 +353,32 @@ const updateMutation = useUpdateAdminUserMutation()
|
||||
|
||||
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 { isValid, payload } = await validate(form.value)
|
||||
if (!isValid) return
|
||||
if (isEditMode.value) {
|
||||
await updateMutation.mutateAsync({ id: userId.value, payload })
|
||||
await updateMutation.mutateAsync({ id: userId.value, payload: buildUpdatePayload() })
|
||||
} else {
|
||||
await addMutation.mutateAsync(payload)
|
||||
}
|
||||
|
||||
@@ -31,17 +31,15 @@
|
||||
:users="users"
|
||||
@edit="onEdit"
|
||||
@delete="onAskDelete"
|
||||
@block="onAskBlock"
|
||||
@unblock="onAskUnblock"
|
||||
@show-details="onShowDetails"
|
||||
@show-assignments="onShowAssignments"
|
||||
@show-exams="onShowExams"
|
||||
/>
|
||||
<NoItems v-else title="متاسفیم" desc="کاربری برای نمایش وجود ندارد." />
|
||||
|
||||
<PaginationBlock :pagination="paginationMeta" @update:page="setPage" />
|
||||
|
||||
<UserDetailsModal v-if="isModal('UserDetailsModal')" />
|
||||
<UserAssignmentsModal v-if="isModal('UserAssignmentsModal')" />
|
||||
<UserExamsModal v-if="isModal('UserExamsModal')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -60,12 +58,11 @@ import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
||||
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||
import UsersFilters from '@/features/admin/users/components/UsersFilters.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 UserAssignmentsModal from '@/features/admin/users/components/modals/UserAssignmentsModal.vue'
|
||||
import {
|
||||
adminUsersKeys,
|
||||
useAdminUsersListQuery,
|
||||
useChangeAdminUserStatusMutation,
|
||||
useDeleteAdminUserMutation,
|
||||
} from '@/services/query/admin-users'
|
||||
|
||||
@@ -73,7 +70,7 @@ const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
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 { data, isLoading } = useAdminUsersListQuery(filters, pagination, {
|
||||
@@ -99,22 +96,38 @@ const onEdit = (user) => {
|
||||
}
|
||||
|
||||
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 onAskDelete = (user) => {
|
||||
const name = `${user.firstName || ''} ${user.lastName || ''}`.trim() || user.phoneNumber
|
||||
openModal('ConfirmModal', {
|
||||
title: 'حذف کاربر',
|
||||
message: `آیا از حذف <strong>${name}</strong> مطمئن هستید؟`,
|
||||
onConfirm: () =>
|
||||
deleteMutation.mutate(user.id, {
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: adminUsersKeys.all }),
|
||||
}),
|
||||
message: `آیا از حذف <strong>${userName(user)}</strong> مطمئن هستید؟`,
|
||||
onConfirm: () => deleteMutation.mutate(user.id, { onSuccess: invalidateUsers }),
|
||||
})
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -16,12 +16,11 @@ export const rejectionReasonSchema = object().shape({
|
||||
})
|
||||
|
||||
const baseUserShape = {
|
||||
roleId: string().required(),
|
||||
firstName: string().required().min(2),
|
||||
lastName: string().required().min(2),
|
||||
phoneNumber: phoneNumberRule,
|
||||
role: string().required(),
|
||||
name: string().required().min(3),
|
||||
phone: phoneNumberRule,
|
||||
nationalCode: nationalCodeRule,
|
||||
birthDate: string().nullable().notRequired(),
|
||||
birthday: string().nullable().notRequired(),
|
||||
maritalStatus: string().nullable().notRequired(),
|
||||
gender: string().nullable().notRequired(),
|
||||
bio: string().nullable().notRequired(),
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
<AuthHeading title="ورود" subtitle="در این قسمت اطلاعات خود را وارد کنید." />
|
||||
|
||||
<TextField
|
||||
v-model="form.phoneNumber"
|
||||
name="phoneNumber"
|
||||
v-model="form.phone"
|
||||
name="phone"
|
||||
label="شماره تلفن همراه"
|
||||
inputmode="numeric"
|
||||
:convert-digits="true"
|
||||
:error="errors.phoneNumber"
|
||||
@blur="validateAt('phoneNumber', form.phoneNumber)"
|
||||
:error="errors.phone"
|
||||
@blur="validateAt('phone', form.phone)"
|
||||
>
|
||||
<template #appendIcon>
|
||||
<SvgIcon name="phone" :size="22" color="var(--color-thd-gray)" />
|
||||
@@ -81,7 +81,7 @@ const emit = defineEmits(['next'])
|
||||
|
||||
const schema = loginSchema
|
||||
|
||||
const form = ref({ phoneNumber: '', password: '' })
|
||||
const form = ref({ phone: '', password: '' })
|
||||
const { validate, validateAt, errors } = useYup(schema)
|
||||
|
||||
const loginMutation = useLoginMutation()
|
||||
@@ -92,7 +92,7 @@ const onSubmit = async () => {
|
||||
const { isValid, payload } = await validate(form.value)
|
||||
if (!isValid) return
|
||||
const response = await loginMutation.mutateAsync({
|
||||
phone: payload.phoneNumber,
|
||||
phone: payload.phone,
|
||||
password: payload.password,
|
||||
deviceName: 'web',
|
||||
})
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
/>
|
||||
|
||||
<TextField
|
||||
v-model="form.phoneNumber"
|
||||
name="phoneNumber"
|
||||
v-model="form.phone"
|
||||
name="phone"
|
||||
label="شماره تلفن همراه"
|
||||
inputmode="numeric"
|
||||
:convert-digits="true"
|
||||
:error="errors.phoneNumber"
|
||||
@blur="validateAt('phoneNumber', form.phoneNumber)"
|
||||
:error="errors.phone"
|
||||
@blur="validateAt('phone', form.phone)"
|
||||
>
|
||||
<template #appendIcon>
|
||||
<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 schema = loginWithCodeSchema
|
||||
const form = ref({ phoneNumber: '' })
|
||||
const form = ref({ phone: '' })
|
||||
const { validate, validateAt, errors } = useYup(schema)
|
||||
|
||||
const { savePhoneNumber } = useAuth()
|
||||
@@ -86,8 +86,8 @@ const onSubmit = async () => {
|
||||
} else {
|
||||
await loginCodeMutation.mutateAsync(payload)
|
||||
}
|
||||
savePhoneNumber(payload.phoneNumber)
|
||||
emit('next', { phoneNumber: payload.phoneNumber, mode: props.mode })
|
||||
savePhoneNumber(payload.phone)
|
||||
emit('next', { phone: payload.phone, mode: props.mode })
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -28,13 +28,13 @@
|
||||
</TextField>
|
||||
|
||||
<TextField
|
||||
v-model="form.phoneNumber"
|
||||
name="phoneNumber"
|
||||
v-model="form.phone"
|
||||
name="phone"
|
||||
label="شماره موبایل"
|
||||
inputmode="numeric"
|
||||
:convert-digits="true"
|
||||
:error="errors.phoneNumber"
|
||||
@blur="validateAt('phoneNumber', form.phoneNumber)"
|
||||
:error="errors.phone"
|
||||
@blur="validateAt('phone', form.phone)"
|
||||
>
|
||||
<template #appendIcon>
|
||||
<SvgIcon name="phone" :size="22" color="var(--color-thd-gray)" />
|
||||
@@ -102,7 +102,7 @@ const schema = signupSchema
|
||||
const form = ref({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
phoneNumber: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
passwordConfirmation: '',
|
||||
})
|
||||
@@ -117,12 +117,12 @@ const onSubmit = async () => {
|
||||
const name = [payload.firstName, payload.lastName].filter(Boolean).join(' ').trim()
|
||||
await registerMutation.mutateAsync({
|
||||
name,
|
||||
phone: payload.phoneNumber,
|
||||
phone: payload.phone,
|
||||
password: payload.password,
|
||||
passwordConfirmation: payload.passwordConfirmation,
|
||||
})
|
||||
savePhoneNumber(payload.phoneNumber)
|
||||
emit('next', { phoneNumber: payload.phoneNumber })
|
||||
savePhoneNumber(payload.phone)
|
||||
emit('next', { phone: payload.phone })
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ const resending = computed(
|
||||
const onSubmit = async () => {
|
||||
const { isValid, payload } = await validate(form.value)
|
||||
if (!isValid) return
|
||||
const body = { phoneNumber: props.phoneNumber, verifyCode: payload.verifyCode }
|
||||
const body = { phone: props.phoneNumber, verifyCode: payload.verifyCode }
|
||||
|
||||
if (props.mode === 'loginWithCode') {
|
||||
const response = await verifyLoginMutation.mutateAsync(body)
|
||||
|
||||
@@ -93,14 +93,14 @@
|
||||
<div class="personal-info__row">
|
||||
<div class="personal-info__cell personal-info__cell--full-sm">
|
||||
<TextField
|
||||
v-model="form.phoneNumber"
|
||||
name="phoneNumber"
|
||||
v-model="form.phone"
|
||||
name="phone"
|
||||
label="شماره تلفن همراه"
|
||||
inputmode="numeric"
|
||||
:convert-digits="true"
|
||||
:disabled="!!initialPhoneNumber"
|
||||
:error="errors.phoneNumber"
|
||||
@blur="validateAt('phoneNumber', form.phoneNumber)"
|
||||
:disabled="!!initialPhone"
|
||||
:error="errors.phone"
|
||||
@blur="validateAt('phone', form.phone)"
|
||||
>
|
||||
<template #appendIcon>
|
||||
<SvgIcon name="phone" :size="20" color="var(--color-thd-gray)" />
|
||||
@@ -206,9 +206,7 @@ const maxBirthDate = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const initialPhoneNumber = ref(
|
||||
user.value?.phone || user.value?.phoneNumber || store.personal.phoneNumber || ''
|
||||
)
|
||||
const initialPhone = ref(user.value?.phone || store.personal.phone || '')
|
||||
|
||||
const form = ref({
|
||||
firstName: store.personal.firstName || '',
|
||||
@@ -217,7 +215,7 @@ const form = ref({
|
||||
nationalCode: store.personal.nationalCode || '',
|
||||
maritalStatus: store.personal.maritalStatus || '',
|
||||
gender: store.personal.gender || '',
|
||||
phoneNumber: initialPhoneNumber.value,
|
||||
phone: initialPhone.value,
|
||||
provinceId: store.personal.provinceId || null,
|
||||
cityId: store.personal.cityId || null,
|
||||
address: store.personal.address || '',
|
||||
|
||||
@@ -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="در این قسمت رمز عبور جدید خود را وارد کنید" />
|
||||
|
||||
<TextField
|
||||
v-model="form.phoneNumber"
|
||||
name="phoneNumber"
|
||||
v-model="form.phone"
|
||||
name="phone"
|
||||
label="شماره موبایل"
|
||||
:disabled="!!(user?.phone || user?.phoneNumber)"
|
||||
:disabled="!!(user?.phone || user?.phone)"
|
||||
>
|
||||
<template #appendIcon>
|
||||
<SvgIcon name="phone" :size="22" color="var(--color-thd-gray)" />
|
||||
@@ -71,7 +71,7 @@ const redirectAfterLogin = useRedirectAfterLogin()
|
||||
const schema = resetPasswordSchema
|
||||
|
||||
const form = ref({
|
||||
phoneNumber: user.value?.phone || user.value?.phoneNumber || '',
|
||||
phone: user.value?.phone || '',
|
||||
password: '',
|
||||
passwordConfirmation: '',
|
||||
})
|
||||
|
||||
@@ -4,18 +4,18 @@ import { phoneNumberRule } from '@/constants/rules/phoneNumberRule'
|
||||
import { passwordConfirmationRule } from '@/constants/rules/passwordConfirmationRule'
|
||||
|
||||
export const loginSchema = object().shape({
|
||||
phoneNumber: phoneNumberRule,
|
||||
phone: phoneNumberRule,
|
||||
password: string().required(),
|
||||
})
|
||||
|
||||
export const loginWithCodeSchema = object().shape({
|
||||
phoneNumber: phoneNumberRule,
|
||||
phone: phoneNumberRule,
|
||||
})
|
||||
|
||||
export const signupSchema = object().shape({
|
||||
firstName: string().required().min(3),
|
||||
lastName: string().required().min(3),
|
||||
phoneNumber: phoneNumberRule,
|
||||
phone: phoneNumberRule,
|
||||
password: passwordRule,
|
||||
passwordConfirmation: passwordConfirmationRule,
|
||||
})
|
||||
|
||||
@@ -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 SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
|
||||
import ConsultationItem from '@/features/admin/consultations/components/ConsultationItem.vue'
|
||||
import ConsultationsFilters from '@/features/admin/consultations/components/ConsultationsFilters.vue'
|
||||
import { ConsultationItem, ConsultationsFilters } from '@/features/admin/consultations'
|
||||
import CounselorConsultationDetailsModal from '@/features/counselor/components/modals/CounselorConsultationDetailsModal.vue'
|
||||
import {
|
||||
counselorTicketsKeys,
|
||||
@@ -62,7 +61,7 @@ import {
|
||||
const { openModal, isModal } = useModal()
|
||||
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 { data, isLoading } = useCounselorTicketsListQuery(filters, pagination, {
|
||||
|
||||
@@ -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)"
|
||||
/>
|
||||
<TextField
|
||||
v-model="form.phoneNumber"
|
||||
name="phoneNumber"
|
||||
v-model="form.phone"
|
||||
name="phone"
|
||||
label="شماره موبایل"
|
||||
placeholder="09xxxxxxxxx"
|
||||
inputmode="numeric"
|
||||
:convert-digits="true"
|
||||
:error="errors.phoneNumber"
|
||||
:error="errors.phone"
|
||||
:vibration="true"
|
||||
@blur="validateAt('phoneNumber', form.phoneNumber)"
|
||||
@blur="validateAt('phone', form.phone)"
|
||||
/>
|
||||
<TextField
|
||||
v-model="form.nationalCode"
|
||||
@@ -295,7 +295,7 @@ const onCitySearch = (q) => {
|
||||
|
||||
const form = ref({
|
||||
fullName: '',
|
||||
phoneNumber: '',
|
||||
phone: '',
|
||||
nationalCode: '',
|
||||
disabled: 'فقط برای نمایش',
|
||||
bio: '',
|
||||
@@ -344,7 +344,7 @@ const onSubmit = async () => {
|
||||
const reset = () => {
|
||||
form.value = {
|
||||
fullName: '',
|
||||
phoneNumber: '',
|
||||
phone: '',
|
||||
disabled: 'فقط برای نمایش',
|
||||
bio: '',
|
||||
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'
|
||||
@@ -0,0 +1 @@
|
||||
export { default as StudentCourseItem } from './components/StudentCourseItem.vue'
|
||||
@@ -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'
|
||||
@@ -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 NoItems from '@/components/blocks/NoItems.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 { useStudentExamsListQuery } from '@/services/query/student-exams'
|
||||
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
||||
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||
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 router = useRouter()
|
||||
|
||||
@@ -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'
|
||||
@@ -0,0 +1 @@
|
||||
export * from './schema'
|
||||
@@ -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'
|
||||
@@ -0,0 +1 @@
|
||||
export { default as StudentSessionItem } from './components/StudentSessionItem.vue'
|
||||
@@ -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 SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||
import NoItems from '@/components/blocks/NoItems.vue'
|
||||
import { StudentLessonItem } from '@/features/student/lessons'
|
||||
import { useStudentTermQuery } from '@/services/query/student-terms'
|
||||
import { useStudentCoursesListQuery } from '@/services/query/student-courses'
|
||||
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
||||
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
|
||||
import StudentTermSummary from '@/features/student/terms/components/StudentTermSummary.vue'
|
||||
import StudentLessonItem from '@/features/student/lessons/components/StudentLessonItem.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -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'
|
||||
@@ -14,8 +14,6 @@ export const apiUpdateAdminUserRole = (id, payload) =>
|
||||
http.patch(buildUrl(endpoints.updateUserRole, { 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 apiGetAdminRoles = () => http.get(endpoints.getRolesList)
|
||||
|
||||
@@ -56,7 +56,6 @@ export const endpoints = {
|
||||
addNewUser: '/admin/users',
|
||||
updateUser: '/admin/users/:id',
|
||||
updateUserRole: '/admin/users/:id/role',
|
||||
getRolesList: '/admin/roles',
|
||||
changeUserStatus: '/admin/users/:id/status',
|
||||
deleteUser: '/admin/users/:id',
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ const makeUser = (i, overrides = {}) => {
|
||||
avatarUrl: '',
|
||||
avatarDownloadUrl: '',
|
||||
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) ---
|
||||
firstName,
|
||||
@@ -94,7 +95,6 @@ const makeUser = (i, overrides = {}) => {
|
||||
},
|
||||
profile: {
|
||||
bio: '',
|
||||
birthDate: new Date(1990, i % 12, ((i * 3) % 27) + 1).toISOString(),
|
||||
maritalStatus: i % 2 ? 'single' : 'married',
|
||||
gender: i % 2 ? 'female' : 'male',
|
||||
},
|
||||
|
||||
@@ -15,7 +15,9 @@ import {
|
||||
register('GET', endpoints.getApprovedUsers, ({ query }) => {
|
||||
let list = filterItems(adminUsers, query, {
|
||||
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',
|
||||
phoneNumber: 'includes',
|
||||
roleId: (item, v) => String(item.roleId) === String(v),
|
||||
@@ -39,30 +41,32 @@ register('POST', endpoints.addNewUser, ({ data }) => {
|
||||
const id = makeId()
|
||||
const roleByName = (n) => roles.find((r) => r.name === n)
|
||||
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) ||
|
||||
roles.find((r) => r.id === Number(data.roleId)) ||
|
||||
null
|
||||
const province = data.provinceId ? { id: data.provinceId, name: '' } : null
|
||||
const city = data.cityId ? { id: data.cityId, name: '' } : null
|
||||
const firstName = data.firstName || ''
|
||||
const lastName = data.lastName || ''
|
||||
const fullName = `${firstName} ${lastName}`.trim()
|
||||
const name = data.name || `${data.firstName || ''} ${data.lastName || ''}`.trim()
|
||||
const [firstName, ...rest] = name.split(' ')
|
||||
const lastName = rest.join(' ')
|
||||
const phoneNumber = data.phoneNumber || ''
|
||||
const user = {
|
||||
// --- spec ---
|
||||
id,
|
||||
name: data.name || fullName,
|
||||
name,
|
||||
email: data.email || '',
|
||||
phone: data.phone || (phoneNumber ? phoneNumber.replace(/^0/, '+98') : null),
|
||||
roles: Array.isArray(data.roles) ? data.roles : role ? [role.name] : [],
|
||||
avatarUrl: '',
|
||||
avatarDownloadUrl: null,
|
||||
createdAt: isoNow(),
|
||||
birthday: data.birthday || null,
|
||||
|
||||
// --- ui-only ---
|
||||
firstName,
|
||||
lastName,
|
||||
fullName,
|
||||
fullName: name,
|
||||
phoneNumber,
|
||||
nationalCode: data.nationalCode || '',
|
||||
status: 'approved',
|
||||
@@ -70,8 +74,7 @@ register('POST', endpoints.addNewUser, ({ data }) => {
|
||||
address: { address: data.address || '', province, city },
|
||||
profile: {
|
||||
bio: data.bio || '',
|
||||
birthDate: data.birthDate || '',
|
||||
maritalStatus: data.maritalStatus || '',
|
||||
maritalStatus: data.marriageStatus || data.maritalStatus || '',
|
||||
gender: data.gender || '',
|
||||
avatarId: data.avatarId || null,
|
||||
},
|
||||
@@ -87,24 +90,25 @@ register('POST', endpoints.addNewUser, ({ data }) => {
|
||||
register('PATCH', endpoints.updateUser, ({ params, data }) => {
|
||||
const roleByName = (n) => roles.find((r) => r.name === n)
|
||||
const role =
|
||||
roles.find((r) => r.id === Number(data.roleId)) ||
|
||||
(Array.isArray(data.roles) && data.roles[0] ? roleByName(data.roles[0]) : null) ||
|
||||
roles.find((r) => r.id === Number(data.roleId)) ||
|
||||
null
|
||||
const firstName = data.firstName ?? ''
|
||||
const lastName = data.lastName ?? ''
|
||||
const fullName = `${firstName} ${lastName}`.trim()
|
||||
const name = data.name ?? `${data.firstName ?? ''} ${data.lastName ?? ''}`.trim()
|
||||
const [firstName, ...rest] = name.split(' ')
|
||||
const lastName = rest.join(' ')
|
||||
const phoneNumber = data.phoneNumber ?? ''
|
||||
const patch = {
|
||||
// --- spec ---
|
||||
name: data.name ?? fullName,
|
||||
name,
|
||||
email: data.email,
|
||||
phone: data.phone ?? (phoneNumber ? phoneNumber.replace(/^0/, '+98') : undefined),
|
||||
roles: Array.isArray(data.roles) ? data.roles : role ? [role.name] : [],
|
||||
birthday: data.birthday ?? null,
|
||||
|
||||
// --- ui-only ---
|
||||
firstName,
|
||||
lastName,
|
||||
fullName,
|
||||
fullName: name,
|
||||
phoneNumber,
|
||||
nationalCode: data.nationalCode,
|
||||
roleId: role?.id,
|
||||
@@ -115,8 +119,7 @@ register('PATCH', endpoints.updateUser, ({ params, data }) => {
|
||||
},
|
||||
profile: {
|
||||
bio: data.bio || '',
|
||||
birthDate: data.birthDate || '',
|
||||
maritalStatus: data.maritalStatus || '',
|
||||
maritalStatus: data.marriageStatus ?? data.maritalStatus ?? '',
|
||||
gender: data.gender || '',
|
||||
avatarId: data.avatarId || null,
|
||||
},
|
||||
@@ -148,9 +151,14 @@ register('PATCH', endpoints.updateUserRole, ({ params, data }) => {
|
||||
}
|
||||
})
|
||||
|
||||
register('POST', endpoints.changeUserStatus, ({ params, data }) => ({
|
||||
data: updateById(adminUsers, params.id, { status: data.status }),
|
||||
}))
|
||||
register('PATCH', endpoints.changeUserStatus, ({ params, data }) => {
|
||||
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 }) => {
|
||||
removeById(adminUsers, params.id)
|
||||
@@ -160,5 +168,3 @@ register('DELETE', endpoints.deleteUser, ({ params }) => {
|
||||
data: null,
|
||||
}
|
||||
})
|
||||
|
||||
register('GET', endpoints.getRolesList, () => ({ data: roles }))
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
apiAddAdminUser,
|
||||
apiChangeAdminUserStatus,
|
||||
apiDeleteAdminUser,
|
||||
apiGetAdminRoles,
|
||||
apiGetAdminUsers,
|
||||
apiShowAdminUser,
|
||||
apiUpdateAdminUser,
|
||||
@@ -15,7 +14,6 @@ export const adminUsersKeys = {
|
||||
all: ['admin', 'users'],
|
||||
list: (filters, pagination) => ['admin', 'users', 'list', filters, pagination],
|
||||
detail: (id) => ['admin', 'users', 'detail', id],
|
||||
roles: () => ['admin', 'users', 'roles'],
|
||||
}
|
||||
|
||||
export const useAdminUsersListQuery = (filtersRef, paginationRef, options = {}) =>
|
||||
@@ -37,14 +35,6 @@ export const useAdminUserQuery = (idRef, options = {}) =>
|
||||
...options,
|
||||
})
|
||||
|
||||
export const useAdminRolesListQuery = (options = {}) =>
|
||||
useQuery({
|
||||
queryKey: adminUsersKeys.roles(),
|
||||
queryFn: () => apiGetAdminRoles(),
|
||||
select: (response) => response?.data ?? response,
|
||||
...options,
|
||||
})
|
||||
|
||||
export const useAddAdminUserMutation = () =>
|
||||
useMutation({ mutationFn: (payload) => apiAddAdminUser(payload) })
|
||||
|
||||
|
||||
Reference in New Issue
Block a user