@@ -17,6 +17,7 @@ import StudentLayout from '@/layouts/StudentLayout.vue'
|
|||||||
import ConfirmModal from '@/components/ConfirmModal.vue'
|
import ConfirmModal from '@/components/ConfirmModal.vue'
|
||||||
import CounselorLayout from '@/layouts/CounselorLayout.vue'
|
import CounselorLayout from '@/layouts/CounselorLayout.vue'
|
||||||
import { tokenService } from '@/services/api/token-service'
|
import { tokenService } from '@/services/api/token-service'
|
||||||
|
import MissionaryLayout from '@/layouts/MissionaryLayout.vue'
|
||||||
import { VueQueryDevtools } from '@tanstack/vue-query-devtools'
|
import { VueQueryDevtools } from '@tanstack/vue-query-devtools'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -26,6 +27,7 @@ const layouts = {
|
|||||||
admin: AdminLayout,
|
admin: AdminLayout,
|
||||||
student: StudentLayout,
|
student: StudentLayout,
|
||||||
counselor: CounselorLayout,
|
counselor: CounselorLayout,
|
||||||
|
missionary: MissionaryLayout,
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentLayout = computed(() => layouts[route.meta?.layout] || PublicLayout)
|
const currentLayout = computed(() => layouts[route.meta?.layout] || PublicLayout)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g clip-path="url(#clip0_1_21191)">
|
<g clip-path="url(#clip0_1_21191)">
|
||||||
<path d="M6.5 11.375C9.19239 11.375 11.375 9.19239 11.375 6.5C11.375 3.80761 9.19239 1.625 6.5 1.625C3.80761 1.625 1.625 3.80761 1.625 6.5C1.625 9.19239 3.80761 11.375 6.5 11.375Z" stroke="#B8B8B8" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M6.5 11.375C9.19239 11.375 11.375 9.19239 11.375 6.5C11.375 3.80761 9.19239 1.625 6.5 1.625C3.80761 1.625 1.625 3.80761 1.625 6.5C1.625 9.19239 3.80761 11.375 6.5 11.375Z" stroke="currentColor" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M6.5 3.65625V6.5H9.34375" stroke="#B8B8B8" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M6.5 3.65625V6.5H9.34375" stroke="currentColor" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</g>
|
</g>
|
||||||
<defs>
|
<defs>
|
||||||
<clipPath id="clip0_1_21191">
|
<clipPath id="clip0_1_21191">
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 666 B After Width: | Height: | Size: 676 B |
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="23" height="23" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_2_135332)">
|
||||||
|
<path d="M4.21875 16.875L18.2813 16.875C19.0579 16.875 19.6875 16.2454 19.6875 15.4688L19.6875 5.625C19.6875 4.84835 19.0579 4.21875 18.2813 4.21875L4.21875 4.21875C3.4421 4.21875 2.8125 4.84835 2.8125 5.625L2.8125 15.4687C2.8125 16.2454 3.4421 16.875 4.21875 16.875Z" stroke="currentColor" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M14.0625 19.6875L8.4375 19.6875" stroke="currentColor" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_2_135332">
|
||||||
|
<rect width="22.5" height="22.5" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 746 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="23" height="23" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8.4375 9.84375L15.4688 9.84375" stroke="currentColor" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M8.4375 12.6562L15.4688 12.6563" stroke="currentColor" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M2.8125 17.5781C3.18546 17.5781 3.54315 17.43 3.80687 17.1662C4.07059 16.9025 4.21875 16.5448 4.21875 16.1719L4.21875 5.625C4.21875 5.43852 4.29283 5.25968 4.42469 5.12782C4.55655 4.99595 4.7354 4.92188 4.92188 4.92188L18.9844 4.92188C19.1709 4.92188 19.3497 4.99595 19.4816 5.12782C19.6134 5.25968 19.6875 5.43852 19.6875 5.625L19.6875 16.1719C19.6875 16.5448 19.5393 16.9025 19.2756 17.1662C19.0119 17.43 18.6542 17.5781 18.2813 17.5781L2.8125 17.5781Z" stroke="currentColor" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M2.8125 17.5781C2.43954 17.5781 2.08185 17.43 1.81813 17.1662C1.55441 16.9025 1.40625 16.5448 1.40625 16.1719L1.40625 7.73438" stroke="currentColor" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 3v9"/>
|
||||||
|
<path d="M15.5 4.5a8 8 0 1 1-7 0"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 228 B |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -21,7 +21,7 @@ import { computed } from 'vue'
|
|||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
/** @type {import('vue').PropType<'neutral' | 'success' | 'danger' | 'warning' | 'primary' | 'info' | 'cyan'>} */
|
/** @type {import('vue').PropType<'neutral' | 'success' | 'danger' | 'warning' | 'primary' | 'info' | 'cyan' | 'blue'>} */
|
||||||
variant: { type: String, default: 'neutral' },
|
variant: { type: String, default: 'neutral' },
|
||||||
/** @type {import('vue').PropType<'sm' | 'md' | 'lg'>} */
|
/** @type {import('vue').PropType<'sm' | 'md' | 'lg'>} */
|
||||||
size: { type: String, default: 'md' },
|
size: { type: String, default: 'md' },
|
||||||
@@ -138,5 +138,10 @@ const onClick = (event) => {
|
|||||||
background: rgba(104, 104, 104, 10%);
|
background: rgba(104, 104, 104, 10%);
|
||||||
color: #686868;
|
color: #686868;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--blue {
|
||||||
|
background: rgba(0, 72, 255, 8%);
|
||||||
|
color: #0048ff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
v-if="item.icon"
|
v-if="item.icon"
|
||||||
:name="item.icon"
|
:name="item.icon"
|
||||||
:size="16"
|
:size="16"
|
||||||
:color="item.iconColor ?? item.color"
|
:color="item.iconColor ?? item.color ?? 'currentColor'"
|
||||||
class="dropdown-menu__icon"
|
class="dropdown-menu__icon"
|
||||||
/>
|
/>
|
||||||
<span class="dropdown-menu__label">{{ item.label }}</span>
|
<span class="dropdown-menu__label">{{ item.label }}</span>
|
||||||
|
|||||||
@@ -11,7 +11,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="header-block__title">
|
<div class="header-block__title">
|
||||||
<slot>
|
<slot>
|
||||||
<div v-if="!hasSlotContent && pageTitle">{{ pageTitle }}</div>
|
<div v-if="pageTitle || pageSubtitle" class="header-block__heading">
|
||||||
|
<span v-if="pageSubtitle" class="header-block__heading-eyebrow">{{ pageTitle }}</span>
|
||||||
|
<span>{{ pageSubtitle || pageTitle }}</span>
|
||||||
|
</div>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-block__icons">
|
<div class="header-block__icons">
|
||||||
@@ -27,7 +30,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="header-block__title-mobile">
|
<div class="header-block__title-mobile">
|
||||||
<slot>
|
<slot>
|
||||||
<div v-if="!hasSlotContent && pageTitle">{{ pageTitle }}</div>
|
<div v-if="pageTitle || pageSubtitle" class="header-block__heading">
|
||||||
|
<span v-if="pageSubtitle" class="header-block__heading-eyebrow">{{ pageTitle }}</span>
|
||||||
|
<span>{{ pageSubtitle || pageTitle }}</span>
|
||||||
|
</div>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -43,22 +49,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useUIStore } from '@/store/ui'
|
import { useUIStore } from '@/store/ui'
|
||||||
import { gallery } from '@/utils/gallery'
|
import { gallery } from '@/utils/gallery'
|
||||||
import useAuth from '@/composables/useAuth'
|
import useAuth from '@/composables/useAuth'
|
||||||
import { computed, ref, useSlots } from 'vue'
|
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
import DropdownMenu from '@/components/DropdownMenu.vue'
|
import DropdownMenu from '@/components/DropdownMenu.vue'
|
||||||
import { useLogoutMutation } from '@/services/query/auth'
|
import { useLogoutMutation } from '@/services/query/auth'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
pageTitle: { type: String, default: '' },
|
pageTitle: { type: String, default: '' },
|
||||||
|
pageSubtitle: { type: String, default: '' },
|
||||||
})
|
})
|
||||||
|
|
||||||
const slots = useSlots()
|
|
||||||
const hasSlotContent = computed(() => !!slots.default)
|
|
||||||
|
|
||||||
const ui = useUIStore()
|
const ui = useUIStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { clearSession } = useAuth()
|
const { clearSession } = useAuth()
|
||||||
@@ -176,6 +180,18 @@ const iconList = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__heading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__heading-eyebrow {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #a7a7a7;
|
||||||
|
}
|
||||||
|
|
||||||
&__icons {
|
&__icons {
|
||||||
flex-basis: 33.33%;
|
flex-basis: 33.33%;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<div class="message-attachments">
|
||||||
|
<template v-for="item in media" :key="item.id">
|
||||||
|
<a
|
||||||
|
v-if="isImage(item)"
|
||||||
|
:href="item.url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="message-attachments__image-link"
|
||||||
|
>
|
||||||
|
<img :src="item.url" :alt="item.fileName" class="message-attachments__image" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
v-else
|
||||||
|
:href="item.downloadUrl || item.url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="message-attachments__file"
|
||||||
|
>
|
||||||
|
<SvgIcon name="file" :size="18" color="currentColor" />
|
||||||
|
<span class="message-attachments__name">{{ item.fileName }}</span>
|
||||||
|
<span class="message-attachments__size">{{ formatFileSize(item.fileSize) }}</span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
media: { type: Array, default: () => [] },
|
||||||
|
})
|
||||||
|
|
||||||
|
const isImage = (item) =>
|
||||||
|
(item.mimeType || '').startsWith('image/') || /\.(jpe?g|png|gif|webp)$/i.test(item.fileName || '')
|
||||||
|
|
||||||
|
const formatFileSize = (bytes) => {
|
||||||
|
if (!bytes) return ''
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.message-attachments {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.375rem;
|
||||||
|
margin-bottom: 0.375rem;
|
||||||
|
|
||||||
|
&__image-link {
|
||||||
|
display: block;
|
||||||
|
max-width: 16rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__image {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 12rem;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__file {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
padding: 0.375rem 0.625rem;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
background: rgba(0, 0, 0, 8%);
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 14%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__size {
|
||||||
|
font-family: var(--font-family-en);
|
||||||
|
font-size: 0.65rem;
|
||||||
|
opacity: 0.75;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -126,7 +126,7 @@ const onActionClick = (tab) => {
|
|||||||
gap: 0.375rem;
|
gap: 0.375rem;
|
||||||
background: rgba(255, 255, 255, 80%);
|
background: rgba(255, 255, 255, 80%);
|
||||||
box-shadow: 0 10px 15px -3px rgba(241, 241, 241, 100%);
|
box-shadow: 0 10px 15px -3px rgba(241, 241, 241, 100%);
|
||||||
padding: 0.875rem;
|
padding: 0.775rem;
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +144,8 @@ const onActionClick = (tab) => {
|
|||||||
&--desktop {
|
&--desktop {
|
||||||
display: none;
|
display: none;
|
||||||
min-width: fit-content;
|
min-width: fit-content;
|
||||||
margin-block-end: 0.625rem;
|
|
||||||
|
// margin-block-end: 0.625rem;
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -162,7 +163,7 @@ const onActionClick = (tab) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__action-btn {
|
&__action-btn {
|
||||||
padding: 0 1.25rem;
|
height: 100%;
|
||||||
min-width: fit-content;
|
min-width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
@click="toggle"
|
@click="toggle"
|
||||||
>
|
>
|
||||||
<span class="datepicker-field__value">{{ displayValue }}</span>
|
<span class="datepicker-field__value">{{ displayValue }}</span>
|
||||||
<SvgIcon name="calendar" :size="20" class="datepicker-field__icon" />
|
<SvgIcon name="calendar" :size="20" color="#DDDDDD" class="datepicker-field__icon" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="error" class="datepicker-field__error" :class="{ 'animate-shake': vibration }">
|
<div v-if="error" class="datepicker-field__error" :class="{ 'animate-shake': vibration }">
|
||||||
@@ -377,6 +377,7 @@ export default {
|
|||||||
this.$emit('change', gregorian)
|
this.$emit('change', gregorian)
|
||||||
this.close()
|
this.close()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.error('Error in handleConfirm:', error)
|
console.error('Error in handleConfirm:', error)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -325,8 +325,7 @@ onBeforeUnmount(cleanup)
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
font-size: 0.75rem;
|
font-size: 0.7rem;
|
||||||
line-height: 1.85rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,13 +36,16 @@ export type IconName =
|
|||||||
| 'map-pin-simple-area'
|
| 'map-pin-simple-area'
|
||||||
| 'menu'
|
| 'menu'
|
||||||
| 'microphone'
|
| 'microphone'
|
||||||
|
| 'monitor'
|
||||||
| 'mood'
|
| 'mood'
|
||||||
|
| 'newspaper'
|
||||||
| 'paper-plane'
|
| 'paper-plane'
|
||||||
| 'paper-plane-right'
|
| 'paper-plane-right'
|
||||||
| 'pencil'
|
| 'pencil'
|
||||||
| 'phone'
|
| 'phone'
|
||||||
| 'play'
|
| 'play'
|
||||||
| 'plus'
|
| 'plus'
|
||||||
|
| 'power'
|
||||||
| 'scroll'
|
| 'scroll'
|
||||||
| 'shopping-bag'
|
| 'shopping-bag'
|
||||||
| 'smiley-sad'
|
| 'smiley-sad'
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const fields = {
|
|||||||
postalCode: 'کد پستی',
|
postalCode: 'کد پستی',
|
||||||
|
|
||||||
birthDate: 'تاریخ تولد',
|
birthDate: 'تاریخ تولد',
|
||||||
|
birthday: 'تاریخ تولد',
|
||||||
gender: 'جنسیت',
|
gender: 'جنسیت',
|
||||||
maritalStatus: 'وضعیت تأهل',
|
maritalStatus: 'وضعیت تأهل',
|
||||||
|
|
||||||
|
|||||||
@@ -206,3 +206,20 @@ export const SERVICE_TYPE = Object.freeze({
|
|||||||
educational: 'آموزشی',
|
educational: 'آموزشی',
|
||||||
other: 'سایر',
|
other: 'سایر',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Lifecycle of a dispatch request assigned to a missionary. `pending` is the
|
||||||
|
// initial backend state; the missionary may set seen, accepted, or rejected.
|
||||||
|
export const MISSIONARY_REQUEST_STATUS = Object.freeze({
|
||||||
|
pending: 'در انتظار بررسی',
|
||||||
|
seen: 'دیده شده',
|
||||||
|
accepted: 'پذیرفته شده',
|
||||||
|
rejected: 'رد شده',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Landing route per role after login / when a logged-in user hits a guest
|
||||||
|
// page. Roles not listed here fall back to the student dashboard.
|
||||||
|
export const ROLE_HOME_ROUTES = Object.freeze({
|
||||||
|
admin: 'admin-dashboard',
|
||||||
|
counselor: 'counselor-consultations',
|
||||||
|
missionary: 'missionary-requests',
|
||||||
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="assignments-filters__grid">
|
<div class="assignments-filters__grid">
|
||||||
<TextField v-model="form.title" name="title" label="عنوان تکلیف">
|
<TextField v-model="form.title" name="title" label="عنوان تکلیف">
|
||||||
<template #appendIcon>
|
<template #appendIcon>
|
||||||
<SvgIcon name="book" :size="20" color="var(--color-thd-gray)" />
|
<SvgIcon name="monitor" :size="20" color="var(--color-thd-gray)" />
|
||||||
</template>
|
</template>
|
||||||
</TextField>
|
</TextField>
|
||||||
<SelectField
|
<SelectField
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
@click="onReset"
|
@click="onReset"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="close" :size="20" />
|
<SvgIcon name="close" color="black" :size="20" />
|
||||||
</template>
|
</template>
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
<SelectField
|
<SelectField
|
||||||
v-model="form.priority"
|
v-model="form.priority"
|
||||||
name="priority"
|
name="priority"
|
||||||
label="اولویت"
|
label="اولویت تکلیف"
|
||||||
:options="priorityOptions"
|
:options="priorityOptions"
|
||||||
option-label="label"
|
option-label="label"
|
||||||
option-value="value"
|
option-value="value"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
desc="در این قسمت میتوانید تکالیف خود را مدیریت کنید"
|
desc="در این قسمت میتوانید تکالیف خود را مدیریت کنید"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="check-square" :size="24" color="var(--color-primary)" />
|
<img :src="assignmentIcon" alt="" />
|
||||||
</template>
|
</template>
|
||||||
</BoxedIconTitleBlock>
|
</BoxedIconTitleBlock>
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
</SimpleTitleIconBlock>
|
</SimpleTitleIconBlock>
|
||||||
<BaseButton text="افزودن تکلیف جدید" custom-class="assignments-page__add-btn" @click="onAdd">
|
<BaseButton text="افزودن تکلیف جدید" custom-class="assignments-page__add-btn" @click="onAdd">
|
||||||
<template #appendIcon>
|
<template #appendIcon>
|
||||||
<SvgIcon name="plus" :size="18" color="#fff" />
|
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
||||||
</template>
|
</template>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import { gallery } from '@/utils/gallery'
|
||||||
import useModal from '@/composables/useModal'
|
import useModal from '@/composables/useModal'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
@@ -75,6 +76,8 @@ import {
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { openModal, isModal } = useModal()
|
const { openModal, isModal } = useModal()
|
||||||
|
|
||||||
|
const assignmentIcon = gallery.assignmentIcon
|
||||||
|
|
||||||
const filters = ref({
|
const filters = ref({
|
||||||
title: '',
|
title: '',
|
||||||
termId: '',
|
termId: '',
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
@click="onReset"
|
@click="onReset"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="close" :size="20" />
|
<SvgIcon name="close" color="black" :size="20" />
|
||||||
</template>
|
</template>
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="courses-filters__grid">
|
<div class="courses-filters__grid">
|
||||||
<TextField v-model="form.title" name="title" label="عنوان دوره">
|
<TextField v-model="form.title" name="title" label="عنوان دوره">
|
||||||
<template #appendIcon>
|
<template #appendIcon>
|
||||||
<SvgIcon name="book" :size="20" color="var(--color-thd-gray)" />
|
<SvgIcon name="graduation-cap" :size="20" color="var(--color-thd-gray)" />
|
||||||
</template>
|
</template>
|
||||||
</TextField>
|
</TextField>
|
||||||
<SelectField
|
<SelectField
|
||||||
|
|||||||
@@ -14,16 +14,6 @@
|
|||||||
|
|
||||||
<form class="course-form__form" @submit.prevent="onSubmit">
|
<form class="course-form__form" @submit.prevent="onSubmit">
|
||||||
<div class="course-form__grid">
|
<div class="course-form__grid">
|
||||||
<div class="course-form__image-col">
|
|
||||||
<ImageCropper
|
|
||||||
v-model="image"
|
|
||||||
name="image"
|
|
||||||
bg-color="#eeeeee"
|
|
||||||
@crop="onImageCropped"
|
|
||||||
@error="onImageError"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="course-form__main-col">
|
<div class="course-form__main-col">
|
||||||
<div class="course-form__row">
|
<div class="course-form__row">
|
||||||
<div class="course-form__cell course-form__cell--third">
|
<div class="course-form__cell course-form__cell--third">
|
||||||
@@ -100,6 +90,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="course-form__image-col">
|
||||||
|
<ImageCropper
|
||||||
|
v-model="image"
|
||||||
|
name="image"
|
||||||
|
bg-color="#eeeeee"
|
||||||
|
@crop="onImageCropped"
|
||||||
|
@error="onImageError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="course-form__divider" />
|
<div class="course-form__divider" />
|
||||||
@@ -309,29 +309,23 @@ const onCancel = () => router.push({ name: 'admin-courses' })
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
grid-template-columns: 4fr 8fr;
|
grid-template-columns: 8fr 4fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
@media (min-width: 1280px) {
|
||||||
grid-template-columns: 3fr 9fr;
|
grid-template-columns: 9fr 3fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__image-col {
|
&__image-col {
|
||||||
order: 2;
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
order: 1;
|
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__main-col {
|
&__main-col {
|
||||||
order: 1;
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
order: 2;
|
padding-inline-end: 1.25rem;
|
||||||
padding-inline-start: 1.25rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
desc="در این قسمت میتوانید دورههای خود را مدیریت کنید"
|
desc="در این قسمت میتوانید دورههای خود را مدیریت کنید"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="book" :size="24" color="var(--color-primary)" />
|
<img :src="courseIcon" alt="" />
|
||||||
</template>
|
</template>
|
||||||
</BoxedIconTitleBlock>
|
</BoxedIconTitleBlock>
|
||||||
|
|
||||||
@@ -37,10 +37,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { gallery } from '@/utils/gallery'
|
||||||
import useModal from '@/composables/useModal'
|
import useModal from '@/composables/useModal'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from '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'
|
||||||
@@ -64,6 +64,8 @@ const router = useRouter()
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { openModal, isModal } = useModal()
|
const { openModal, isModal } = useModal()
|
||||||
|
|
||||||
|
const courseIcon = gallery.courseIcon
|
||||||
|
|
||||||
const routeTermId = computed(() => (route.params.termId ? Number(route.params.termId) : null))
|
const routeTermId = computed(() => (route.params.termId ? Number(route.params.termId) : null))
|
||||||
|
|
||||||
const onAdd = () => {
|
const onAdd = () => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="exams-filters__grid">
|
<div class="exams-filters__grid">
|
||||||
<TextField v-model="form.title" name="title" label="عنوان آزمون">
|
<TextField v-model="form.title" name="title" label="عنوان آزمون">
|
||||||
<template #appendIcon>
|
<template #appendIcon>
|
||||||
<SvgIcon name="book" :size="20" color="var(--color-thd-gray)" />
|
<SvgIcon name="monitor" :size="20" color="var(--color-thd-gray)" />
|
||||||
</template>
|
</template>
|
||||||
</TextField>
|
</TextField>
|
||||||
<SelectField
|
<SelectField
|
||||||
|
|||||||
@@ -30,7 +30,11 @@
|
|||||||
label="عنوان آزمون"
|
label="عنوان آزمون"
|
||||||
:error="errors.title"
|
:error="errors.title"
|
||||||
@blur="validateAt('title', form.title)"
|
@blur="validateAt('title', form.title)"
|
||||||
/>
|
>
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="book" :size="20" color="var(--color-thd-gray)" />
|
||||||
|
</template>
|
||||||
|
</TextField>
|
||||||
<TextField
|
<TextField
|
||||||
v-model="form.minimumScore"
|
v-model="form.minimumScore"
|
||||||
name="minimumScore"
|
name="minimumScore"
|
||||||
@@ -39,7 +43,11 @@
|
|||||||
:convert-digits="true"
|
:convert-digits="true"
|
||||||
:error="errors.minimumScore"
|
:error="errors.minimumScore"
|
||||||
@blur="validateAt('minimumScore', form.minimumScore)"
|
@blur="validateAt('minimumScore', form.minimumScore)"
|
||||||
/>
|
>
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="newspaper" :size="20" color="var(--color-thd-gray)" />
|
||||||
|
</template>
|
||||||
|
</TextField>
|
||||||
<TextField
|
<TextField
|
||||||
v-model="form.score"
|
v-model="form.score"
|
||||||
name="score"
|
name="score"
|
||||||
@@ -48,7 +56,11 @@
|
|||||||
:convert-digits="true"
|
:convert-digits="true"
|
||||||
:error="errors.score"
|
:error="errors.score"
|
||||||
@blur="validateAt('score', form.score)"
|
@blur="validateAt('score', form.score)"
|
||||||
/>
|
>
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="newspaper" :size="20" color="var(--color-thd-gray)" />
|
||||||
|
</template>
|
||||||
|
</TextField>
|
||||||
<TextField
|
<TextField
|
||||||
v-model="form.durationMinutes"
|
v-model="form.durationMinutes"
|
||||||
name="durationMinutes"
|
name="durationMinutes"
|
||||||
@@ -57,14 +69,15 @@
|
|||||||
:convert-digits="true"
|
:convert-digits="true"
|
||||||
:error="errors.durationMinutes"
|
:error="errors.durationMinutes"
|
||||||
@blur="validateAt('durationMinutes', form.durationMinutes)"
|
@blur="validateAt('durationMinutes', form.durationMinutes)"
|
||||||
/>
|
>
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="clock" :size="20" color="var(--color-thd-gray)" />
|
||||||
|
</template>
|
||||||
|
</TextField>
|
||||||
<div class="exam-form__toggle-cell">
|
<div class="exam-form__toggle-cell">
|
||||||
<ToggleSwitch v-model="form.isRandom" label="به صــــورت رندوم باشد." />
|
<ToggleSwitch v-model="form.isRandom" label="به صــــورت رندوم باشد." />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="exam-form__card">
|
|
||||||
<TextareaField
|
<TextareaField
|
||||||
v-model="form.description"
|
v-model="form.description"
|
||||||
name="description"
|
name="description"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
desc="در این قسمت میتوانید آزمونهای خود را مدیریت کنید"
|
desc="در این قسمت میتوانید آزمونهای خود را مدیریت کنید"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="pencil" :size="24" color="var(--color-primary)" />
|
<img :src="sessionIcon" alt="" />
|
||||||
</template>
|
</template>
|
||||||
</BoxedIconTitleBlock>
|
</BoxedIconTitleBlock>
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
</SimpleTitleIconBlock>
|
</SimpleTitleIconBlock>
|
||||||
<BaseButton text="افزودن آزمون جدید" custom-class="exams-page__add-btn" @click="onAdd">
|
<BaseButton text="افزودن آزمون جدید" custom-class="exams-page__add-btn" @click="onAdd">
|
||||||
<template #appendIcon>
|
<template #appendIcon>
|
||||||
<SvgIcon name="plus" :size="18" color="#fff" />
|
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
||||||
</template>
|
</template>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { gallery } from '@/utils/gallery'
|
||||||
import useModal from '@/composables/useModal'
|
import useModal from '@/composables/useModal'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
@@ -75,6 +76,8 @@ const router = useRouter()
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { openModal, isModal } = useModal()
|
const { openModal, isModal } = useModal()
|
||||||
|
|
||||||
|
const sessionIcon = gallery.sessionIcon
|
||||||
|
|
||||||
const filters = ref({
|
const filters = ref({
|
||||||
title: '',
|
title: '',
|
||||||
termId: '',
|
termId: '',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<form class="tickets-filters" @submit.prevent="onSubmit">
|
<form class="tickets-filters" @submit.prevent="onSubmit">
|
||||||
<div class="tickets-filters__grid">
|
<div class="tickets-filters__grid">
|
||||||
<TextField v-model="form.userName" name="userName" label="نام کاربر">
|
<TextField v-model="form.userName" name="userName" label="جستجو اسم کاربر">
|
||||||
<template #appendIcon>
|
<template #appendIcon>
|
||||||
<SvgIcon name="user" :size="20" color="var(--color-thd-gray)" />
|
<SvgIcon name="user" :size="20" color="var(--color-thd-gray)" />
|
||||||
</template>
|
</template>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
@click="onReset"
|
@click="onReset"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="close" :size="20" />
|
<SvgIcon name="close" color="black" :size="20" />
|
||||||
</template>
|
</template>
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
|
|||||||
@@ -14,7 +14,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SkeletonLoaderBlock v-if="isLoading && !ticket" :rows="3" :cols-per-row="1" />
|
<SkeletonLoaderBlock v-if="isLoading && !ticket" :rows="3" :cols-per-row="1" />
|
||||||
<div v-else-if="messages.length > 0" class="ticket-details__thread" ref="thread">
|
<div
|
||||||
|
v-else-if="messages.length > 0 || ticketMedia.length > 0"
|
||||||
|
class="ticket-details__thread"
|
||||||
|
ref="thread"
|
||||||
|
>
|
||||||
|
<div v-if="ticketMedia.length > 0" class="ticket-details__row ticket-details__row--user">
|
||||||
|
<div class="ticket-details__bubble">
|
||||||
|
<MessageAttachments :media="ticketMedia" />
|
||||||
|
<span class="ticket-details__time">{{ messageTime(ticket) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="message in messages"
|
v-for="message in messages"
|
||||||
:key="message.id"
|
:key="message.id"
|
||||||
@@ -22,6 +32,7 @@
|
|||||||
:class="`ticket-details__row--${senderClass(message)}`"
|
:class="`ticket-details__row--${senderClass(message)}`"
|
||||||
>
|
>
|
||||||
<div class="ticket-details__bubble">
|
<div class="ticket-details__bubble">
|
||||||
|
<MessageAttachments v-if="message.media?.length" :media="message.media" />
|
||||||
<p class="ticket-details__text">{{ message.message }}</p>
|
<p class="ticket-details__text">{{ message.message }}</p>
|
||||||
<span class="ticket-details__time">{{ messageTime(message) }}</span>
|
<span class="ticket-details__time">{{ messageTime(message) }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,6 +42,25 @@
|
|||||||
|
|
||||||
<div class="ticket-details__divider" />
|
<div class="ticket-details__divider" />
|
||||||
|
|
||||||
|
<div v-if="attachments.length > 0" class="ticket-details__attachments">
|
||||||
|
<span
|
||||||
|
v-for="(file, index) in attachments"
|
||||||
|
:key="file.id ?? index"
|
||||||
|
class="ticket-details__attachment"
|
||||||
|
>
|
||||||
|
<SvgIcon name="file" :size="14" color="currentColor" />
|
||||||
|
<span class="ticket-details__attachment-name">{{ file.name }}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ticket-details__attachment-remove"
|
||||||
|
aria-label="حذف فایل"
|
||||||
|
@click="removeAttachment(index)"
|
||||||
|
>
|
||||||
|
<SvgIcon name="close" :size="12" color="var(--color-error)" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form class="ticket-details__compose" @submit.prevent="onSend">
|
<form class="ticket-details__compose" @submit.prevent="onSend">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -57,6 +87,23 @@
|
|||||||
>
|
>
|
||||||
<SvgIcon name="mood" :size="30" color="currentColor" />
|
<SvgIcon name="mood" :size="30" color="currentColor" />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ticket-details__compose-btn"
|
||||||
|
:disabled="uploadMutation.isPending.value"
|
||||||
|
aria-label="پیوست فایل"
|
||||||
|
@click="fileInput?.click()"
|
||||||
|
>
|
||||||
|
<SvgIcon name="attach-file" :size="30" color="currentColor" />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
ref="fileInput"
|
||||||
|
type="file"
|
||||||
|
class="ticket-details__file-input"
|
||||||
|
accept="image/*,application/pdf"
|
||||||
|
multiple
|
||||||
|
@change="onFilesPicked"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,13 +128,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { toast } from 'vue3-toastify'
|
||||||
import useModal from '@/composables/useModal'
|
import useModal from '@/composables/useModal'
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
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 { formatJalaaliDate } from '@/utils/date-utils'
|
import { formatJalaaliDate } from '@/utils/date-utils'
|
||||||
|
import { objectToFormData } from '@/utils/object-to-formdata'
|
||||||
|
import { useUploadMediaMutation } from '@/services/query/auth'
|
||||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
|
import MessageAttachments from '@/components/blocks/MessageAttachments.vue'
|
||||||
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||||
import {
|
import {
|
||||||
autoUpdate,
|
autoUpdate,
|
||||||
@@ -143,6 +194,8 @@ const { data: ticket, isLoading } = useAdminTicketQuery(ticketId, {
|
|||||||
|
|
||||||
const messages = computed(() => ticket.value?.messages ?? [])
|
const messages = computed(() => ticket.value?.messages ?? [])
|
||||||
|
|
||||||
|
const ticketMedia = computed(() => ticket.value?.media ?? [])
|
||||||
|
|
||||||
const ticketDate = computed(() => formatJalaaliDate(ticket.value?.createdAt) || '—')
|
const ticketDate = computed(() => formatJalaaliDate(ticket.value?.createdAt) || '—')
|
||||||
|
|
||||||
// Anyone whose id matches the ticket's student is "the student"; everyone else
|
// Anyone whose id matches the ticket's student is "the student"; everyone else
|
||||||
@@ -157,7 +210,49 @@ const messageTime = (message) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const text = ref('')
|
const text = ref('')
|
||||||
const canSend = computed(() => text.value.trim().length > 0)
|
|
||||||
|
const MAX_ATTACHMENTS = 5
|
||||||
|
|
||||||
|
const fileInput = ref(null)
|
||||||
|
const attachments = ref([])
|
||||||
|
|
||||||
|
const uploadMutation = useUploadMediaMutation()
|
||||||
|
|
||||||
|
const onFilesPicked = async (event) => {
|
||||||
|
const picked = [...event.target.files]
|
||||||
|
event.target.value = ''
|
||||||
|
if (attachments.value.length + picked.length > MAX_ATTACHMENTS) {
|
||||||
|
toast.warning(`حداکثر تعداد فایل مجاز ${MAX_ATTACHMENTS} عدد است.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const file of picked) {
|
||||||
|
try {
|
||||||
|
const fd = objectToFormData({ file, purpose: 'attachment', context: 'ticket' })
|
||||||
|
const response = await uploadMutation.mutateAsync(fd)
|
||||||
|
const payload = response?.data ?? response
|
||||||
|
const id = payload?.id ?? payload?.uploadId
|
||||||
|
if (id == null) continue
|
||||||
|
attachments.value = [
|
||||||
|
...attachments.value,
|
||||||
|
{ id, name: file.name, size: file.size, url: payload?.url ?? '' },
|
||||||
|
]
|
||||||
|
} catch {
|
||||||
|
toast.error(`بارگذاری فایل "${file.name}" با خطا مواجه شد.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeAttachment = (index) => {
|
||||||
|
const next = [...attachments.value]
|
||||||
|
next.splice(index, 1)
|
||||||
|
attachments.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
const canSend = computed(
|
||||||
|
() =>
|
||||||
|
(text.value.trim().length > 0 || attachments.value.length > 0) &&
|
||||||
|
!uploadMutation.isPending.value
|
||||||
|
)
|
||||||
|
|
||||||
const emojiOpen = ref(false)
|
const emojiOpen = ref(false)
|
||||||
const emojiButton = ref(null)
|
const emojiButton = ref(null)
|
||||||
@@ -213,10 +308,15 @@ const sendMutation = useSendAdminTicketMessageMutation()
|
|||||||
const onSend = async () => {
|
const onSend = async () => {
|
||||||
if (!canSend.value || !ticketId.value) return
|
if (!canSend.value || !ticketId.value) return
|
||||||
const value = text.value.trim()
|
const value = text.value.trim()
|
||||||
|
const mediaIds = attachments.value.map((f) => f.id).filter((id) => id != null)
|
||||||
text.value = ''
|
text.value = ''
|
||||||
|
attachments.value = []
|
||||||
emojiOpen.value = false
|
emojiOpen.value = false
|
||||||
// Backend POST /admin/tickets/:id/messages — body is { message: string }.
|
// Backend POST /admin/tickets/:id/messages — body is { message: string,
|
||||||
await sendMutation.mutateAsync({ id: ticketId.value, payload: { message: value } })
|
// mediaIds?: number[] } where mediaIds reference previously uploaded media.
|
||||||
|
const payload = { message: value }
|
||||||
|
if (mediaIds.length > 0) payload.mediaIds = mediaIds
|
||||||
|
await sendMutation.mutateAsync({ id: ticketId.value, payload })
|
||||||
await queryClient.invalidateQueries({ queryKey: adminTicketsKeys.all })
|
await queryClient.invalidateQueries({ queryKey: adminTicketsKeys.all })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,6 +505,12 @@ watch(messages, async () => {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__attachments {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
&__attachment {
|
&__attachment {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -3,138 +3,253 @@ export default [
|
|||||||
path: '/main-dashboard',
|
path: '/main-dashboard',
|
||||||
name: 'admin-dashboard',
|
name: 'admin-dashboard',
|
||||||
component: () => import('@/features/admin/pages/AdminDashboardPage.vue'),
|
component: () => import('@/features/admin/pages/AdminDashboardPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'پیشخوان' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'سلام بر شما',
|
||||||
|
subtitle: 'به سامانه موسسه بانو خوش آمدید.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/users',
|
path: '/users',
|
||||||
name: 'admin-users',
|
name: 'admin-users',
|
||||||
component: () => import('@/features/admin/users/pages/UsersListPage.vue'),
|
component: () => import('@/features/admin/users/pages/UsersListPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'مدیریت کاربران' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'لیست کاربران',
|
||||||
|
subtitle: 'کنترل کامل روی اطلاعات، نقشها و وضعیت کاربران',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/add-user',
|
path: '/add-user',
|
||||||
name: 'admin-add-user',
|
name: 'admin-add-user',
|
||||||
component: () => import('@/features/admin/users/pages/UserFormPage.vue'),
|
component: () => import('@/features/admin/users/pages/UserFormPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'افزودن کاربر جدید' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'لیست کاربران',
|
||||||
|
subtitle: 'کنترل کامل روی اطلاعات، نقشها و وضعیت کاربران',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/edit-user/:id',
|
path: '/edit-user/:id',
|
||||||
name: 'admin-edit-user',
|
name: 'admin-edit-user',
|
||||||
component: () => import('@/features/admin/users/pages/UserFormPage.vue'),
|
component: () => import('@/features/admin/users/pages/UserFormPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'ویرایش کاربر' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'لیست کاربران',
|
||||||
|
subtitle: 'کنترل کامل روی اطلاعات، نقشها و وضعیت کاربران',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/user-verification',
|
path: '/user-verification',
|
||||||
name: 'admin-user-verification',
|
name: 'admin-user-verification',
|
||||||
component: () => import('@/features/admin/users/pages/UserVerificationPage.vue'),
|
component: () => import('@/features/admin/users/pages/UserVerificationPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'تایید هویت کاربران' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'کاربران سیستم من',
|
||||||
|
subtitle: 'دسترسی کامل به اطلاعات، نقشها و وضعیت کاربران',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/terms',
|
path: '/terms',
|
||||||
name: 'admin-terms',
|
name: 'admin-terms',
|
||||||
component: () => import('@/features/admin/terms/pages/TermsListPage.vue'),
|
component: () => import('@/features/admin/terms/pages/TermsListPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'مدیریت ترم' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/add-term',
|
path: '/add-term',
|
||||||
name: 'admin-add-term',
|
name: 'admin-add-term',
|
||||||
component: () => import('@/features/admin/terms/pages/TermFormPage.vue'),
|
component: () => import('@/features/admin/terms/pages/TermFormPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'افزودن ترم جدید' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/edit-term/:id',
|
path: '/edit-term/:id',
|
||||||
name: 'admin-edit-term',
|
name: 'admin-edit-term',
|
||||||
component: () => import('@/features/admin/terms/pages/TermFormPage.vue'),
|
component: () => import('@/features/admin/terms/pages/TermFormPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'ویرایش ترم' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/courses',
|
path: '/courses',
|
||||||
name: 'admin-courses',
|
name: 'admin-courses',
|
||||||
component: () => import('@/features/admin/courses/pages/CoursesListPage.vue'),
|
component: () => import('@/features/admin/courses/pages/CoursesListPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'مدیریت دوره' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/add-course',
|
path: '/add-course',
|
||||||
name: 'admin-add-course',
|
name: 'admin-add-course',
|
||||||
component: () => import('@/features/admin/courses/pages/CourseFormPage.vue'),
|
component: () => import('@/features/admin/courses/pages/CourseFormPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'افزودن دوره' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/edit-course/:id',
|
path: '/edit-course/:id',
|
||||||
name: 'admin-edit-course',
|
name: 'admin-edit-course',
|
||||||
component: () => import('@/features/admin/courses/pages/CourseFormPage.vue'),
|
component: () => import('@/features/admin/courses/pages/CourseFormPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'ویرایش دوره' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/terms/:termId/courses',
|
path: '/terms/:termId/courses',
|
||||||
name: 'admin-term-courses',
|
name: 'admin-term-courses',
|
||||||
component: () => import('@/features/admin/courses/pages/CoursesListPage.vue'),
|
component: () => import('@/features/admin/courses/pages/CoursesListPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'دورههای ترم' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/courses/:courseId/sessions',
|
path: '/courses/:courseId/sessions',
|
||||||
name: 'admin-course-sessions',
|
name: 'admin-course-sessions',
|
||||||
component: () => import('@/features/admin/sessions/pages/SessionsListPage.vue'),
|
component: () => import('@/features/admin/sessions/pages/SessionsListPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'جلسات دوره' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/sessions',
|
path: '/sessions',
|
||||||
name: 'admin-sessions',
|
name: 'admin-sessions',
|
||||||
component: () => import('@/features/admin/sessions/pages/SessionsListPage.vue'),
|
component: () => import('@/features/admin/sessions/pages/SessionsListPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'مدیریت جلسات' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/add-session',
|
path: '/add-session',
|
||||||
name: 'admin-add-session',
|
name: 'admin-add-session',
|
||||||
component: () => import('@/features/admin/sessions/pages/SessionFormPage.vue'),
|
component: () => import('@/features/admin/sessions/pages/SessionFormPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'افزودن جلسه جدید' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/edit-session/:id',
|
path: '/edit-session/:id',
|
||||||
name: 'admin-edit-session',
|
name: 'admin-edit-session',
|
||||||
component: () => import('@/features/admin/sessions/pages/SessionFormPage.vue'),
|
component: () => import('@/features/admin/sessions/pages/SessionFormPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'ویرایش جلسه' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/exams',
|
path: '/exams',
|
||||||
name: 'admin-exams',
|
name: 'admin-exams',
|
||||||
component: () => import('@/features/admin/exams/pages/ExamsListPage.vue'),
|
component: () => import('@/features/admin/exams/pages/ExamsListPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'مدیریت آزمونها' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/add-exam',
|
path: '/add-exam',
|
||||||
name: 'admin-add-exam',
|
name: 'admin-add-exam',
|
||||||
component: () => import('@/features/admin/exams/pages/ExamFormPage.vue'),
|
component: () => import('@/features/admin/exams/pages/ExamFormPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'افزودن آزمون جدید' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/edit-exam/:id',
|
path: '/edit-exam/:id',
|
||||||
name: 'admin-edit-exam',
|
name: 'admin-edit-exam',
|
||||||
component: () => import('@/features/admin/exams/pages/ExamFormPage.vue'),
|
component: () => import('@/features/admin/exams/pages/ExamFormPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'ویرایش آزمون' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/assignments',
|
path: '/assignments',
|
||||||
name: 'admin-assignments',
|
name: 'admin-assignments',
|
||||||
component: () => import('@/features/admin/assignments/pages/AssignmentsListPage.vue'),
|
component: () => import('@/features/admin/assignments/pages/AssignmentsListPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'مدیریت تکالیف' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز آموزش',
|
||||||
|
subtitle: 'دسترسی سریع به دورهها، آزمونها و نتایج آموزشی.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/messages',
|
path: '/messages',
|
||||||
name: 'admin-messages',
|
name: 'admin-messages',
|
||||||
component: () => import('@/features/admin/messages/pages/TicketsListPage.vue'),
|
component: () => import('@/features/admin/messages/pages/TicketsListPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'مدیریت پیامها' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز پیامها',
|
||||||
|
subtitle: 'تمام ارتباطات سیستمی و کاربری را در این بخش مدیریت نمایید.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/services',
|
path: '/services',
|
||||||
name: 'admin-services',
|
name: 'admin-services',
|
||||||
component: () => import('@/features/admin/services/pages/ServicesListPage.vue'),
|
component: () => import('@/features/admin/services/pages/ServicesListPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'مدیریت خدمات' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز خدمات',
|
||||||
|
subtitle: 'لیست خدمات را مشاهده و مدیریت نمایید.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/consultations',
|
path: '/consultations',
|
||||||
name: 'admin-consultations',
|
name: 'admin-consultations',
|
||||||
component: () => import('@/features/admin/consultations/pages/ConsultationsListPage.vue'),
|
component: () => import('@/features/admin/consultations/pages/ConsultationsListPage.vue'),
|
||||||
meta: { layout: 'admin', role: 'admin', title: 'مدیریت مشاوره' },
|
meta: {
|
||||||
|
layout: 'admin',
|
||||||
|
role: 'admin',
|
||||||
|
title: 'مرکز مشاوره',
|
||||||
|
subtitle: 'جلسات مشاوره را پیگیری کرده و پاسخگوی نیازهای کاربران باشید.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
@click="onReset"
|
@click="onReset"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="close" :size="20" />
|
<SvgIcon name="close" color="black" :size="20" />
|
||||||
</template>
|
</template>
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="sessions-filters__grid">
|
<div class="sessions-filters__grid">
|
||||||
<TextField v-model="form.title" name="title" label="عنوان جلسه">
|
<TextField v-model="form.title" name="title" label="عنوان جلسه">
|
||||||
<template #appendIcon>
|
<template #appendIcon>
|
||||||
<SvgIcon name="book" :size="20" color="var(--color-thd-gray)" />
|
<SvgIcon name="monitor" :size="20" color="var(--color-thd-gray)" />
|
||||||
</template>
|
</template>
|
||||||
</TextField>
|
</TextField>
|
||||||
<SelectField
|
<SelectField
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
@click="onReset"
|
@click="onReset"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="close" :size="20" />
|
<SvgIcon name="close" color="black" :size="20" />
|
||||||
</template>
|
</template>
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
|
|||||||
@@ -12,16 +12,6 @@
|
|||||||
|
|
||||||
<form class="session-form__form" @submit.prevent="onSubmit">
|
<form class="session-form__form" @submit.prevent="onSubmit">
|
||||||
<div class="session-form__grid">
|
<div class="session-form__grid">
|
||||||
<div class="session-form__image-col">
|
|
||||||
<ImageCropper
|
|
||||||
v-model="image"
|
|
||||||
name="image"
|
|
||||||
bg-color="#eeeeee"
|
|
||||||
@crop="onImageCropped"
|
|
||||||
@error="onImageError"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="session-form__main-col">
|
<div class="session-form__main-col">
|
||||||
<LineTitleBlock title="اطلاعات جلسه" title-en="Session Details" />
|
<LineTitleBlock title="اطلاعات جلسه" title-en="Session Details" />
|
||||||
|
|
||||||
@@ -112,6 +102,16 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="session-form__image-col">
|
||||||
|
<ImageCropper
|
||||||
|
v-model="image"
|
||||||
|
name="image"
|
||||||
|
bg-color="#eeeeee"
|
||||||
|
@crop="onImageCropped"
|
||||||
|
@error="onImageError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="session-form__divider" />
|
<div class="session-form__divider" />
|
||||||
@@ -392,32 +392,27 @@ const onCancel = () => router.push({ name: 'admin-sessions' })
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
grid-template-columns: 4fr 8fr;
|
grid-template-columns: 8fr 4fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
@media (min-width: 1280px) {
|
||||||
grid-template-columns: 3fr 9fr;
|
grid-template-columns: 9fr 3fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__image-col {
|
&__image-col {
|
||||||
order: 2;
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
order: 1;
|
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__main-col {
|
&__main-col {
|
||||||
order: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
order: 2;
|
padding-inline-end: 1.25rem;
|
||||||
padding-inline-start: 1.25rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
desc="در این قسمت میتوانید جلسات خود را مدیریت کنید"
|
desc="در این قسمت میتوانید جلسات خود را مدیریت کنید"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="book" :size="24" color="var(--color-primary)" />
|
<img :src="sessionIcon" alt="" />
|
||||||
</template>
|
</template>
|
||||||
</BoxedIconTitleBlock>
|
</BoxedIconTitleBlock>
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
</SimpleTitleIconBlock>
|
</SimpleTitleIconBlock>
|
||||||
<BaseButton text="افزودن جلسه جدید" custom-class="sessions-page__add-btn" @click="onAdd">
|
<BaseButton text="افزودن جلسه جدید" custom-class="sessions-page__add-btn" @click="onAdd">
|
||||||
<template #appendIcon>
|
<template #appendIcon>
|
||||||
<SvgIcon name="plus" :size="18" color="#fff" />
|
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
||||||
</template>
|
</template>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="terms-filters__grid">
|
<div class="terms-filters__grid">
|
||||||
<TextField v-model="form.title" name="title" label="عنوان">
|
<TextField v-model="form.title" name="title" label="عنوان">
|
||||||
<template #appendIcon>
|
<template #appendIcon>
|
||||||
<SvgIcon name="book" :size="20" color="var(--color-thd-gray)" />
|
<SvgIcon name="graduation-cap" :size="20" color="var(--color-thd-gray)" />
|
||||||
</template>
|
</template>
|
||||||
</TextField>
|
</TextField>
|
||||||
<SelectField
|
<SelectField
|
||||||
|
|||||||
@@ -14,16 +14,6 @@
|
|||||||
|
|
||||||
<form class="term-form__form" @submit.prevent="onSubmit">
|
<form class="term-form__form" @submit.prevent="onSubmit">
|
||||||
<div class="term-form__grid">
|
<div class="term-form__grid">
|
||||||
<div class="term-form__image-col">
|
|
||||||
<ImageCropper
|
|
||||||
v-model="image"
|
|
||||||
name="image"
|
|
||||||
bg-color="#eeeeee"
|
|
||||||
@crop="onImageCropped"
|
|
||||||
@error="onImageError"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="term-form__main-col">
|
<div class="term-form__main-col">
|
||||||
<LineTitleBlock title="اطلاعات ترم" title-en="Term Details" />
|
<LineTitleBlock title="اطلاعات ترم" title-en="Term Details" />
|
||||||
<div class="term-form__row">
|
<div class="term-form__row">
|
||||||
@@ -91,6 +81,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="term-form__image-col">
|
||||||
|
<ImageCropper
|
||||||
|
v-model="image"
|
||||||
|
name="image"
|
||||||
|
bg-color="#eeeeee"
|
||||||
|
@crop="onImageCropped"
|
||||||
|
@error="onImageError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="term-form__divider" />
|
<div class="term-form__divider" />
|
||||||
@@ -249,29 +249,23 @@ const onCancel = () => router.push({ name: 'admin-terms' })
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
grid-template-columns: 4fr 8fr;
|
grid-template-columns: 8fr 4fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
@media (min-width: 1280px) {
|
||||||
grid-template-columns: 3fr 9fr;
|
grid-template-columns: 9fr 3fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__image-col {
|
&__image-col {
|
||||||
order: 2;
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
order: 1;
|
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__main-col {
|
&__main-col {
|
||||||
order: 1;
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
order: 2;
|
padding-inline-end: 1.25rem;
|
||||||
padding-inline-start: 1.25rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +306,7 @@ const onCancel = () => router.push({ name: 'admin-terms' })
|
|||||||
|
|
||||||
&__actions {
|
&__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: space-between;
|
||||||
gap: 0.625rem;
|
gap: 0.625rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
@click="onReset"
|
@click="onReset"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="close" :size="20" />
|
<SvgIcon name="close" color="black" :size="20" />
|
||||||
</template>
|
</template>
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
|
|||||||
@@ -14,9 +14,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Badge v-if="user.status === 'blocked'" variant="danger" size="sm" dot>
|
<div class="users-table__middle">
|
||||||
{{ USER_STATUS.blocked }}
|
<div class="users-table__date">
|
||||||
</Badge>
|
<span class="users-table__date-label">تاریخ ثبت نام:</span>
|
||||||
|
<span class="users-table__date-value">
|
||||||
|
{{ formatJalaaliDateTime(user.createdAt) || '—' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -29,6 +33,10 @@
|
|||||||
<span>{{ roleLabel(user) || 'بدون نقش' }}</span>
|
<span>{{ roleLabel(user) || 'بدون نقش' }}</span>
|
||||||
<SvgIcon name="caret-down" :size="12" color="currentColor" />
|
<SvgIcon name="caret-down" :size="12" color="currentColor" />
|
||||||
</button>
|
</button>
|
||||||
|
<Badge v-if="user.status === 'blocked'" variant="danger" size="sm" dot>
|
||||||
|
{{ USER_STATUS.blocked }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="users-table__actions">
|
<div class="users-table__actions">
|
||||||
<button
|
<button
|
||||||
@@ -77,6 +85,7 @@ 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'
|
||||||
import DropdownMenu from '@/components/DropdownMenu.vue'
|
import DropdownMenu from '@/components/DropdownMenu.vue'
|
||||||
|
import { formatJalaaliDateTime } from '@/utils/date-utils'
|
||||||
import { adminUsersKeys, useUpdateAdminUserRoleMutation } from '@/services/query/admin-users'
|
import { adminUsersKeys, useUpdateAdminUserRoleMutation } from '@/services/query/admin-users'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
@@ -219,10 +228,38 @@ const menuItems = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__info {
|
&__info {
|
||||||
flex: 1;
|
flex: 1 1 33%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__middle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
flex: 1 1 33%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__date {
|
||||||
|
background: rgba(107, 107, 107, 5%);
|
||||||
|
padding: 0.25rem 1rem;
|
||||||
|
border-radius: 0.875rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__date-label {
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #848484;
|
||||||
|
margin-inline-end: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__date-value {
|
||||||
|
font-family: var(--font-family-en);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
&__name {
|
&__name {
|
||||||
font-family: var(--font-family-fa);
|
font-family: var(--font-family-fa);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|||||||
@@ -13,11 +13,7 @@
|
|||||||
|
|
||||||
<template v-else-if="user">
|
<template v-else-if="user">
|
||||||
<div class="user-details__section">
|
<div class="user-details__section">
|
||||||
<div v-if="isStudent && !user.profileCompleted" class="user-details__notice">
|
<div class="user-details__row user-details__row--main">
|
||||||
<SvgIcon name="warning" :size="20" color="var(--color-primary)" />
|
|
||||||
<span>اطلاعات ثبت نام این دانشجو کامل نیست.</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="user-details__head">
|
<div class="user-details__head">
|
||||||
<div class="user-details__avatar">
|
<div class="user-details__avatar">
|
||||||
<img v-if="user.avatarUrl" :src="user.avatarUrl" :alt="fullName" />
|
<img v-if="user.avatarUrl" :src="user.avatarUrl" :alt="fullName" />
|
||||||
@@ -30,14 +26,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="user-details__label">نام و نام خانوادگی</p>
|
<p class="user-details__label">نام و نام خانوادگی</p>
|
||||||
<p class="user-details__name">{{ fullName || '—' }}</p>
|
<p class="user-details__name">{{ fullName || '—' }}</p>
|
||||||
<p class="user-details__role">
|
<Badge
|
||||||
<span class="user-details__role-key">نقش:</span>
|
class="user-details__role"
|
||||||
<span class="user-details__role-val">{{ roleText }}</span>
|
:variant="roleVariant"
|
||||||
</p>
|
label="نقش:"
|
||||||
|
:value="roleText"
|
||||||
|
dot
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="user-details__grid">
|
|
||||||
<LineInfoBlock
|
<LineInfoBlock
|
||||||
title="تاریخ تولد"
|
title="تاریخ تولد"
|
||||||
:numeric-desc="formatJalaaliDate(user.birthday) || '—'"
|
:numeric-desc="formatJalaaliDate(user.birthday) || '—'"
|
||||||
@@ -49,11 +46,14 @@
|
|||||||
:desc="MARITAL_STATUS[user.marriageStatus] || '—'"
|
: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?.city?.name || '—'" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="user-details__row user-details__row--location">
|
||||||
|
<LineInfoBlock title="استان" :desc="user?.province?.name || '—'" />
|
||||||
|
<LineInfoBlock title="شهر" :desc="user?.city?.name || '—'" />
|
||||||
<LineInfoBlock title="آدرس" :desc="user?.address || '—'" />
|
<LineInfoBlock title="آدرس" :desc="user?.address || '—'" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<LineInfoBlock title="بیوگرافی" :desc="user.bio || '—'" />
|
<LineInfoBlock title="بیوگرافی" :desc="user.bio || '—'" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -95,6 +95,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import Badge from '@/components/Badge.vue'
|
||||||
import useModal from '@/composables/useModal'
|
import useModal from '@/composables/useModal'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
import BaseButton from '@/components/BaseButton.vue'
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
@@ -137,6 +138,16 @@ const roleText = computed(() => {
|
|||||||
return name ? ROLE_LABELS[name] || name : 'بدون نقش'
|
return name ? ROLE_LABELS[name] || name : 'بدون نقش'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const ROLE_BADGE_VARIANTS = {
|
||||||
|
student: 'danger',
|
||||||
|
teacher: 'warning',
|
||||||
|
counselor: 'blue',
|
||||||
|
missionary: 'info',
|
||||||
|
admin: 'neutral',
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleVariant = computed(() => ROLE_BADGE_VARIANTS[roleName.value] || 'neutral')
|
||||||
|
|
||||||
const isStudent = computed(() => roleName.value === 'student')
|
const isStudent = computed(() => roleName.value === 'student')
|
||||||
|
|
||||||
const statusModifier = computed(() =>
|
const statusModifier = computed(() =>
|
||||||
@@ -172,7 +183,43 @@ const statusModifier = computed(() =>
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.875rem;
|
gap: 0.875rem;
|
||||||
margin-bottom: 0.75rem;
|
}
|
||||||
|
|
||||||
|
&__row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__row--main {
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
grid-template-columns: auto repeat(5, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__row--main &__head {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
grid-column: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__row--location {
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__avatar {
|
&__avatar {
|
||||||
@@ -245,23 +292,7 @@ const statusModifier = computed(() =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__role {
|
&__role {
|
||||||
display: inline-flex;
|
margin-top: 0.25rem;
|
||||||
align-items: center;
|
|
||||||
gap: 0.25rem;
|
|
||||||
margin: 0.25rem 0 0;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
background-color: rgba(0, 0, 0, 4%);
|
|
||||||
font-family: var(--font-family-fa);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__role-key {
|
|
||||||
color: #808080;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__role-val {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__grid {
|
&__grid {
|
||||||
|
|||||||
@@ -21,25 +21,25 @@
|
|||||||
<div class="verification-item__date">
|
<div class="verification-item__date">
|
||||||
<span class="verification-item__date-label">تاریخ ثبت نام:</span>
|
<span class="verification-item__date-label">تاریخ ثبت نام:</span>
|
||||||
<span class="verification-item__date-value">
|
<span class="verification-item__date-value">
|
||||||
{{ formatJalaaliDate(user.createdAt) || '—' }}
|
{{ formatJalaaliDateTime(user.createdAt) || '—' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<Badge
|
||||||
v-if="user.status === 'verified'"
|
v-if="user.status === 'rejected'"
|
||||||
class="verification-item__badge verification-item__badge--pending"
|
variant="danger"
|
||||||
>
|
:dot="true"
|
||||||
<span class="verification-item__dot" />
|
value="رد شده"
|
||||||
<span>در انتظار تایید</span>
|
:clickable="true"
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
v-else-if="user.status === 'rejected'"
|
|
||||||
type="button"
|
|
||||||
class="verification-item__badge verification-item__badge--rejected"
|
|
||||||
@click="emit('show-reject-reason', user)"
|
@click="emit('show-reject-reason', user)"
|
||||||
>
|
/>
|
||||||
<span class="verification-item__dot" />
|
<Badge
|
||||||
<span>رد شده</span>
|
v-else-if="statusLabel"
|
||||||
</button>
|
:variant="statusVariant"
|
||||||
|
:dot="true"
|
||||||
|
:value="statusLabel"
|
||||||
|
:clickable="user.status === 'blocked'"
|
||||||
|
@click="emit('show-reject-reason', user)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="verification-item__actions">
|
<div class="verification-item__actions">
|
||||||
@@ -54,11 +54,11 @@
|
|||||||
</template>
|
</template>
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
v-if="user.status !== 'rejected'"
|
v-if="user.status !== 'blocked'"
|
||||||
tooltip="عدم تایید"
|
tooltip="عدم تایید"
|
||||||
bg-color="rgba(204, 40, 49, 0.08)"
|
bg-color="rgba(204, 40, 49, 0.08)"
|
||||||
size="2.5rem"
|
size="2.5rem"
|
||||||
@click="emit('verify-user', { id: user.id, status: 'rejected' })"
|
@click="emit('verify-user', { id: user.id, status: 'blocked' })"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="close" :size="20" color="var(--color-error)" />
|
<SvgIcon name="close" :size="20" color="var(--color-error)" />
|
||||||
@@ -80,10 +80,12 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { USER_STATUS } from '@/enums'
|
||||||
|
import Badge from '@/components/Badge.vue'
|
||||||
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 { formatJalaaliDate } from '@/utils/date-utils'
|
|
||||||
import CircleButton from '@/components/CircleButton.vue'
|
import CircleButton from '@/components/CircleButton.vue'
|
||||||
|
import { formatJalaaliDateTime } from '@/utils/date-utils'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
user: { type: Object, required: true },
|
user: { type: Object, required: true },
|
||||||
@@ -92,7 +94,16 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['show-details', 'show-reject-reason', 'verify-user'])
|
const emit = defineEmits(['show-details', 'show-reject-reason', 'verify-user'])
|
||||||
|
|
||||||
|
const STATUS_VARIANTS = {
|
||||||
|
pending: 'neutral',
|
||||||
|
verified: 'warning',
|
||||||
|
approved: 'success',
|
||||||
|
blocked: 'danger',
|
||||||
|
}
|
||||||
|
|
||||||
const fullName = computed(() => `${props.user.name || ''}`.trim() || '—')
|
const fullName = computed(() => `${props.user.name || ''}`.trim() || '—')
|
||||||
|
const statusLabel = computed(() => USER_STATUS[props.user.status] || '')
|
||||||
|
const statusVariant = computed(() => STATUS_VARIANTS[props.user.status] || 'neutral')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -202,37 +213,6 @@ const fullName = computed(() => `${props.user.name || ''}`.trim() || '—')
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.375rem;
|
|
||||||
padding: 0.25rem 1rem;
|
|
||||||
border-radius: 0.875rem;
|
|
||||||
font-family: var(--font-family-fa);
|
|
||||||
font-size: 0.7rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
border: none;
|
|
||||||
cursor: default;
|
|
||||||
|
|
||||||
&--pending {
|
|
||||||
background: rgba(0, 255, 68, 6%);
|
|
||||||
color: #009a12;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--rejected {
|
|
||||||
background: rgba(204, 40, 49, 6%);
|
|
||||||
color: var(--color-error);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__dot {
|
|
||||||
width: 0.375rem;
|
|
||||||
height: 0.375rem;
|
|
||||||
border-radius: 9999px;
|
|
||||||
background: currentcolor;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__actions {
|
&__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
@click="onReset"
|
@click="onReset"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon name="close" :size="20" />
|
<SvgIcon name="close" color="black" :size="20" />
|
||||||
</template>
|
</template>
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<div class="reject-reason-details">
|
<div class="reject-reason-details">
|
||||||
<div class="reject-reason-details__grid">
|
<div class="reject-reason-details__grid">
|
||||||
<div class="reject-reason-details__cell--full">
|
<div class="reject-reason-details__cell--full">
|
||||||
<LineInfoBlock title="توضیحات" :desc="verification(data).adminNote || ''" />
|
<LineInfoBlock title="توضیحات" :desc="data?.user?.blockReason || '—'" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -34,8 +34,6 @@ import SvgIcon from '@/components/icons/SvgIcon.vue'
|
|||||||
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
|
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'RejectReasonDetailsModal' })
|
defineOptions({ name: 'RejectReasonDetailsModal' })
|
||||||
|
|
||||||
const verification = (data) => data?.verification || {}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -9,31 +9,13 @@
|
|||||||
>
|
>
|
||||||
<template #default="{ data, close }">
|
<template #default="{ data, close }">
|
||||||
<form class="reject-reason-modal" @submit.prevent="onSubmit(data, close)">
|
<form class="reject-reason-modal" @submit.prevent="onSubmit(data, close)">
|
||||||
<div class="reject-reason-modal__row">
|
|
||||||
<TextField
|
|
||||||
v-model="form.rejectionReason"
|
|
||||||
name="rejectionReason"
|
|
||||||
label="علت رد"
|
|
||||||
:error="errors.rejectionReason"
|
|
||||||
@blur="validateAt('rejectionReason', form.rejectionReason)"
|
|
||||||
/>
|
|
||||||
<SelectField
|
|
||||||
:model-value="form.priority?.id"
|
|
||||||
name="priority"
|
|
||||||
label="وضعیت تیکت"
|
|
||||||
:options="statusOptions"
|
|
||||||
option-label="label"
|
|
||||||
option-value="value"
|
|
||||||
:error="errors.priority"
|
|
||||||
@update:model-value="(e) => handleChange('priority', { id: e })"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<TextareaField
|
<TextareaField
|
||||||
v-model="form.adminNote"
|
v-model="form.reason"
|
||||||
name="adminNote"
|
name="reason"
|
||||||
label="لطفا علت رد را توضیح دهید"
|
label="لطفا علت رد را توضیح دهید"
|
||||||
:row="4"
|
:row="4"
|
||||||
:error="errors.adminNote"
|
:error="errors.reason"
|
||||||
|
@blur="validateAt('reason', form.reason)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="reject-reason-modal__divider" />
|
<div class="reject-reason-modal__divider" />
|
||||||
@@ -68,14 +50,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import useYup from '@/composables/useYup'
|
import useYup from '@/composables/useYup'
|
||||||
import { PRIORITY_STATUS } from '@/enums'
|
|
||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
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 { rejectionReasonSchema } from '../../../schema'
|
import { rejectionReasonSchema } from '../../../schema'
|
||||||
import SelectField from '@/components/form/SelectField.vue'
|
|
||||||
import TextareaField from '@/components/form/TextareaField.vue'
|
import TextareaField from '@/components/form/TextareaField.vue'
|
||||||
import { adminUsersKeys, useChangeAdminUserStatusMutation } from '@/services/query/admin-users'
|
import { adminUsersKeys, useChangeAdminUserStatusMutation } from '@/services/query/admin-users'
|
||||||
|
|
||||||
@@ -84,22 +63,12 @@ defineOptions({ name: 'RejectReasonModal' })
|
|||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const changeStatusMutation = useChangeAdminUserStatusMutation()
|
const changeStatusMutation = useChangeAdminUserStatusMutation()
|
||||||
|
|
||||||
const DEFAULT_FORM = { rejectionReason: '', adminNote: '' }
|
const DEFAULT_FORM = { reason: '' }
|
||||||
|
|
||||||
const { validate, validateAt, errors, resetErrors } = useYup(rejectionReasonSchema)
|
const { validate, validateAt, errors, resetErrors } = useYup(rejectionReasonSchema)
|
||||||
|
|
||||||
const form = ref(structuredClone({ ...DEFAULT_FORM }))
|
const form = ref(structuredClone({ ...DEFAULT_FORM }))
|
||||||
|
|
||||||
const statusOptions = Object.entries(PRIORITY_STATUS).map(([value, label]) => ({
|
|
||||||
value,
|
|
||||||
label,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const handleChange = (key, value) => {
|
|
||||||
form.value = { ...form.value, [key]: value }
|
|
||||||
validateAt(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSubmit = async (data, close) => {
|
const onSubmit = async (data, close) => {
|
||||||
const { isValid, payload } = await validate(form.value)
|
const { isValid, payload } = await validate(form.value)
|
||||||
if (!isValid) return
|
if (!isValid) return
|
||||||
@@ -107,7 +76,7 @@ const onSubmit = async (data, close) => {
|
|||||||
if (!id) return
|
if (!id) return
|
||||||
await changeStatusMutation.mutateAsync({
|
await changeStatusMutation.mutateAsync({
|
||||||
id,
|
id,
|
||||||
payload: { status: 'blocked', ...payload },
|
payload: { status: 'blocked', reason: payload.reason },
|
||||||
})
|
})
|
||||||
await queryClient.invalidateQueries({ queryKey: adminUsersKeys.all })
|
await queryClient.invalidateQueries({ queryKey: adminUsersKeys.all })
|
||||||
form.value = structuredClone({ ...DEFAULT_FORM })
|
form.value = structuredClone({ ...DEFAULT_FORM })
|
||||||
@@ -122,11 +91,6 @@ const onSubmit = async (data, close) => {
|
|||||||
text-align: start;
|
text-align: start;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
|
||||||
&__row {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__divider {
|
&__divider {
|
||||||
border-block-end: 1px solid var(--color-thd-gray);
|
border-block-end: 1px solid var(--color-thd-gray);
|
||||||
margin-block: 1rem;
|
margin-block: 1rem;
|
||||||
|
|||||||
@@ -21,10 +21,7 @@
|
|||||||
/>
|
/>
|
||||||
<LineInfoBlock title="شماره تماس" :numeric-desc="personal.phone" />
|
<LineInfoBlock title="شماره تماس" :numeric-desc="personal.phone" />
|
||||||
<LineInfoBlock title="کد ملی" :numeric-desc="personal.nationalCode" />
|
<LineInfoBlock title="کد ملی" :numeric-desc="personal.nationalCode" />
|
||||||
<LineInfoBlock
|
<LineInfoBlock title="وضعیت" :desc="MARITAL_STATUS[personal.maritalStatus] || ''" />
|
||||||
title="وضعیت تاهل"
|
|
||||||
:desc="MARITAL_STATUS[personal.maritalStatus] || ''"
|
|
||||||
/>
|
|
||||||
<LineInfoBlock title="جنسیت" :desc="GENDER[personal.gender] || ''" />
|
<LineInfoBlock title="جنسیت" :desc="GENDER[personal.gender] || ''" />
|
||||||
<LineInfoBlock title="استان" :desc="personal.province || '-'" />
|
<LineInfoBlock title="استان" :desc="personal.province || '-'" />
|
||||||
<LineInfoBlock title="شهر" :desc="personal.city || '-'" />
|
<LineInfoBlock title="شهر" :desc="personal.city || '-'" />
|
||||||
|
|||||||
@@ -14,16 +14,6 @@
|
|||||||
|
|
||||||
<form class="user-form__form" @submit.prevent="onSubmit">
|
<form class="user-form__form" @submit.prevent="onSubmit">
|
||||||
<div class="user-form__grid">
|
<div class="user-form__grid">
|
||||||
<div class="user-form__avatar-col">
|
|
||||||
<ImageCropper
|
|
||||||
v-model="avatar"
|
|
||||||
name="avatar"
|
|
||||||
bg-color="#eeeeee"
|
|
||||||
@crop="onAvatarCropped"
|
|
||||||
@error="onAvatarError"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="user-form__main-col">
|
<div class="user-form__main-col">
|
||||||
<LineTitleBlock title="مشخصات فردی" title-en="Personal Details" />
|
<LineTitleBlock title="مشخصات فردی" title-en="Personal Details" />
|
||||||
<div class="user-form__row">
|
<div class="user-form__row">
|
||||||
@@ -200,6 +190,16 @@
|
|||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="user-form__avatar-col">
|
||||||
|
<ImageCropper
|
||||||
|
v-model="avatar"
|
||||||
|
name="avatar"
|
||||||
|
bg-color="#eeeeee"
|
||||||
|
@crop="onAvatarCropped"
|
||||||
|
@error="onAvatarError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -285,7 +285,7 @@ const avatar = ref(null)
|
|||||||
|
|
||||||
const schema = isEditMode.value ? userEditSchema : userAddSchema
|
const schema = isEditMode.value ? userEditSchema : userAddSchema
|
||||||
|
|
||||||
const { validate, validateAt, errors } = useYup(schema)
|
const { validate, validateAt, errors } = useYup(schema, () => form.value)
|
||||||
|
|
||||||
const onProvinceChange = () => {
|
const onProvinceChange = () => {
|
||||||
provinceIdRef.value = form.value.provinceId
|
provinceIdRef.value = form.value.provinceId
|
||||||
@@ -336,7 +336,7 @@ const uploadMutation = useUploadMediaMutation()
|
|||||||
|
|
||||||
const onAvatarCropped = async (file) => {
|
const onAvatarCropped = async (file) => {
|
||||||
try {
|
try {
|
||||||
const formData = objectToFormData({ file, subType: 'avatar', context: 'user' })
|
const formData = objectToFormData({ file, purpose: 'avatar', context: 'user' })
|
||||||
const response = await uploadMutation.mutateAsync(formData)
|
const response = await uploadMutation.mutateAsync(formData)
|
||||||
const payload = response?.data ?? response
|
const payload = response?.data ?? response
|
||||||
avatar.value = { url: payload?.url, uploadId: payload?.uploadId, ...payload }
|
avatar.value = { url: payload?.url, uploadId: payload?.uploadId, ...payload }
|
||||||
@@ -411,29 +411,23 @@ const onCancel = () => router.push({ name: 'admin-users' })
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
grid-template-columns: 4fr 8fr;
|
grid-template-columns: 8fr 4fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
@media (min-width: 1280px) {
|
||||||
grid-template-columns: 3fr 9fr;
|
grid-template-columns: 9fr 3fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__avatar-col {
|
&__avatar-col {
|
||||||
order: 2;
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
order: 1;
|
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__main-col {
|
&__main-col {
|
||||||
order: 1;
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
order: 2;
|
padding-inline-end: 1.25rem;
|
||||||
padding-inline-start: 1.25rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ const onFilterReset = () => resetPagination()
|
|||||||
const changeStatusMutation = useChangeAdminUserStatusMutation()
|
const changeStatusMutation = useChangeAdminUserStatusMutation()
|
||||||
|
|
||||||
const onVerifyUser = ({ id, status }) => {
|
const onVerifyUser = ({ id, status }) => {
|
||||||
if (status === 'rejected') {
|
if (status === 'blocked') {
|
||||||
openModal('RejectReasonModal', { id })
|
openModal('RejectReasonModal', { id })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -106,7 +106,7 @@ const onShowDetails = (id) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onShowRejectReason = (user) => {
|
const onShowRejectReason = (user) => {
|
||||||
openModal('RejectReasonDetailsModal', { verification: user.verification })
|
openModal('RejectReasonDetailsModal', { user })
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</SimpleTitleIconBlock>
|
</SimpleTitleIconBlock>
|
||||||
<BaseButton text="افزودن کاربر جدید" custom-class="users-page__add-btn" @click="onAdd">
|
<BaseButton text="افزودن کاربر جدید" custom-class="users-page__add-btn" @click="onAdd">
|
||||||
<template #appendIcon>
|
<template #appendIcon>
|
||||||
<SvgIcon name="plus" :size="18" color="#fff" />
|
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
||||||
</template>
|
</template>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,14 +5,7 @@ import { nationalCodeRule } from '@/constants/rules/nationalCodeRule'
|
|||||||
import { passwordConfirmationRule } from '@/constants/rules/passwordConfirmationRule'
|
import { passwordConfirmationRule } from '@/constants/rules/passwordConfirmationRule'
|
||||||
|
|
||||||
export const rejectionReasonSchema = object().shape({
|
export const rejectionReasonSchema = object().shape({
|
||||||
rejectionReason: string().required(),
|
reason: string().required(),
|
||||||
priority: object()
|
|
||||||
.shape({
|
|
||||||
id: string().required().default(null),
|
|
||||||
})
|
|
||||||
.required()
|
|
||||||
.default(null),
|
|
||||||
adminNote: string().notRequired(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const baseUserShape = {
|
const baseUserShape = {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
<SvgIcon name="arrow-left" :size="22" color="#fff" />
|
<SvgIcon name="arrow-left" :size="22" color="#fff" />
|
||||||
</template>
|
</template>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="login-form__links">
|
<div class="login-form__links">
|
||||||
<div class="login-form__link">
|
<div class="login-form__link">
|
||||||
@@ -55,7 +56,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="auth-form__footer">
|
<div class="auth-form__footer">
|
||||||
<AuthCopyright />
|
<AuthCopyright />
|
||||||
@@ -110,26 +110,30 @@ const goForgotPassword = () => emit('next', { mode: 'forgotPassword' })
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
&__body {
|
&__body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-grow: 1;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
flex-grow: 1;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__footer {
|
&__footer {
|
||||||
|
margin-top: auto;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,16 +166,12 @@ const goForgotPassword = () => emit('next', { mode: 'forgotPassword' })
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-form__links {
|
.login-form__links {
|
||||||
margin-top: 1.5rem;
|
margin: 0.5rem 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form__link {
|
.login-form__link {
|
||||||
|
|||||||
@@ -34,13 +34,13 @@
|
|||||||
<SvgIcon name="arrow-left" :size="22" color="#fff" />
|
<SvgIcon name="arrow-left" :size="22" color="#fff" />
|
||||||
</template>
|
</template>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="login-otp__links">
|
<div class="login-otp__links">
|
||||||
<button type="button" class="login-otp__back" @click="emit('back')">
|
<button type="button" class="login-otp__back" @click="emit('back')">
|
||||||
بازگشت به ورود با رمز عبور
|
بازگشت به ورود با رمز عبور
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="auth-form__footer">
|
<div class="auth-form__footer">
|
||||||
<AuthCopyright />
|
<AuthCopyright />
|
||||||
@@ -98,26 +98,30 @@ const onSubmit = async () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
&__body {
|
&__body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-grow: 1;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
flex-grow: 1;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__footer {
|
&__footer {
|
||||||
|
margin-top: auto;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,14 +131,10 @@ const onSubmit = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-otp__links {
|
.login-otp__links {
|
||||||
margin-top: 1.5rem;
|
margin: 0.5rem 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-otp__back {
|
.login-otp__back {
|
||||||
|
|||||||
@@ -67,13 +67,13 @@
|
|||||||
<SvgIcon name="arrow-left" :size="22" color="#fff" />
|
<SvgIcon name="arrow-left" :size="22" color="#fff" />
|
||||||
</template>
|
</template>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="signup-form__login-link">
|
<div class="signup-form__login-link">
|
||||||
<SvgIcon name="user" :size="12" color="#A7A7A7" />
|
<SvgIcon name="user" :size="12" color="#A7A7A7" />
|
||||||
<span>حساب کاربری دارم</span>
|
<span>حساب کاربری دارم</span>
|
||||||
<RouterLink :to="{ name: 'login' }">ورود</RouterLink>
|
<RouterLink :to="{ name: 'login' }">ورود</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="auth-form__footer">
|
<div class="auth-form__footer">
|
||||||
<AuthCopyright />
|
<AuthCopyright />
|
||||||
@@ -131,26 +131,30 @@ const onSubmit = async () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
&__body {
|
&__body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-grow: 1;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
flex-grow: 1;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__footer {
|
&__footer {
|
||||||
|
margin-top: auto;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +164,7 @@ const onSubmit = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.signup-form__login-link {
|
.signup-form__login-link {
|
||||||
margin-top: 1.5rem;
|
margin: 0.5rem 0;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
@@ -168,10 +172,6 @@ const onSubmit = async () => {
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: #a7a7a7;
|
color: #a7a7a7;
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--color-sec-gray);
|
color: var(--color-sec-gray);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
<SvgIcon name="arrow-left" :size="22" color="#fff" />
|
<SvgIcon name="arrow-left" :size="22" color="#fff" />
|
||||||
</template>
|
</template>
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="verify__links">
|
<div class="verify__links">
|
||||||
<div class="verify__link">
|
<div class="verify__link">
|
||||||
@@ -43,7 +44,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="auth-form__footer">
|
<div class="auth-form__footer">
|
||||||
<AuthCopyright />
|
<AuthCopyright />
|
||||||
@@ -137,7 +137,6 @@ const onSubmit = async () => {
|
|||||||
const response = await verifySignupMutation.mutateAsync({ phone: props.phoneNumber, code })
|
const response = await verifySignupMutation.mutateAsync({ phone: props.phoneNumber, code })
|
||||||
applySession(response?.data ?? response)
|
applySession(response?.data ?? response)
|
||||||
emit('verified', { phone: props.phoneNumber, code })
|
emit('verified', { phone: props.phoneNumber, code })
|
||||||
redirectAfterLogin()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onResend = async () => {
|
const onResend = async () => {
|
||||||
@@ -154,26 +153,30 @@ const onResend = async () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
&__body {
|
&__body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-grow: 1;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
flex-grow: 1;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__footer {
|
&__footer {
|
||||||
|
margin-top: auto;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,16 +204,12 @@ const onResend = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.verify__links {
|
.verify__links {
|
||||||
margin-top: 1.5rem;
|
margin: 0.5rem 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.verify__link {
|
.verify__link {
|
||||||
|
|||||||
@@ -111,11 +111,8 @@ import TextareaField from '@/components/form/TextareaField.vue'
|
|||||||
import { useSaveRegisterDataMutation } from '@/services/query/auth'
|
import { useSaveRegisterDataMutation } from '@/services/query/auth'
|
||||||
import StepActions from '@/features/auth/components/studentRegister/StepActions.vue'
|
import StepActions from '@/features/auth/components/studentRegister/StepActions.vue'
|
||||||
import { educationalInformationSchema } from '@/features/auth/schema/student-register'
|
import { educationalInformationSchema } from '@/features/auth/schema/student-register'
|
||||||
|
import { useStudentRegistrationStore } from '@/features/auth/store/student-registration'
|
||||||
import { EDUCATION_STATUS, SEMINARY_LEVEL, STUDENT_REGISTRATION, UNIVERSITY_LEVEL } from '@/enums'
|
import { EDUCATION_STATUS, SEMINARY_LEVEL, STUDENT_REGISTRATION, UNIVERSITY_LEVEL } from '@/enums'
|
||||||
import {
|
|
||||||
toKeyValueList,
|
|
||||||
useStudentRegistrationStore,
|
|
||||||
} from '@/features/auth/store/student-registration'
|
|
||||||
|
|
||||||
const store = useStudentRegistrationStore()
|
const store = useStudentRegistrationStore()
|
||||||
|
|
||||||
@@ -148,7 +145,7 @@ const saveRegisterMutation = useSaveRegisterDataMutation()
|
|||||||
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
|
||||||
await saveRegisterMutation.mutateAsync(toKeyValueList(payload))
|
await saveRegisterMutation.mutateAsync(payload)
|
||||||
store.educational = { ...store.educational, ...payload }
|
store.educational = { ...store.educational, ...payload }
|
||||||
store.markStepAsCompleted(STUDENT_REGISTRATION.EDUCATIONAL_INFORMATION)
|
store.markStepAsCompleted(STUDENT_REGISTRATION.EDUCATIONAL_INFORMATION)
|
||||||
store.goToNextStep()
|
store.goToNextStep()
|
||||||
|
|||||||
@@ -30,11 +30,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="personal-info__cell">
|
<div class="personal-info__cell">
|
||||||
<DatePickerField
|
<DatePickerField
|
||||||
v-model="form.birthDate"
|
v-model="form.birthday"
|
||||||
name="birthDate"
|
name="birthday"
|
||||||
label="تاریخ تولد"
|
label="تاریخ تولد"
|
||||||
:max="maxBirthDate"
|
:max="maxBirthDate"
|
||||||
:error="errors.birthDate"
|
:error="errors.birthday"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="personal-info__cell">
|
<div class="personal-info__cell">
|
||||||
@@ -198,21 +198,25 @@ const maxBirthDate = computed(() => {
|
|||||||
const initialPhone = ref(user.value?.phone || store.personal.phone || '')
|
const initialPhone = ref(user.value?.phone || store.personal.phone || '')
|
||||||
const initialNationalCode = ref(user.value?.nationalCode || store.personal.nationalCode || '')
|
const initialNationalCode = ref(user.value?.nationalCode || store.personal.nationalCode || '')
|
||||||
|
|
||||||
|
const initialAvatar =
|
||||||
|
store.personal.avatar || (user.value?.avatarUrl ? { url: user.value.avatarUrl } : null)
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
name: user.value?.name || store.personal.name || '',
|
name: user.value?.name || store.personal.name || '',
|
||||||
birthDate: store.personal.birthDate || '',
|
birthday: user.value?.birthday || store.personal.birthday || '',
|
||||||
nationalCode: initialNationalCode.value,
|
nationalCode: initialNationalCode.value,
|
||||||
maritalStatus: store.personal.maritalStatus || '',
|
maritalStatus: user.value?.marriageStatus || store.personal.maritalStatus || '',
|
||||||
gender: store.personal.gender || '',
|
gender: user.value?.gender || store.personal.gender || '',
|
||||||
phone: initialPhone.value,
|
phone: initialPhone.value,
|
||||||
provinceId: store.personal.provinceId || null,
|
provinceId: user.value?.provinceId || store.personal.provinceId || null,
|
||||||
cityId: store.personal.cityId || null,
|
cityId: user.value?.cityId || store.personal.cityId || null,
|
||||||
address: store.personal.address || '',
|
address:
|
||||||
avatar: store.personal.avatar || null,
|
(typeof user.value?.address === 'string' && user.value.address) || store.personal.address || '',
|
||||||
|
avatar: initialAvatar,
|
||||||
avatarId: store.personal.avatarId || '',
|
avatarId: store.personal.avatarId || '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const avatar = ref(store.personal.avatar || null)
|
const avatar = ref(initialAvatar)
|
||||||
|
|
||||||
const { validate, validateAt, errors } = useYup(personalInformationSchema)
|
const { validate, validateAt, errors } = useYup(personalInformationSchema)
|
||||||
|
|
||||||
@@ -263,7 +267,7 @@ const onSubmit = async () => {
|
|||||||
// (http auto-converts camelCase → snake_case: birthday, national_code, …)
|
// (http auto-converts camelCase → snake_case: birthday, national_code, …)
|
||||||
const body = {
|
const body = {
|
||||||
name: payload.name,
|
name: payload.name,
|
||||||
birthday: payload.birthDate,
|
birthday: payload.birthday,
|
||||||
nationalCode: payload.nationalCode,
|
nationalCode: payload.nationalCode,
|
||||||
marriageStatus: payload.maritalStatus,
|
marriageStatus: payload.maritalStatus,
|
||||||
gender: payload.gender,
|
gender: payload.gender,
|
||||||
|
|||||||
@@ -117,10 +117,7 @@ import { useSaveRegisterDataMutation } from '@/services/query/auth'
|
|||||||
import { PROPAGATION_PLATFORM, STUDENT_REGISTRATION } from '@/enums'
|
import { PROPAGATION_PLATFORM, STUDENT_REGISTRATION } from '@/enums'
|
||||||
import StepActions from '@/features/auth/components/studentRegister/StepActions.vue'
|
import StepActions from '@/features/auth/components/studentRegister/StepActions.vue'
|
||||||
import { professionalInformationSchema } from '@/features/auth/schema/student-register'
|
import { professionalInformationSchema } from '@/features/auth/schema/student-register'
|
||||||
import {
|
import { useStudentRegistrationStore } from '@/features/auth/store/student-registration'
|
||||||
toKeyValueList,
|
|
||||||
useStudentRegistrationStore,
|
|
||||||
} from '@/features/auth/store/student-registration'
|
|
||||||
|
|
||||||
const store = useStudentRegistrationStore()
|
const store = useStudentRegistrationStore()
|
||||||
|
|
||||||
@@ -193,7 +190,7 @@ const onSubmit = async () => {
|
|||||||
specializedTopics: payload.specializedTopics,
|
specializedTopics: payload.specializedTopics,
|
||||||
propagationPlatforms,
|
propagationPlatforms,
|
||||||
}
|
}
|
||||||
await saveRegisterMutation.mutateAsync(toKeyValueList(next))
|
await saveRegisterMutation.mutateAsync(next)
|
||||||
store.professional = next
|
store.professional = next
|
||||||
store.markStepAsCompleted(STUDENT_REGISTRATION.PROFESSIONAL_INFORMATION)
|
store.markStepAsCompleted(STUDENT_REGISTRATION.PROFESSIONAL_INFORMATION)
|
||||||
store.goToNextStep()
|
store.goToNextStep()
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
<SvgIcon :name="tab.icon" :size="22" color="#fff" />
|
<SvgIcon :name="tab.icon" :size="22" color="#fff" />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<SidebarLogout color="#fff" stacked />
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { gallery } from '@/utils/gallery'
|
import { gallery } from '@/utils/gallery'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import SidebarLogout from '@/layouts/sidebars/SidebarLogout.vue'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
tabs: { type: Array, required: true },
|
tabs: { type: Array, required: true },
|
||||||
|
|||||||
@@ -42,10 +42,7 @@ import { useSaveRegisterDataMutation } from '@/services/query/auth'
|
|||||||
import StepActions from '@/features/auth/components/studentRegister/StepActions.vue'
|
import StepActions from '@/features/auth/components/studentRegister/StepActions.vue'
|
||||||
import { skillAssessmentPart6Schema } from '@/features/auth/schema/student-register'
|
import { skillAssessmentPart6Schema } from '@/features/auth/schema/student-register'
|
||||||
import MediaRecorder from '@/features/auth/components/studentRegister/MediaRecorder.vue'
|
import MediaRecorder from '@/features/auth/components/studentRegister/MediaRecorder.vue'
|
||||||
import {
|
import { useStudentRegistrationStore } from '@/features/auth/store/student-registration'
|
||||||
toKeyValueList,
|
|
||||||
useStudentRegistrationStore,
|
|
||||||
} from '@/features/auth/store/student-registration'
|
|
||||||
|
|
||||||
const store = useStudentRegistrationStore()
|
const store = useStudentRegistrationStore()
|
||||||
|
|
||||||
@@ -70,7 +67,7 @@ const saveRegisterMutation = useSaveRegisterDataMutation()
|
|||||||
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
|
||||||
await saveRegisterMutation.mutateAsync(toKeyValueList(payload))
|
await saveRegisterMutation.mutateAsync(payload)
|
||||||
store.skill6 = { ...store.skill6, ...payload }
|
store.skill6 = { ...store.skill6, ...payload }
|
||||||
store.markStepAsCompleted(STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_6)
|
store.markStepAsCompleted(STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_6)
|
||||||
store.goToNextStep()
|
store.goToNextStep()
|
||||||
|
|||||||
@@ -46,10 +46,7 @@ import { useSaveRegisterDataMutation } from '@/services/query/auth'
|
|||||||
import StepActions from '@/features/auth/components/studentRegister/StepActions.vue'
|
import StepActions from '@/features/auth/components/studentRegister/StepActions.vue'
|
||||||
import { skillAssessmentPart7Schema } from '@/features/auth/schema/student-register'
|
import { skillAssessmentPart7Schema } from '@/features/auth/schema/student-register'
|
||||||
import MediaRecorder from '@/features/auth/components/studentRegister/MediaRecorder.vue'
|
import MediaRecorder from '@/features/auth/components/studentRegister/MediaRecorder.vue'
|
||||||
import {
|
import { useStudentRegistrationStore } from '@/features/auth/store/student-registration'
|
||||||
toKeyValueList,
|
|
||||||
useStudentRegistrationStore,
|
|
||||||
} from '@/features/auth/store/student-registration'
|
|
||||||
|
|
||||||
const store = useStudentRegistrationStore()
|
const store = useStudentRegistrationStore()
|
||||||
|
|
||||||
@@ -75,7 +72,7 @@ const onSubmit = async () => {
|
|||||||
const { isValid, payload } = await validate(form.value)
|
const { isValid, payload } = await validate(form.value)
|
||||||
if (!isValid) return
|
if (!isValid) return
|
||||||
try {
|
try {
|
||||||
await saveRegisterMutation.mutateAsync(toKeyValueList(payload))
|
await saveRegisterMutation.mutateAsync(payload)
|
||||||
store.skill7 = { ...store.skill7, ...payload }
|
store.skill7 = { ...store.skill7, ...payload }
|
||||||
store.markStepAsCompleted(STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_7)
|
store.markStepAsCompleted(STUDENT_REGISTRATION.SKILL_ASSESSMENT_PART_7)
|
||||||
store.isProfileCompleted = true
|
store.isProfileCompleted = true
|
||||||
|
|||||||
@@ -46,10 +46,7 @@ import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
|||||||
import TextareaField from '@/components/form/TextareaField.vue'
|
import TextareaField from '@/components/form/TextareaField.vue'
|
||||||
import { useSaveRegisterDataMutation } from '@/services/query/auth'
|
import { useSaveRegisterDataMutation } from '@/services/query/auth'
|
||||||
import StepActions from '@/features/auth/components/studentRegister/StepActions.vue'
|
import StepActions from '@/features/auth/components/studentRegister/StepActions.vue'
|
||||||
import {
|
import { useStudentRegistrationStore } from '@/features/auth/store/student-registration'
|
||||||
toKeyValueList,
|
|
||||||
useStudentRegistrationStore,
|
|
||||||
} from '@/features/auth/store/student-registration'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
step: { type: Number, required: true },
|
step: { type: Number, required: true },
|
||||||
@@ -89,7 +86,7 @@ const saveRegisterMutation = useSaveRegisterDataMutation()
|
|||||||
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
|
||||||
await saveRegisterMutation.mutateAsync(toKeyValueList(payload))
|
await saveRegisterMutation.mutateAsync(payload)
|
||||||
store[props.storeKey] = { ...store[props.storeKey], ...payload }
|
store[props.storeKey] = { ...store[props.storeKey], ...payload }
|
||||||
store.markStepAsCompleted(props.step)
|
store.markStepAsCompleted(props.step)
|
||||||
store.goToNextStep()
|
store.goToNextStep()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ROLE_HOME_ROUTES } from '@/enums'
|
||||||
import { useAuthStore } from '@/store/auth'
|
import { useAuthStore } from '@/store/auth'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
@@ -12,8 +13,7 @@ export const useRedirectAfterLogin = () => {
|
|||||||
router.push(redirect).catch(() => router.push('/'))
|
router.push(redirect).catch(() => router.push('/'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const target =
|
const target = { name: ROLE_HOME_ROUTES[auth.role] || 'student-dashboard' }
|
||||||
auth.role === 'admin' ? { name: 'admin-dashboard' } : { name: 'student-dashboard' }
|
|
||||||
router.push(target).catch(() => router.push('/'))
|
router.push(target).catch(() => router.push('/'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ const { isModal, openModal } = useModal()
|
|||||||
const { data: registerData } = useGetRegisterDataQuery()
|
const { data: registerData } = useGetRegisterDataQuery()
|
||||||
watch(
|
watch(
|
||||||
registerData,
|
registerData,
|
||||||
(list) => {
|
(value) => {
|
||||||
if (Array.isArray(list) && list.length > 0) store.hydrateFromKeyValueList(list)
|
if (value && Object.keys(value).length > 0) store.hydrateFromRegisterData(value)
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const personalInformationSchema = object().shape({
|
|||||||
avatar: mixed().notRequired(),
|
avatar: mixed().notRequired(),
|
||||||
avatarId: mixed().notRequired(),
|
avatarId: mixed().notRequired(),
|
||||||
name: requiredString(3),
|
name: requiredString(3),
|
||||||
birthDate: string().required(),
|
birthday: string().required(),
|
||||||
nationalCode: nationalCodeRule,
|
nationalCode: nationalCodeRule,
|
||||||
maritalStatus: string().required(),
|
maritalStatus: string().required(),
|
||||||
gender: string().required(),
|
gender: string().required(),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const emptyState = () => ({
|
|||||||
avatar: null,
|
avatar: null,
|
||||||
avatarId: '',
|
avatarId: '',
|
||||||
name: '',
|
name: '',
|
||||||
birthDate: '',
|
birthday: '',
|
||||||
nationalCode: '',
|
nationalCode: '',
|
||||||
maritalStatus: '',
|
maritalStatus: '',
|
||||||
gender: '',
|
gender: '',
|
||||||
@@ -75,7 +75,7 @@ const SECTION_FIELDS = {
|
|||||||
'avatar',
|
'avatar',
|
||||||
'avatarId',
|
'avatarId',
|
||||||
'name',
|
'name',
|
||||||
'birthDate',
|
'birthday',
|
||||||
'nationalCode',
|
'nationalCode',
|
||||||
'maritalStatus',
|
'maritalStatus',
|
||||||
'gender',
|
'gender',
|
||||||
@@ -115,15 +115,16 @@ const FIELD_TO_SECTION = Object.entries(SECTION_FIELDS).reduce((acc, [section, f
|
|||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
export const toKeyValueList = (obj = {}) =>
|
// Register-data travels as a flat `{ field: value }` object. Legacy payloads
|
||||||
Object.entries(obj).map(([key, value]) => ({ key, value }))
|
// were a `[{ key, value }]` list — tolerate both when reading.
|
||||||
|
export const toRegisterDataObject = (input) => {
|
||||||
export const fromKeyValueList = (list = []) => {
|
if (Array.isArray(input)) {
|
||||||
if (!Array.isArray(list)) return {}
|
return input.reduce((acc, item) => {
|
||||||
return list.reduce((acc, item) => {
|
|
||||||
if (item && typeof item === 'object' && 'key' in item) acc[item.key] = item.value
|
if (item && typeof item === 'object' && 'key' in item) acc[item.key] = item.value
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
}
|
||||||
|
return input && typeof input === 'object' ? input : {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const readPersisted = () => {
|
const readPersisted = () => {
|
||||||
@@ -241,10 +242,11 @@ export const useStudentRegistrationStore = defineStore('studentRegistration', ()
|
|||||||
: personal.value.avatarId,
|
: personal.value.avatarId,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Take the flat `{key,value}` list from GET /me/register-data and slot each
|
// Take the flat `{ field: value }` map from GET /me/register-data and slot
|
||||||
// value into the right section ref, leaving anything we don't recognise alone.
|
// each value into the right section ref, leaving anything we don't
|
||||||
const hydrateFromKeyValueList = (list) => {
|
// recognise alone.
|
||||||
const flat = fromKeyValueList(list)
|
const hydrateFromRegisterData = (input) => {
|
||||||
|
const flat = toRegisterDataObject(input)
|
||||||
const buckets = {}
|
const buckets = {}
|
||||||
Object.entries(flat).forEach(([key, value]) => {
|
Object.entries(flat).forEach(([key, value]) => {
|
||||||
const section = FIELD_TO_SECTION[key]
|
const section = FIELD_TO_SECTION[key]
|
||||||
@@ -301,7 +303,7 @@ export const useStudentRegistrationStore = defineStore('studentRegistration', ()
|
|||||||
goToPrevStep,
|
goToPrevStep,
|
||||||
canGoToStep,
|
canGoToStep,
|
||||||
buildCompletePayload,
|
buildCompletePayload,
|
||||||
hydrateFromKeyValueList,
|
hydrateFromRegisterData,
|
||||||
reset,
|
reset,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ export default [
|
|||||||
path: '/counselor/consultations',
|
path: '/counselor/consultations',
|
||||||
name: 'counselor-consultations',
|
name: 'counselor-consultations',
|
||||||
component: () => import('@/features/counselor/pages/CounselorConsultationsPage.vue'),
|
component: () => import('@/features/counselor/pages/CounselorConsultationsPage.vue'),
|
||||||
meta: { layout: 'counselor', role: 'counselor', title: 'مشاوره' },
|
meta: {
|
||||||
|
layout: 'counselor',
|
||||||
|
role: 'counselor',
|
||||||
|
title: 'مرکز مشاوره',
|
||||||
|
subtitle: 'جلسات مشاوره را پیگیری کرده و پاسخگوی نیازهای کاربران باشید.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
<template>
|
||||||
|
<div class="missionary-request" @click="emit('show-details', request)">
|
||||||
|
<div class="missionary-request__heading">
|
||||||
|
<p class="missionary-request__title">{{ request.title }}</p>
|
||||||
|
<p class="missionary-request__subtitle">{{ request.requesterName }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="missionary-request__meta">
|
||||||
|
<div class="missionary-request__pill">
|
||||||
|
<span class="missionary-request__pill-label">تاریخ درخواست :</span>
|
||||||
|
<span class="missionary-request__pill-value">
|
||||||
|
{{ formatJalaaliDate(request.requestedDate) || '—' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="missionary-request__pill">
|
||||||
|
<span class="missionary-request__pill-label">مکان :</span>
|
||||||
|
<span class="missionary-request__pill-value missionary-request__pill-value--fa">
|
||||||
|
{{ request.location || '—' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Badge :variant="statusVariant" :dot="true" :value="statusLabel" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="missionary-request__actions" @click.stop>
|
||||||
|
<CircleButton
|
||||||
|
v-if="request.status !== 'accepted'"
|
||||||
|
tooltip="پذیرش درخواست"
|
||||||
|
bg-color="rgba(0, 154, 18, 0.08)"
|
||||||
|
size="2.5rem"
|
||||||
|
@click="emit('change-status', { id: request.id, status: 'accepted' })"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<SvgIcon name="check" :size="20" color="var(--color-secondary)" />
|
||||||
|
</template>
|
||||||
|
</CircleButton>
|
||||||
|
<CircleButton
|
||||||
|
v-if="request.status !== 'rejected'"
|
||||||
|
tooltip="رد درخواست"
|
||||||
|
bg-color="rgba(204, 40, 49, 0.08)"
|
||||||
|
size="2.5rem"
|
||||||
|
@click="emit('change-status', { id: request.id, status: 'rejected' })"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<SvgIcon name="close" :size="20" color="var(--color-error)" />
|
||||||
|
</template>
|
||||||
|
</CircleButton>
|
||||||
|
<BaseButton
|
||||||
|
text="جزئیات درخواست"
|
||||||
|
custom-class="missionary-request__details-btn"
|
||||||
|
@click="emit('show-details', request)"
|
||||||
|
>
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="caret-left" :size="16" color="#fff" />
|
||||||
|
</template>
|
||||||
|
</BaseButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import Badge from '@/components/Badge.vue'
|
||||||
|
import { MISSIONARY_REQUEST_STATUS } from '@/enums'
|
||||||
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import { formatJalaaliDate } from '@/utils/date-utils'
|
||||||
|
import CircleButton from '@/components/CircleButton.vue'
|
||||||
|
|
||||||
|
const STATUS_VARIANTS = {
|
||||||
|
pending: 'warning',
|
||||||
|
seen: 'info',
|
||||||
|
accepted: 'success',
|
||||||
|
rejected: 'danger',
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
request: { type: Object, required: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['show-details', 'change-status'])
|
||||||
|
|
||||||
|
const statusLabel = computed(() => MISSIONARY_REQUEST_STATUS[props.request.status] || '—')
|
||||||
|
const statusVariant = computed(() => STATUS_VARIANTS[props.request.status] || 'neutral')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.missionary-request {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: rgba(255, 255, 255, 58%);
|
||||||
|
box-shadow: 0 6.75px 19.425px rgba(0, 0, 0, 4%);
|
||||||
|
border-radius: 0.875rem;
|
||||||
|
margin-bottom: 0.625rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 75%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__heading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
flex: 1 1 33%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #4b4b4b;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__subtitle {
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #848484;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
flex: 1 1 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
background: rgba(107, 107, 107, 5%);
|
||||||
|
padding: 0.25rem 1rem;
|
||||||
|
border-radius: 0.875rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pill-label {
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #848484;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pill-value {
|
||||||
|
font-family: var(--font-family-en);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #5d5d5d;
|
||||||
|
|
||||||
|
&--fa {
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__details-btn {
|
||||||
|
min-width: 9rem;
|
||||||
|
padding: 0 0.625rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<template>
|
||||||
|
<BasicModal
|
||||||
|
title="جزئیات درخواست"
|
||||||
|
title-en="Request Details"
|
||||||
|
width="95%"
|
||||||
|
max-width="50rem"
|
||||||
|
min-width="auto"
|
||||||
|
:show-close-button="true"
|
||||||
|
>
|
||||||
|
<template #default="{ close }">
|
||||||
|
<div class="request-details">
|
||||||
|
<SkeletonLoaderBlock v-if="isLoading" :rows="3" :cols-per-row="1" />
|
||||||
|
|
||||||
|
<template v-else-if="request">
|
||||||
|
<div class="request-details__status">
|
||||||
|
<Badge :variant="statusVariant" :dot="true" :value="statusLabel" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="request-details__grid">
|
||||||
|
<LineInfoBlock title="عنوان درخواست" :desc="request.title || '—'" />
|
||||||
|
<LineInfoBlock title="نام درخواست کننده" :desc="request.requesterName || '—'" />
|
||||||
|
<LineInfoBlock title="شماره تماس" :numeric-desc="request.requesterPhone || '—'" />
|
||||||
|
<LineInfoBlock title="ایمیل" :numeric-desc="request.requesterEmail || '—'" />
|
||||||
|
<LineInfoBlock title="مکان" :desc="request.location || '—'" />
|
||||||
|
<LineInfoBlock
|
||||||
|
title="تاریخ درخواست"
|
||||||
|
:numeric-desc="formatJalaaliDate(request.requestedDate) || '—'"
|
||||||
|
/>
|
||||||
|
<LineInfoBlock
|
||||||
|
title="تاریخ ثبت"
|
||||||
|
:numeric-desc="formatJalaaliDateTime(request.createdAt) || '—'"
|
||||||
|
/>
|
||||||
|
<LineInfoBlock title="منبع درخواست" :desc="request.externalSource || '—'" />
|
||||||
|
<div class="request-details__cell--full">
|
||||||
|
<LineInfoBlock title="توضیحات" :desc="request.description || '—'" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<NoItems v-else title="یافت نشد" desc="اطلاعات این درخواست در دسترس نیست." />
|
||||||
|
|
||||||
|
<div class="request-details__actions">
|
||||||
|
<BaseButton text="بسیار خب" custom-class="request-details__btn" @click="close">
|
||||||
|
<template #appendIcon>
|
||||||
|
<SvgIcon name="arrow-left" :size="18" color="#fff" />
|
||||||
|
</template>
|
||||||
|
</BaseButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</BasicModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import Badge from '@/components/Badge.vue'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
|
import { MISSIONARY_REQUEST_STATUS } from '@/enums'
|
||||||
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
|
import BaseButton from '@/components/BaseButton.vue'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import NoItems from '@/components/blocks/NoItems.vue'
|
||||||
|
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
|
||||||
|
import { formatJalaaliDate, formatJalaaliDateTime } from '@/utils/date-utils'
|
||||||
|
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||||
|
import { useMissionaryRequestQuery } from '@/services/query/missionary-requests'
|
||||||
|
|
||||||
|
defineOptions({ name: 'MissionaryRequestDetailsModal' })
|
||||||
|
|
||||||
|
const STATUS_VARIANTS = {
|
||||||
|
pending: 'warning',
|
||||||
|
seen: 'info',
|
||||||
|
accepted: 'success',
|
||||||
|
rejected: 'danger',
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getModal } = useModal()
|
||||||
|
|
||||||
|
const modalData = computed(() => getModal('MissionaryRequestDetailsModal')?.data ?? {})
|
||||||
|
const requestId = computed(() => modalData.value.id ?? null)
|
||||||
|
|
||||||
|
const { data: request, isLoading } = useMissionaryRequestQuery(requestId, {
|
||||||
|
enabled: () => !!requestId.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusLabel = computed(() => MISSIONARY_REQUEST_STATUS[request.value?.status] || '—')
|
||||||
|
const statusVariant = computed(() => STATUS_VARIANTS[request.value?.status] || 'neutral')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.request-details {
|
||||||
|
width: 100%;
|
||||||
|
text-align: start;
|
||||||
|
|
||||||
|
&__status {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.25rem;
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__cell--full {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__btn {
|
||||||
|
min-width: 12rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as MissionaryRequestDetailsModal } from './components/modals/MissionaryRequestDetailsModal.vue'
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<template>
|
||||||
|
<div class="missionary-requests-page">
|
||||||
|
<BoxedIconTitleBlock
|
||||||
|
class="missionary-requests-page__heading"
|
||||||
|
title="لیست درخواست ها"
|
||||||
|
desc="در این قسمت میتوانید درخواستهای اعزام خود را مدیریت کنید"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<SvgIcon name="user-sound" :size="24" color="var(--color-primary)" />
|
||||||
|
</template>
|
||||||
|
</BoxedIconTitleBlock>
|
||||||
|
|
||||||
|
<SimpleTitleIconBlock title="لیست همه درخواستها" class="missionary-requests-page__list-title">
|
||||||
|
<template #header-icon>
|
||||||
|
<SvgIcon name="list-bullets" :size="16" color="#bcbcbc" />
|
||||||
|
</template>
|
||||||
|
</SimpleTitleIconBlock>
|
||||||
|
|
||||||
|
<SkeletonLoaderBlock v-if="isLoading" :rows="6" :cols-per-row="1" />
|
||||||
|
<div v-else-if="requests.length > 0">
|
||||||
|
<MissionaryRequestItem
|
||||||
|
v-for="request in requests"
|
||||||
|
:key="request.id"
|
||||||
|
:request="request"
|
||||||
|
@show-details="onShowDetails"
|
||||||
|
@change-status="onAskChangeStatus"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<NoItems v-else title="متاسفیم" desc="درخواستی برای نمایش وجود ندارد." />
|
||||||
|
|
||||||
|
<MissionaryRequestDetailsModal v-if="isModal('MissionaryRequestDetailsModal')" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import useModal from '@/composables/useModal'
|
||||||
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import NoItems from '@/components/blocks/NoItems.vue'
|
||||||
|
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
|
||||||
|
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||||
|
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
|
||||||
|
import MissionaryRequestItem from '@/features/missionary/components/MissionaryRequestItem.vue'
|
||||||
|
import MissionaryRequestDetailsModal from '@/features/missionary/components/modals/MissionaryRequestDetailsModal.vue'
|
||||||
|
import {
|
||||||
|
missionaryRequestsKeys,
|
||||||
|
useChangeMissionaryRequestStatusMutation,
|
||||||
|
useMissionaryRequestsListQuery,
|
||||||
|
} from '@/services/query/missionary-requests'
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const { openModal, isModal } = useModal()
|
||||||
|
|
||||||
|
const { data, isLoading } = useMissionaryRequestsListQuery()
|
||||||
|
|
||||||
|
const requests = computed(() => data.value ?? [])
|
||||||
|
|
||||||
|
const invalidate = () =>
|
||||||
|
queryClient.invalidateQueries({ queryKey: missionaryRequestsKeys.all, refetchType: 'all' })
|
||||||
|
|
||||||
|
const statusMutation = useChangeMissionaryRequestStatusMutation()
|
||||||
|
|
||||||
|
const changeStatus = (id, status) =>
|
||||||
|
statusMutation.mutate({ id, payload: { status } }, { onSuccess: invalidate })
|
||||||
|
|
||||||
|
const onShowDetails = (request) => {
|
||||||
|
openModal('MissionaryRequestDetailsModal', { id: request.id })
|
||||||
|
if (request.status === 'pending') changeStatus(request.id, 'seen')
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATUS_CONFIRMS = {
|
||||||
|
accepted: { title: 'پذیرش درخواست', verb: 'پذیرش' },
|
||||||
|
rejected: { title: 'رد درخواست', verb: 'رد' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const onAskChangeStatus = ({ id, status }) => {
|
||||||
|
const config = STATUS_CONFIRMS[status]
|
||||||
|
if (!config) return
|
||||||
|
const request = requests.value.find((r) => r.id === id)
|
||||||
|
openModal('ConfirmModal', {
|
||||||
|
title: config.title,
|
||||||
|
message: `آیا از ${config.verb} درخواست <strong>${request?.title || ''}</strong> مطمئن هستید؟`,
|
||||||
|
onConfirm: () => changeStatus(id, status),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.missionary-requests-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
&__heading {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list-title {
|
||||||
|
margin-bottom: 0.375rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export default [
|
||||||
|
{
|
||||||
|
path: '/missionary-panel/requests',
|
||||||
|
name: 'missionary-requests',
|
||||||
|
component: () => import('@/features/missionary/pages/MissionaryRequestsPage.vue'),
|
||||||
|
meta: {
|
||||||
|
layout: 'missionary',
|
||||||
|
role: 'missionary',
|
||||||
|
title: 'سیستم مدیریت اعزام',
|
||||||
|
subtitle: 'سادهترین راه برای برنامهریزی و پایش مأموریتها',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/missionary-panel/tickets',
|
||||||
|
name: 'missionary-tickets',
|
||||||
|
component: () => import('@/features/student/tickets/pages/StudentTicketsPage.vue'),
|
||||||
|
meta: {
|
||||||
|
layout: 'missionary',
|
||||||
|
role: 'missionary',
|
||||||
|
title: 'تیکتهای شما',
|
||||||
|
subtitle: 'اینجا پل ارتباطی شما با پشتیبانی است.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -3,90 +3,165 @@ export default [
|
|||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
name: 'student-dashboard',
|
name: 'student-dashboard',
|
||||||
component: () => import('@/features/student/pages/StudentDashboardPage.vue'),
|
component: () => import('@/features/student/pages/StudentDashboardPage.vue'),
|
||||||
meta: { layout: 'student', role: 'student', title: 'پیشخوان' },
|
meta: {
|
||||||
|
layout: 'student',
|
||||||
|
role: 'student',
|
||||||
|
title: 'سلام بر شما',
|
||||||
|
subtitle: 'به سامانه موسسه بانو خوش آمدید.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/profile',
|
path: '/profile',
|
||||||
name: 'student-profile',
|
name: 'student-profile',
|
||||||
component: () => import('@/features/student/profile/pages/EditProfilePage.vue'),
|
component: () => import('@/features/student/profile/pages/EditProfilePage.vue'),
|
||||||
meta: { layout: 'student', role: 'student', title: 'ویرایش پروفایل' },
|
meta: {
|
||||||
|
layout: 'student',
|
||||||
|
role: 'student',
|
||||||
|
title: 'پروفایل شما',
|
||||||
|
subtitle: 'مشخصاتتان را ویرایش نمایید.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/my-courses',
|
path: '/my-courses',
|
||||||
name: 'student-courses',
|
name: 'student-courses',
|
||||||
component: () => import('@/features/student/courses/pages/StudentCoursesPage.vue'),
|
component: () => import('@/features/student/courses/pages/StudentCoursesPage.vue'),
|
||||||
meta: { layout: 'student', role: 'student', title: 'دورههای من' },
|
meta: {
|
||||||
|
layout: 'student',
|
||||||
|
role: 'student',
|
||||||
|
title: 'یادگیری شما',
|
||||||
|
subtitle: 'مهارتها و دورههای خود را دنبال کنید.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/my-courses/:id',
|
path: '/my-courses/:id',
|
||||||
name: 'student-course-details',
|
name: 'student-course-details',
|
||||||
component: () => import('@/features/student/courses/pages/StudentCourseDetailsPage.vue'),
|
component: () => import('@/features/student/courses/pages/StudentCourseDetailsPage.vue'),
|
||||||
meta: { layout: 'student', role: 'student', title: 'جزئیات دوره' },
|
meta: {
|
||||||
|
layout: 'student',
|
||||||
|
role: 'student',
|
||||||
|
title: 'یادگیری شما',
|
||||||
|
subtitle: 'مهارتها و دورههای خود را دنبال کنید.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/my-terms',
|
path: '/my-terms',
|
||||||
name: 'student-terms',
|
name: 'student-terms',
|
||||||
component: () => import('@/features/student/terms/pages/StudentTermsPage.vue'),
|
component: () => import('@/features/student/terms/pages/StudentTermsPage.vue'),
|
||||||
meta: { layout: 'student', role: 'student', title: 'ترمهای آموزشی' },
|
meta: {
|
||||||
|
layout: 'student',
|
||||||
|
role: 'student',
|
||||||
|
title: 'یادگیری شما',
|
||||||
|
subtitle: 'مهارتها و دورههای خود را دنبال کنید.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/my-terms/:id',
|
path: '/my-terms/:id',
|
||||||
name: 'student-term-details',
|
name: 'student-term-details',
|
||||||
component: () => import('@/features/student/terms/pages/StudentTermDetailsPage.vue'),
|
component: () => import('@/features/student/terms/pages/StudentTermDetailsPage.vue'),
|
||||||
meta: { layout: 'student', role: 'student', title: 'جزئیات دوره' },
|
meta: {
|
||||||
|
layout: 'student',
|
||||||
|
role: 'student',
|
||||||
|
title: 'یادگیری شما',
|
||||||
|
subtitle: 'مهارتها و دورههای خود را دنبال کنید.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/my-lessons/:id',
|
path: '/my-lessons/:id',
|
||||||
name: 'student-lesson-details',
|
name: 'student-lesson-details',
|
||||||
component: () => import('@/features/student/lessons/pages/StudentLessonDetailsPage.vue'),
|
component: () => import('@/features/student/lessons/pages/StudentLessonDetailsPage.vue'),
|
||||||
meta: { layout: 'student', role: 'student', title: 'جزئیات درس' },
|
meta: {
|
||||||
|
layout: 'student',
|
||||||
|
role: 'student',
|
||||||
|
title: 'یادگیری شما',
|
||||||
|
subtitle: 'مهارتها و دورههای خود را دنبال کنید.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/my-sessions/:id',
|
path: '/my-sessions/:id',
|
||||||
name: 'student-session-details',
|
name: 'student-session-details',
|
||||||
component: () => import('@/features/student/sessions/pages/StudentSessionDetailsPage.vue'),
|
component: () => import('@/features/student/sessions/pages/StudentSessionDetailsPage.vue'),
|
||||||
meta: { layout: 'student', role: 'student', title: 'جزئیات جلسه' },
|
meta: {
|
||||||
|
layout: 'student',
|
||||||
|
role: 'student',
|
||||||
|
title: 'یادگیری شما',
|
||||||
|
subtitle: 'مهارتها و دورههای خود را دنبال کنید.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/my-exams/:id',
|
path: '/my-exams/:id',
|
||||||
name: 'student-exam',
|
name: 'student-exam',
|
||||||
component: () => import('@/features/student/exams/pages/StudentExamPage.vue'),
|
component: () => import('@/features/student/exams/pages/StudentExamPage.vue'),
|
||||||
meta: { layout: 'student', role: 'student', title: 'آزمون' },
|
meta: {
|
||||||
|
layout: 'student',
|
||||||
|
role: 'student',
|
||||||
|
title: 'یادگیری شما',
|
||||||
|
subtitle: 'مهارتها و دورههای خود را دنبال کنید.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/my-exams/:id/take',
|
path: '/my-exams/:id/take',
|
||||||
name: 'student-take-exam',
|
name: 'student-take-exam',
|
||||||
component: () => import('@/features/student/exams/pages/StudentTakeExamPage.vue'),
|
component: () => import('@/features/student/exams/pages/StudentTakeExamPage.vue'),
|
||||||
meta: { layout: 'student', role: 'student', title: 'شروع آزمون' },
|
meta: {
|
||||||
|
layout: 'student',
|
||||||
|
role: 'student',
|
||||||
|
title: 'یادگیری شما',
|
||||||
|
subtitle: 'مهارتها و دورههای خود را دنبال کنید.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/missionary',
|
path: '/missionary',
|
||||||
name: 'student-missionary',
|
name: 'student-missionary',
|
||||||
component: () => import('@/features/student/missionary/pages/StudentMissionaryPage.vue'),
|
component: () => import('@/features/student/missionary/pages/StudentMissionaryPage.vue'),
|
||||||
meta: { layout: 'student', role: 'student', title: 'سامانه اعزام' },
|
meta: {
|
||||||
|
layout: 'student',
|
||||||
|
role: 'student',
|
||||||
|
title: 'سیستم مدیریت اعزام',
|
||||||
|
subtitle: 'سادهترین راه برای برنامهریزی و پایش مأموریتها',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/user/services',
|
path: '/user/services',
|
||||||
name: 'student-services',
|
name: 'student-services',
|
||||||
component: () => import('@/features/student/services/pages/StudentServicesPage.vue'),
|
component: () => import('@/features/student/services/pages/StudentServicesPage.vue'),
|
||||||
meta: { layout: 'student', role: 'student', title: 'خدمات' },
|
meta: {
|
||||||
|
layout: 'student',
|
||||||
|
role: 'student',
|
||||||
|
title: 'درخواست خدمت',
|
||||||
|
subtitle: 'فقط یک مرحله تا شروع کار شما.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/user/consultants',
|
path: '/user/consultants',
|
||||||
name: 'student-consultants',
|
name: 'student-consultants',
|
||||||
component: () => import('@/features/student/consultants/pages/StudentConsultantsPage.vue'),
|
component: () => import('@/features/student/consultants/pages/StudentConsultantsPage.vue'),
|
||||||
meta: { layout: 'student', role: 'student', title: 'مشاوره' },
|
meta: {
|
||||||
|
layout: 'student',
|
||||||
|
role: 'student',
|
||||||
|
title: 'مشاوره سریع',
|
||||||
|
subtitle: 'چند ثانیه برای دریافت مشاوره کافیست.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/user/certificates',
|
path: '/user/certificates',
|
||||||
name: 'student-certificates',
|
name: 'student-certificates',
|
||||||
component: () => import('@/features/student/certificates/pages/StudentCertificatesPage.vue'),
|
component: () => import('@/features/student/certificates/pages/StudentCertificatesPage.vue'),
|
||||||
meta: { layout: 'student', role: 'student', title: 'گواهینامه ها' },
|
meta: {
|
||||||
|
layout: 'student',
|
||||||
|
role: 'student',
|
||||||
|
title: 'گواهینامه معتبر',
|
||||||
|
subtitle: 'نشان اعتماد و تخصص ما در هر حوزه',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/my-tickets',
|
path: '/my-tickets',
|
||||||
name: 'student-tickets',
|
name: 'student-tickets',
|
||||||
component: () => import('@/features/student/tickets/pages/StudentTicketsPage.vue'),
|
component: () => import('@/features/student/tickets/pages/StudentTicketsPage.vue'),
|
||||||
meta: { layout: 'student', role: 'student', title: 'تیکتهای من' },
|
meta: {
|
||||||
|
layout: 'student',
|
||||||
|
role: 'student',
|
||||||
|
title: 'تیکتهای شما',
|
||||||
|
subtitle: 'اینجا پل ارتباطی شما با پشتیبانی است.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -14,7 +14,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SkeletonLoaderBlock v-if="isLoading && !ticket" :rows="3" :cols-per-row="1" />
|
<SkeletonLoaderBlock v-if="isLoading && !ticket" :rows="3" :cols-per-row="1" />
|
||||||
<div v-else-if="messages.length > 0" ref="thread" class="ticket-details__thread">
|
<div
|
||||||
|
v-else-if="messages.length > 0 || ticketMedia.length > 0"
|
||||||
|
ref="thread"
|
||||||
|
class="ticket-details__thread"
|
||||||
|
>
|
||||||
|
<div v-if="ticketMedia.length > 0" class="ticket-details__row ticket-details__row--self">
|
||||||
|
<div class="ticket-details__bubble">
|
||||||
|
<MessageAttachments :media="ticketMedia" />
|
||||||
|
<span class="ticket-details__time">{{ messageTime(ticket) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="message in messages"
|
v-for="message in messages"
|
||||||
:key="message.id"
|
:key="message.id"
|
||||||
@@ -22,6 +32,7 @@
|
|||||||
:class="`ticket-details__row--${senderClass(message)}`"
|
:class="`ticket-details__row--${senderClass(message)}`"
|
||||||
>
|
>
|
||||||
<div class="ticket-details__bubble">
|
<div class="ticket-details__bubble">
|
||||||
|
<MessageAttachments v-if="message.media?.length" :media="message.media" />
|
||||||
<p class="ticket-details__text">{{ message.message }}</p>
|
<p class="ticket-details__text">{{ message.message }}</p>
|
||||||
<span class="ticket-details__time">{{ messageTime(message) }}</span>
|
<span class="ticket-details__time">{{ messageTime(message) }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,6 +71,7 @@ import BasicModal from '@/components/BasicModal.vue'
|
|||||||
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 { formatJalaaliDate } from '@/utils/date-utils'
|
import { formatJalaaliDate } from '@/utils/date-utils'
|
||||||
|
import MessageAttachments from '@/components/blocks/MessageAttachments.vue'
|
||||||
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||||
import {
|
import {
|
||||||
studentTicketsKeys,
|
studentTicketsKeys,
|
||||||
@@ -81,6 +93,8 @@ const { data: ticket, isLoading } = useStudentTicketQuery(ticketId, {
|
|||||||
|
|
||||||
const messages = computed(() => ticket.value?.messages ?? [])
|
const messages = computed(() => ticket.value?.messages ?? [])
|
||||||
|
|
||||||
|
const ticketMedia = computed(() => ticket.value?.media ?? [])
|
||||||
|
|
||||||
const ticketDate = computed(() => formatJalaaliDate(ticket.value?.createdAt) || '—')
|
const ticketDate = computed(() => formatJalaaliDate(ticket.value?.createdAt) || '—')
|
||||||
|
|
||||||
// Student viewing their own ticket — their own messages get the highlighted
|
// Student viewing their own ticket — their own messages get the highlighted
|
||||||
|
|||||||
@@ -6,7 +6,11 @@
|
|||||||
|
|
||||||
<div class="admin-layout__content">
|
<div class="admin-layout__content">
|
||||||
<div class="admin-layout__inner">
|
<div class="admin-layout__inner">
|
||||||
<HeaderBlock :page-title="pageTitle" class="admin-layout__header" />
|
<HeaderBlock
|
||||||
|
:page-title="pageTitle"
|
||||||
|
:page-subtitle="pageSubtitle"
|
||||||
|
class="admin-layout__header"
|
||||||
|
/>
|
||||||
<div class="admin-layout__page">
|
<div class="admin-layout__page">
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</div>
|
</div>
|
||||||
@@ -53,6 +57,7 @@ onMounted(() => window.addEventListener('resize', onResize))
|
|||||||
onBeforeUnmount(() => window.removeEventListener('resize', onResize))
|
onBeforeUnmount(() => window.removeEventListener('resize', onResize))
|
||||||
|
|
||||||
const pageTitle = computed(() => route.meta?.title || 'پیشخوان')
|
const pageTitle = computed(() => route.meta?.title || 'پیشخوان')
|
||||||
|
const pageSubtitle = computed(() => route.meta?.subtitle || '')
|
||||||
|
|
||||||
const educationRoutes = new Set([
|
const educationRoutes = new Set([
|
||||||
'admin-terms',
|
'admin-terms',
|
||||||
|
|||||||
@@ -6,7 +6,11 @@
|
|||||||
|
|
||||||
<div class="counselor-layout__content">
|
<div class="counselor-layout__content">
|
||||||
<div class="counselor-layout__inner">
|
<div class="counselor-layout__inner">
|
||||||
<HeaderBlock :page-title="pageTitle" class="counselor-layout__header" />
|
<HeaderBlock
|
||||||
|
:page-title="pageTitle"
|
||||||
|
:page-subtitle="pageSubtitle"
|
||||||
|
class="counselor-layout__header"
|
||||||
|
/>
|
||||||
<div class="counselor-layout__page">
|
<div class="counselor-layout__page">
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</div>
|
</div>
|
||||||
@@ -53,6 +57,7 @@ onMounted(() => window.addEventListener('resize', onResize))
|
|||||||
onBeforeUnmount(() => window.removeEventListener('resize', onResize))
|
onBeforeUnmount(() => window.removeEventListener('resize', onResize))
|
||||||
|
|
||||||
const pageTitle = computed(() => route.meta?.title || 'مشاوره')
|
const pageTitle = computed(() => route.meta?.title || 'مشاوره')
|
||||||
|
const pageSubtitle = computed(() => route.meta?.subtitle || '')
|
||||||
|
|
||||||
const menuItems = computed(() => [
|
const menuItems = computed(() => [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<template>
|
||||||
|
<div class="missionary-layout">
|
||||||
|
<div class="missionary-layout__row">
|
||||||
|
<Transition name="fade">
|
||||||
|
<div
|
||||||
|
v-if="isMobile && ui.isSidebarOpen"
|
||||||
|
class="missionary-layout__backdrop"
|
||||||
|
@click="ui.setSidebarState(false)"
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<Transition :name="isMobile ? 'slide-drawer' : ''">
|
||||||
|
<aside
|
||||||
|
v-if="(isMobile && ui.isSidebarOpen) || !isMobile"
|
||||||
|
class="missionary-layout__sidebar"
|
||||||
|
:class="{ 'missionary-layout__sidebar--mobile': isMobile }"
|
||||||
|
>
|
||||||
|
<StudentSidebar :menu-items="menuItems" />
|
||||||
|
</aside>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
<div class="missionary-layout__content">
|
||||||
|
<div class="missionary-layout__inner">
|
||||||
|
<HeaderBlock
|
||||||
|
:page-title="pageTitle"
|
||||||
|
:page-subtitle="pageSubtitle"
|
||||||
|
class="missionary-layout__header"
|
||||||
|
/>
|
||||||
|
<RouterView />
|
||||||
|
<div class="missionary-layout__footer">
|
||||||
|
<Copyright />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useUIStore } from '@/store/ui'
|
||||||
|
import { RouterView, useRoute } from 'vue-router'
|
||||||
|
import Copyright from '@/components/Copyright.vue'
|
||||||
|
import HeaderBlock from '@/components/HeaderBlock.vue'
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
import StudentSidebar from '@/layouts/sidebars/StudentSidebar.vue'
|
||||||
|
|
||||||
|
const ui = useUIStore()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const isMobile = ref(window.innerWidth < 1024)
|
||||||
|
const onResize = () => {
|
||||||
|
isMobile.value = window.innerWidth < 1024
|
||||||
|
}
|
||||||
|
onMounted(() => window.addEventListener('resize', onResize))
|
||||||
|
onBeforeUnmount(() => window.removeEventListener('resize', onResize))
|
||||||
|
|
||||||
|
const pageTitle = computed(() => route.meta?.title || 'سیستم مدیریت اعزام')
|
||||||
|
const pageSubtitle = computed(() => route.meta?.subtitle || '')
|
||||||
|
|
||||||
|
const menuItems = computed(() => [
|
||||||
|
{
|
||||||
|
title: 'لیست درخواست ها',
|
||||||
|
icon: 'user-sound',
|
||||||
|
to: { name: 'missionary-requests' },
|
||||||
|
active: route.name === 'missionary-requests',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'تیکت',
|
||||||
|
icon: 'envelope-open',
|
||||||
|
to: { name: 'missionary-tickets' },
|
||||||
|
active: route.name === 'missionary-tickets',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.missionary-layout {
|
||||||
|
min-height: 100dvh;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&__row {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 30;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 100dvh;
|
||||||
|
width: 70%;
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
position: static;
|
||||||
|
width: auto;
|
||||||
|
flex-basis: 16.6667%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--mobile {
|
||||||
|
width: 70%;
|
||||||
|
max-width: 20rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 20;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
flex-basis: 83.3333%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
flex-basis: 93%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100dvh;
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
margin-top: auto;
|
||||||
|
padding: 0.5rem 0 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -21,7 +21,11 @@
|
|||||||
|
|
||||||
<div class="student-layout__content">
|
<div class="student-layout__content">
|
||||||
<div class="student-layout__inner">
|
<div class="student-layout__inner">
|
||||||
<HeaderBlock :page-title="pageTitle" class="student-layout__header" />
|
<HeaderBlock
|
||||||
|
:page-title="pageTitle"
|
||||||
|
:page-subtitle="pageSubtitle"
|
||||||
|
class="student-layout__header"
|
||||||
|
/>
|
||||||
<RouterView />
|
<RouterView />
|
||||||
<div class="student-layout__footer">
|
<div class="student-layout__footer">
|
||||||
<Copyright />
|
<Copyright />
|
||||||
@@ -51,8 +55,15 @@ onMounted(() => window.addEventListener('resize', onResize))
|
|||||||
onBeforeUnmount(() => window.removeEventListener('resize', onResize))
|
onBeforeUnmount(() => window.removeEventListener('resize', onResize))
|
||||||
|
|
||||||
const pageTitle = computed(() => route.meta?.title || 'پیشخوان')
|
const pageTitle = computed(() => route.meta?.title || 'پیشخوان')
|
||||||
|
const pageSubtitle = computed(() => route.meta?.subtitle || '')
|
||||||
|
|
||||||
const menuItems = computed(() => [
|
const menuItems = computed(() => [
|
||||||
|
{
|
||||||
|
title: 'داشبورد',
|
||||||
|
icon: 'monitor',
|
||||||
|
to: { name: 'student-dashboard' },
|
||||||
|
active: route.name === 'student-dashboard',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'آموزش',
|
title: 'آموزش',
|
||||||
icon: 'graduation-cap',
|
icon: 'graduation-cap',
|
||||||
@@ -65,7 +76,6 @@ const menuItems = computed(() => [
|
|||||||
'student-exam',
|
'student-exam',
|
||||||
'student-courses',
|
'student-courses',
|
||||||
'student-course-details',
|
'student-course-details',
|
||||||
'student-dashboard',
|
|
||||||
].includes(route.name),
|
].includes(route.name),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
<SidebarLogout />
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
import { RouterLink } from 'vue-router'
|
import { RouterLink } from 'vue-router'
|
||||||
import { gallery } from '@/utils/gallery'
|
import { gallery } from '@/utils/gallery'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import SidebarLogout from '@/layouts/sidebars/SidebarLogout.vue'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
menuItems: { type: Array, default: () => [] },
|
menuItems: { type: Array, default: () => [] },
|
||||||
@@ -46,7 +48,7 @@ const logo = gallery.logoPinkishRed
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.admin-sidebar {
|
.admin-sidebar {
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 1.5rem 1rem;
|
padding: 1.5rem 1rem;
|
||||||
|
|||||||
@@ -18,12 +18,14 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
<SidebarLogout />
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { gallery } from '@/utils/gallery'
|
import { gallery } from '@/utils/gallery'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import SidebarLogout from '@/layouts/sidebars/SidebarLogout.vue'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
menuItems: { type: Array, default: () => [] },
|
menuItems: { type: Array, default: () => [] },
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="sidebar-logout"
|
||||||
|
:class="{ 'sidebar-logout--stacked': stacked }"
|
||||||
|
:style="{ color }"
|
||||||
|
@click="onLogout"
|
||||||
|
>
|
||||||
|
<SvgIcon name="power" :size="34" color="currentColor" />
|
||||||
|
<span class="sidebar-logout__labels">
|
||||||
|
<span class="sidebar-logout__title">خــروج</span>
|
||||||
|
<span class="sidebar-logout__subtitle">Logout</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import useAuth from '@/composables/useAuth'
|
||||||
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import { useLogoutMutation } from '@/services/query/auth'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
color: { type: String, default: '#9a9a9a' },
|
||||||
|
stacked: { type: Boolean, default: false },
|
||||||
|
})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const { clearSession } = useAuth()
|
||||||
|
const logoutMutation = useLogoutMutation()
|
||||||
|
|
||||||
|
const onLogout = async () => {
|
||||||
|
try {
|
||||||
|
await logoutMutation.mutateAsync()
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
} finally {
|
||||||
|
clearSession()
|
||||||
|
router.push({ name: 'login' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.sidebar-logout {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.625rem 0.875rem;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__labels {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--stacked {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.375rem;
|
||||||
|
padding: 0.625rem 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--stacked &__labels {
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-family: var(--font-family-fa);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__subtitle {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -18,12 +18,14 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
<SidebarLogout color="#fff" />
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { gallery } from '@/utils/gallery'
|
import { gallery } from '@/utils/gallery'
|
||||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||||
|
import SidebarLogout from '@/layouts/sidebars/SidebarLogout.vue'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
menuItems: { type: Array, default: () => [] },
|
menuItems: { type: Array, default: () => [] },
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ROLE_HOME_ROUTES } from '@/enums'
|
||||||
import { useAuthStore } from '@/store/auth'
|
import { useAuthStore } from '@/store/auth'
|
||||||
import { tokenService } from '@/services/api/token-service'
|
import { tokenService } from '@/services/api/token-service'
|
||||||
|
|
||||||
@@ -18,11 +19,25 @@ export const guestGuard = (to) => {
|
|||||||
if (!to.meta?.requiresGuest) return
|
if (!to.meta?.requiresGuest) return
|
||||||
if (tokenService.hasToken()) {
|
if (tokenService.hasToken()) {
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
return auth.role === 'admin' ? { name: 'admin-dashboard' } : { name: 'student-dashboard' }
|
return { name: ROLE_HOME_ROUTES[auth.role] || 'student-dashboard' }
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Students whose account is not yet `approved` (pending/verified) must finish
|
||||||
|
// the registration wizard before reaching any authenticated page.
|
||||||
|
export const statusGuard = (to) => {
|
||||||
|
if (isPublic(to)) return
|
||||||
|
if (to.meta?.requiresGuest) return
|
||||||
|
if (to.name === 'student-register') return
|
||||||
|
if (!tokenService.hasToken()) return
|
||||||
|
const auth = useAuthStore()
|
||||||
|
if (!auth.user) return
|
||||||
|
if (auth.role && auth.role !== 'student') return
|
||||||
|
if (auth.user.status === 'approved') return
|
||||||
|
return { name: 'student-register' }
|
||||||
|
}
|
||||||
|
|
||||||
export const roleGuard = (to) => {
|
export const roleGuard = (to) => {
|
||||||
if (!to.meta?.role) return
|
if (!to.meta?.role) return
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
@@ -37,4 +52,4 @@ export const titleGuard = (to) => {
|
|||||||
document.title = title ? `${APP_TITLE} - ${title}` : APP_TITLE
|
document.title = title ? `${APP_TITLE} - ${title}` : APP_TITLE
|
||||||
}
|
}
|
||||||
|
|
||||||
export const guards = [authGuard, guestGuard, roleGuard]
|
export const guards = [authGuard, guestGuard, statusGuard, roleGuard]
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ const router = createRouter({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// router.beforeEach((to, from) => {
|
router.beforeEach((to, from) => {
|
||||||
// for (const guard of guards) {
|
for (const guard of guards) {
|
||||||
// const result = guard(to, from)
|
const result = guard(to, from)
|
||||||
// if (result !== undefined) return result
|
if (result !== undefined) return result
|
||||||
// }
|
}
|
||||||
// return
|
return
|
||||||
// })
|
})
|
||||||
|
|
||||||
router.afterEach((to) => titleGuard(to))
|
router.afterEach((to) => titleGuard(to))
|
||||||
|
|
||||||
|
|||||||
@@ -164,6 +164,10 @@ export const endpoints = {
|
|||||||
showCounselorTicket: '/counselor/tickets/:id',
|
showCounselorTicket: '/counselor/tickets/:id',
|
||||||
sendCounselorTicketMessage: '/counselor/tickets/:id/messages',
|
sendCounselorTicketMessage: '/counselor/tickets/:id/messages',
|
||||||
changeCounselorTicketStatus: '/counselor/tickets/:id/status',
|
changeCounselorTicketStatus: '/counselor/tickets/:id/status',
|
||||||
|
|
||||||
|
listMissionaryRequests: '/missionary/requests',
|
||||||
|
showMissionaryRequest: '/missionary/requests/:id',
|
||||||
|
changeMissionaryRequestStatus: '/missionary/requests/:id/status',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildUrl = (template, params = {}) =>
|
export const buildUrl = (template, params = {}) =>
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { http } from '@/services/api/http'
|
||||||
|
import { buildUrl, endpoints } from '@/services/api/endpoints'
|
||||||
|
|
||||||
|
export const apiListMissionaryRequests = (params) =>
|
||||||
|
http.get(endpoints.listMissionaryRequests, { params })
|
||||||
|
|
||||||
|
export const apiShowMissionaryRequest = (id) =>
|
||||||
|
http.get(buildUrl(endpoints.showMissionaryRequest, { id }))
|
||||||
|
|
||||||
|
export const apiChangeMissionaryRequestStatus = (id, payload) =>
|
||||||
|
http.patch(buildUrl(endpoints.changeMissionaryRequestStatus, { id }), payload)
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
export const missionaryRequests = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
missionaryId: 8,
|
||||||
|
externalSource: 'wordpress',
|
||||||
|
externalReferenceId: 'wp-ticket-12341',
|
||||||
|
requesterName: 'احمد رضایی',
|
||||||
|
requesterPhone: '09123456789',
|
||||||
|
requesterEmail: 'ahmad@example.com',
|
||||||
|
title: 'درخواست جلسه هفتگی روضه خانگی',
|
||||||
|
description: 'به دنبال برگزاری یک جلسه ثابت هفتگی در منزل هستیم.',
|
||||||
|
location: 'تهران، ایران',
|
||||||
|
requestedDate: '2026-05-20',
|
||||||
|
status: 'pending',
|
||||||
|
createdAt: '2026-05-09T10:00:00+00:00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
missionaryId: 8,
|
||||||
|
externalSource: 'wordpress',
|
||||||
|
externalReferenceId: 'wp-ticket-12342',
|
||||||
|
requesterName: 'مریم اکبری',
|
||||||
|
requesterPhone: '09121112233',
|
||||||
|
requesterEmail: 'maryam@example.com',
|
||||||
|
title: 'مراسم مناسبتی دهه محرم',
|
||||||
|
description: 'برای دهه اول محرم نیاز به مبلغ برای جلسات خانگی داریم.',
|
||||||
|
location: 'قم، ایران',
|
||||||
|
requestedDate: '2026-06-25',
|
||||||
|
status: 'seen',
|
||||||
|
createdAt: '2026-05-11T08:30:00+00:00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
missionaryId: 8,
|
||||||
|
externalSource: 'wordpress',
|
||||||
|
externalReferenceId: 'wp-ticket-12343',
|
||||||
|
requesterName: 'فاطمه حسینی',
|
||||||
|
requesterPhone: '09354445566',
|
||||||
|
requesterEmail: 'fatemeh@example.com',
|
||||||
|
title: 'جلسه پرسش و پاسخ اعتقادی دانشگاه',
|
||||||
|
description: 'برگزاری نشست پرسش و پاسخ برای دانشجویان خوابگاه.',
|
||||||
|
location: 'اصفهان، ایران',
|
||||||
|
requestedDate: '2026-06-02',
|
||||||
|
status: 'accepted',
|
||||||
|
createdAt: '2026-05-12T14:15:00+00:00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
missionaryId: 8,
|
||||||
|
externalSource: 'wordpress',
|
||||||
|
externalReferenceId: 'wp-ticket-12344',
|
||||||
|
requesterName: 'حسین مولایی',
|
||||||
|
requesterPhone: '09107778899',
|
||||||
|
requesterEmail: 'hossein@example.com',
|
||||||
|
title: 'کلاس آموزش احکام مدرسه',
|
||||||
|
description: 'برگزاری کلاس هفتگی احکام برای دانشآموزان دوره اول.',
|
||||||
|
location: 'مشهد، ایران',
|
||||||
|
requestedDate: '2026-05-30',
|
||||||
|
status: 'rejected',
|
||||||
|
createdAt: '2026-05-13T09:45:00+00:00',
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { register } from '@/services/mock/registry'
|
||||||
|
import { endpoints } from '@/services/api/endpoints'
|
||||||
|
import { findOrThrow, updateById } from '@/services/mock/helpers'
|
||||||
|
import { missionaryRequests } from '@/services/mock/fixtures/missionary-requests'
|
||||||
|
|
||||||
|
register('GET', endpoints.listMissionaryRequests, () => ({
|
||||||
|
success: true,
|
||||||
|
message: 'OK',
|
||||||
|
data: missionaryRequests,
|
||||||
|
}))
|
||||||
|
|
||||||
|
register('GET', endpoints.showMissionaryRequest, ({ params }) => ({
|
||||||
|
success: true,
|
||||||
|
message: 'OK',
|
||||||
|
data: findOrThrow(missionaryRequests, params.id),
|
||||||
|
}))
|
||||||
|
|
||||||
|
register('PATCH', endpoints.changeMissionaryRequestStatus, ({ params, data }) => ({
|
||||||
|
success: true,
|
||||||
|
message: 'Status updated.',
|
||||||
|
data: updateById(missionaryRequests, params.id, { status: data.status }),
|
||||||
|
}))
|
||||||
@@ -70,16 +70,25 @@ export const useGetRegistrationVideoQuery = (options = {}) =>
|
|||||||
|
|
||||||
export const useUploadMediaMutation = () => useMutation({ mutationFn: apiUploadMedia })
|
export const useUploadMediaMutation = () => useMutation({ mutationFn: apiUploadMedia })
|
||||||
|
|
||||||
|
// Register-data is a flat `{ field: value }` object; legacy responses were a
|
||||||
|
// `[{ key, value }]` list. Normalize both into the object shape.
|
||||||
|
const toRegisterDataObject = (response) => {
|
||||||
|
const body = response?.data ?? response
|
||||||
|
const raw = Array.isArray(body) ? body : body?.data ?? body
|
||||||
|
if (Array.isArray(raw)) {
|
||||||
|
return raw.reduce((acc, item) => {
|
||||||
|
if (item && typeof item === 'object' && 'key' in item) acc[item.key] = item.value
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
return raw && typeof raw === 'object' ? raw : {}
|
||||||
|
}
|
||||||
|
|
||||||
export const useGetRegisterDataQuery = (options = {}) =>
|
export const useGetRegisterDataQuery = (options = {}) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: authKeys.registerData(),
|
queryKey: authKeys.registerData(),
|
||||||
queryFn: () => apiGetRegisterData(),
|
queryFn: () => apiGetRegisterData(),
|
||||||
select: (response) => {
|
select: toRegisterDataObject,
|
||||||
const data = response?.data ?? response
|
|
||||||
if (Array.isArray(data)) return data
|
|
||||||
if (Array.isArray(data?.data)) return data.data
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { useMutation, useQuery } from '@tanstack/vue-query'
|
||||||
|
import {
|
||||||
|
apiChangeMissionaryRequestStatus,
|
||||||
|
apiListMissionaryRequests,
|
||||||
|
apiShowMissionaryRequest,
|
||||||
|
} from '@/services/api/missionary-requests'
|
||||||
|
|
||||||
|
export const missionaryRequestsKeys = {
|
||||||
|
all: ['missionary', 'requests'],
|
||||||
|
list: () => ['missionary', 'requests', 'list'],
|
||||||
|
detail: (id) => ['missionary', 'requests', 'detail', id],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMissionaryRequestsListQuery = (options = {}) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: missionaryRequestsKeys.list(),
|
||||||
|
queryFn: () => apiListMissionaryRequests(),
|
||||||
|
select: (response) => response?.data ?? [],
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useMissionaryRequestQuery = (idRef, options = {}) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: ['missionary', 'requests', 'detail', idRef],
|
||||||
|
queryFn: () => apiShowMissionaryRequest(idRef.value),
|
||||||
|
select: (response) => response?.data ?? response,
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useChangeMissionaryRequestStatusMutation = () =>
|
||||||
|
useMutation({
|
||||||
|
mutationFn: ({ id, payload }) => apiChangeMissionaryRequestStatus(id, payload),
|
||||||
|
})
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import VideoCover from '@/assets/images/cover.png'
|
import VideoCover from '@/assets/images/cover.png'
|
||||||
import logoWhite from '@/assets/images/logo-white.png'
|
import logoWhite from '@/assets/images/logo-white.png'
|
||||||
|
import courseIcon from '@/assets/images/course-icon.png'
|
||||||
import authLogo from '@/assets/images/auth/auth-logo.png'
|
import authLogo from '@/assets/images/auth/auth-logo.png'
|
||||||
|
import sessionIcon from '@/assets/images/session-icon.png'
|
||||||
|
import assignmentIcon from '@/assets/images/assignment-icon.png'
|
||||||
import loginPattern from '@/assets/images/auth/login-pattern.jpg'
|
import loginPattern from '@/assets/images/auth/login-pattern.jpg'
|
||||||
import logoPinkishRed from '@/assets/images/logo-pinkish-red.png'
|
import logoPinkishRed from '@/assets/images/logo-pinkish-red.png'
|
||||||
import registrationCompleteFlower from '@/assets/images/auth/flower.png'
|
import registrationCompleteFlower from '@/assets/images/auth/flower.png'
|
||||||
@@ -15,6 +18,9 @@ export const gallery = {
|
|||||||
loginPatternWithLogo,
|
loginPatternWithLogo,
|
||||||
logoWhite,
|
logoWhite,
|
||||||
logoPinkishRed,
|
logoPinkishRed,
|
||||||
|
courseIcon,
|
||||||
|
sessionIcon,
|
||||||
|
assignmentIcon,
|
||||||
VideoCover,
|
VideoCover,
|
||||||
registrationCompleteFlower,
|
registrationCompleteFlower,
|
||||||
studentSidebarPattern,
|
studentSidebarPattern,
|
||||||
|
|||||||