@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
Vendored
-3
@@ -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
@@ -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({
|
||||||
|
|||||||
@@ -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 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
|
||||||
|
const isBlocked = user?.status === 'blocked'
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
key: 'edit',
|
key: 'edit',
|
||||||
label: 'ویرایش',
|
label: 'ویرایش',
|
||||||
icon: 'pencil',
|
icon: 'pencil',
|
||||||
onClick: () => emit('edit', activeUser.value),
|
onClick: () => emit('edit', user),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
label: 'حذف',
|
label: 'حذف',
|
||||||
icon: 'trash',
|
icon: 'trash',
|
||||||
onClick: () => emit('delete', activeUser.value),
|
onClick: () => emit('delete', user),
|
||||||
},
|
},
|
||||||
{
|
isBlocked
|
||||||
|
? {
|
||||||
|
key: 'unblock',
|
||||||
|
label: 'رفع مسدودیت کاربر',
|
||||||
|
icon: 'check',
|
||||||
|
onClick: () => emit('unblock', user),
|
||||||
|
}
|
||||||
|
: {
|
||||||
key: 'block',
|
key: 'block',
|
||||||
label: 'مسدود کردن کاربر',
|
label: 'مسدود کردن کاربر',
|
||||||
icon: 'close',
|
icon: 'close',
|
||||||
danger: true,
|
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>
|
</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]
|
||||||
|
|||||||
@@ -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__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(
|
||||||
|
existingUser,
|
||||||
|
(user) => {
|
||||||
if (!user) return
|
if (!user) return
|
||||||
const firstRole = Array.isArray(user.roles) ? user.roles[0] : null
|
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 = {
|
||||||
...form.value,
|
...form.value,
|
||||||
roleId: user.roleId || user.profile?.roleId || firstRoleId || '',
|
role: roleValue || '',
|
||||||
firstName: user.firstName || '',
|
name: user.name || `${user.firstName || ''} ${user.lastName || ''}`.trim(),
|
||||||
lastName: user.lastName || '',
|
birthday: user.birthday || '',
|
||||||
birthDate: user.profile?.birthDate || '',
|
|
||||||
nationalCode: user.nationalCode || '',
|
nationalCode: user.nationalCode || '',
|
||||||
maritalStatus: user.profile?.maritalStatus || '',
|
maritalStatus: user.marriageStatus ?? user.profile?.maritalStatus ?? '',
|
||||||
gender: user.profile?.gender || '',
|
gender: user.gender ?? user.profile?.gender ?? '',
|
||||||
bio: user.profile?.bio || '',
|
bio: user.bio ?? user.profile?.bio ?? '',
|
||||||
phoneNumber: user.phoneNumber || '',
|
phone: user.phone || '',
|
||||||
provinceId: user?.province?.id || '',
|
provinceId: user?.province?.id || '',
|
||||||
cityId: user?.city?.id || '',
|
cityId: user?.city?.id || '',
|
||||||
address: user?.address || '',
|
address: typeof user.address === 'string' ? user.address : user.address?.address || '',
|
||||||
avatarId: user.profile?.avatarId || null,
|
avatarId: user.avatarId ?? user.profile?.avatarId ?? null,
|
||||||
}
|
}
|
||||||
initialNationalCode.value = user.nationalCode || ''
|
initialNationalCode.value = user.nationalCode || ''
|
||||||
if (user.avatarUrl) {
|
if (user.avatarUrl) {
|
||||||
avatar.value = { url: 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>
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 || '',
|
||||||
|
|||||||
@@ -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: '',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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'
|
||||||
@@ -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 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()
|
||||||
|
|||||||
@@ -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 { 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()
|
||||||
|
|||||||
@@ -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)
|
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)
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 }))
|
|
||||||
|
|||||||
@@ -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) })
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user