Compare commits
12 Commits
master
..
216efa7c1f
| Author | SHA1 | Date | |
|---|---|---|---|
| 216efa7c1f | |||
| 274284dc05 | |||
| 0caea18eb8 | |||
| 9b61fff128 | |||
| 55ed80e652 | |||
| bdb77786f2 | |||
| d227b1c8f4 | |||
| fdc52381db | |||
| c0fe9e9918 | |||
| 8743d37a7d | |||
| fc1c249033 | |||
| 9f86acacb0 |
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<Transition name="fade-scale" @after-enter="onOpened" @after-leave="onClosed">
|
<Transition name="fade-scale" @after-enter="onOpened" @after-leave="onClosed">
|
||||||
<div v-if="isOpen" class="basic-modal" :style="{ zIndex }" @click="onBackdropClick">
|
<div v-if="isOpen" class="basic-modal" @click="onBackdropClick">
|
||||||
<div class="basic-modal__container" :style="containerStyle" @click.stop>
|
<div class="basic-modal__container" :style="containerStyle" @click.stop>
|
||||||
<div class="basic-modal__card scrollbar-thin">
|
<div class="basic-modal__card scrollbar-thin">
|
||||||
<header v-if="hasHeader" class="basic-modal__header">
|
<header v-if="hasHeader" class="basic-modal__header">
|
||||||
@@ -43,7 +43,6 @@ const props = defineProps({
|
|||||||
width: { type: String, default: '' },
|
width: { type: String, default: '' },
|
||||||
maxWidth: { type: String, default: '98%' },
|
maxWidth: { type: String, default: '98%' },
|
||||||
minWidth: { type: String, default: '85%' },
|
minWidth: { type: String, default: '85%' },
|
||||||
zIndex: { type: [Number, String], default: 50 },
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['open', 'close'])
|
const emit = defineEmits(['open', 'close'])
|
||||||
@@ -102,6 +101,7 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown))
|
|||||||
.basic-modal {
|
.basic-modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
z-index: 50;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<BasicModal min-width="0" max-width="640px" width="95%" :show-close-button="false" :z-index="60">
|
<BasicModal min-width="0" max-width="640px" width="95%" :show-close-button="false">
|
||||||
<template #default="{ data }">
|
<template #default="{ data }">
|
||||||
<div class="confirm-modal">
|
<div class="confirm-modal">
|
||||||
<h3 class="confirm-modal__title">{{ data?.title || 'تأیید حذف' }}</h3>
|
<h3 class="confirm-modal__title">{{ data?.title || 'تأیید حذف' }}</h3>
|
||||||
|
|||||||
@@ -283,11 +283,8 @@ const addExamMutation = useAddAdminExamMutation()
|
|||||||
const updateExamMutation = useUpdateAdminExamMutation()
|
const updateExamMutation = useUpdateAdminExamMutation()
|
||||||
const addQuestionMutation = useAddAdminExamQuestionMutation()
|
const addQuestionMutation = useAddAdminExamQuestionMutation()
|
||||||
|
|
||||||
const isSubmitting = ref(false)
|
|
||||||
|
|
||||||
const submitting = computed(
|
const submitting = computed(
|
||||||
() =>
|
() =>
|
||||||
isSubmitting.value ||
|
|
||||||
addExamMutation.isPending.value ||
|
addExamMutation.isPending.value ||
|
||||||
updateExamMutation.isPending.value ||
|
updateExamMutation.isPending.value ||
|
||||||
addQuestionMutation.isPending.value
|
addQuestionMutation.isPending.value
|
||||||
@@ -301,29 +298,23 @@ const postQuestionsSequentially = async (id, list) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
if (isSubmitting.value) return
|
const { isValid } = await validate(form.value)
|
||||||
isSubmitting.value = true
|
const newQuestions = validateLocalQuestions()
|
||||||
try {
|
if (!isValid || !newQuestions) return
|
||||||
const { isValid } = await validate(form.value)
|
|
||||||
const newQuestions = validateLocalQuestions()
|
|
||||||
if (!isValid || !newQuestions) return
|
|
||||||
|
|
||||||
const examPayload = buildExamPayload(form.value)
|
const examPayload = buildExamPayload(form.value)
|
||||||
let targetExamId = examId.value
|
let targetExamId = examId.value
|
||||||
if (isEditMode.value) {
|
if (isEditMode.value) {
|
||||||
await updateExamMutation.mutateAsync({ id: targetExamId, payload: examPayload })
|
await updateExamMutation.mutateAsync({ id: targetExamId, payload: examPayload })
|
||||||
} else {
|
} else {
|
||||||
const created = await addExamMutation.mutateAsync(examPayload)
|
const created = await addExamMutation.mutateAsync(examPayload)
|
||||||
targetExamId = created?.data?.id ?? created?.id ?? targetExamId
|
targetExamId = created?.data?.id ?? created?.id ?? targetExamId
|
||||||
}
|
|
||||||
if (targetExamId && newQuestions.length > 0) {
|
|
||||||
await postQuestionsSequentially(targetExamId, newQuestions)
|
|
||||||
}
|
|
||||||
await queryClient.invalidateQueries({ queryKey: adminExamsKeys.all, refetchType: 'all' })
|
|
||||||
router.push({ name: 'admin-exams' })
|
|
||||||
} finally {
|
|
||||||
isSubmitting.value = false
|
|
||||||
}
|
}
|
||||||
|
if (targetExamId && newQuestions.length > 0) {
|
||||||
|
await postQuestionsSequentially(targetExamId, newQuestions)
|
||||||
|
}
|
||||||
|
await queryClient.invalidateQueries({ queryKey: adminExamsKeys.all, refetchType: 'all' })
|
||||||
|
router.push({ name: 'admin-exams' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const onCancel = () => router.push({ name: 'admin-exams' })
|
const onCancel = () => router.push({ name: 'admin-exams' })
|
||||||
|
|||||||
@@ -274,15 +274,4 @@ export default [
|
|||||||
subtitle: 'درخواستهای اعزام مبلغین را مشاهده و مدیریت نمایید.',
|
subtitle: 'درخواستهای اعزام مبلغین را مشاهده و مدیریت نمایید.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/certificates',
|
|
||||||
name: 'admin-certificates',
|
|
||||||
component: () => import('@/features/admin/certificates/pages/AdminCertificatesPage.vue'),
|
|
||||||
meta: {
|
|
||||||
layout: 'admin',
|
|
||||||
role: 'admin',
|
|
||||||
title: 'گواهینامهها',
|
|
||||||
subtitle: 'گواهینامههای دورهها را صادر و مدیریت نمایید.',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -70,18 +70,6 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="term-details__student-actions">
|
|
||||||
<CircleButton
|
|
||||||
tooltip="حذف از ترم"
|
|
||||||
bg-color="rgba(104, 104, 104, 0.05)"
|
|
||||||
size="2.5rem"
|
|
||||||
@click="onAskRemoveStudent(student)"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<SvgIcon name="trash" :size="18" color="var(--color-error)" />
|
|
||||||
</template>
|
|
||||||
</CircleButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NoItems v-else title="متاسفیم" desc="طلبهای برای نمایش وجود ندارد." />
|
<NoItems v-else title="متاسفیم" desc="طلبهای برای نمایش وجود ندارد." />
|
||||||
@@ -142,24 +130,18 @@ import BaseButton from '@/components/BaseButton.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 CircleButton from '@/components/CircleButton.vue'
|
|
||||||
import TabsBlock from '@/components/blocks/TabsBlock.vue'
|
import TabsBlock from '@/components/blocks/TabsBlock.vue'
|
||||||
import { usePagination } from '@/composables/usePagination'
|
import { usePagination } from '@/composables/usePagination'
|
||||||
import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
import LineTitleBlock from '@/components/LineTitleBlock.vue'
|
||||||
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
|
import LineInfoBlock from '@/components/blocks/LineInfoBlock.vue'
|
||||||
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
|
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
|
||||||
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
|
||||||
|
import { useAdminTermEnrolleesQuery, useAdminTermQuery } from '@/services/query/admin-terms'
|
||||||
import {
|
import {
|
||||||
adminCoursesKeys,
|
adminCoursesKeys,
|
||||||
useAdminCoursesListQuery,
|
useAdminCoursesListQuery,
|
||||||
useUpdateAdminCourseMutation,
|
useUpdateAdminCourseMutation,
|
||||||
} from '@/services/query/admin-courses'
|
} from '@/services/query/admin-courses'
|
||||||
import {
|
|
||||||
adminTermsKeys,
|
|
||||||
useAdminTermEnrolleesQuery,
|
|
||||||
useAdminTermQuery,
|
|
||||||
useRemoveAdminTermStudentMutation,
|
|
||||||
} from '@/services/query/admin-terms'
|
|
||||||
|
|
||||||
defineOptions({ name: 'TermDetailsModal' })
|
defineOptions({ name: 'TermDetailsModal' })
|
||||||
|
|
||||||
@@ -205,24 +187,6 @@ const studentPaginationMeta = computed(() => ({
|
|||||||
|
|
||||||
const studentAvatar = (student) => student.avatarUrl || student.avatarDownloadUrl || ''
|
const studentAvatar = (student) => student.avatarUrl || student.avatarDownloadUrl || ''
|
||||||
|
|
||||||
const invalidateStudents = () => queryClient.invalidateQueries({ queryKey: adminTermsKeys.all })
|
|
||||||
|
|
||||||
const removeStudentMutation = useRemoveAdminTermStudentMutation()
|
|
||||||
|
|
||||||
const onAskRemoveStudent = (student) => {
|
|
||||||
openModal('ConfirmModal', {
|
|
||||||
title: `حذف ${student?.user?.name || 'طلبه'}`,
|
|
||||||
message: `بعد از حذف، دسترسی <strong>${
|
|
||||||
student?.user?.name || 'این طلبه'
|
|
||||||
}</strong> به این ترم و محتوای آن قطع میشود. آیا مطمئن هستید؟`,
|
|
||||||
onConfirm: () =>
|
|
||||||
removeStudentMutation.mutate(
|
|
||||||
{ termId: termId.value, userId: student.user.id },
|
|
||||||
{ onSuccess: invalidateStudents }
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const courseFilters = computed(() => ({ termId: termId.value }))
|
const courseFilters = computed(() => ({ termId: termId.value }))
|
||||||
const { pagination: coursePagination, setPage: setCoursePage } = usePagination({
|
const { pagination: coursePagination, setPage: setCoursePage } = usePagination({
|
||||||
page: 1,
|
page: 1,
|
||||||
|
|||||||
@@ -173,12 +173,6 @@ const menuItems = computed(() => [
|
|||||||
to: { name: 'admin-missionary-requests' },
|
to: { name: 'admin-missionary-requests' },
|
||||||
active: route.name === 'admin-missionary-requests',
|
active: route.name === 'admin-missionary-requests',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'گواهینامهها',
|
|
||||||
icon: 'certificate',
|
|
||||||
to: { name: 'admin-certificates' },
|
|
||||||
active: route.name === 'admin-certificates',
|
|
||||||
},
|
|
||||||
])
|
])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export const buildStudentMenuItems = (route) => [
|
|||||||
icon: 'monitor',
|
icon: 'monitor',
|
||||||
to: { name: 'student-dashboard' },
|
to: { name: 'student-dashboard' },
|
||||||
active: route.name === 'student-dashboard',
|
active: route.name === 'student-dashboard',
|
||||||
dedupeKey: 'dashboard',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'آموزش',
|
title: 'آموزش',
|
||||||
@@ -26,7 +25,6 @@ export const buildStudentMenuItems = (route) => [
|
|||||||
'student-courses',
|
'student-courses',
|
||||||
'student-course-details',
|
'student-course-details',
|
||||||
].includes(route.name),
|
].includes(route.name),
|
||||||
dedupeKey: 'education',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'خدمات',
|
title: 'خدمات',
|
||||||
@@ -57,7 +55,6 @@ export const buildStudentMenuItems = (route) => [
|
|||||||
icon: 'envelope-open',
|
icon: 'envelope-open',
|
||||||
to: { name: 'student-tickets' },
|
to: { name: 'student-tickets' },
|
||||||
active: route.name === 'student-tickets',
|
active: route.name === 'student-tickets',
|
||||||
dedupeKey: 'ticket',
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -67,7 +64,6 @@ export const buildMissionaryMenuItems = (route) => [
|
|||||||
icon: 'menu',
|
icon: 'menu',
|
||||||
to: { name: 'missionary-dashboard' },
|
to: { name: 'missionary-dashboard' },
|
||||||
active: route.name === 'missionary-dashboard',
|
active: route.name === 'missionary-dashboard',
|
||||||
dedupeKey: 'dashboard',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'سامانه اعزام',
|
title: 'سامانه اعزام',
|
||||||
@@ -80,14 +76,12 @@ export const buildMissionaryMenuItems = (route) => [
|
|||||||
icon: 'graduation-cap',
|
icon: 'graduation-cap',
|
||||||
to: { name: 'missionary-education' },
|
to: { name: 'missionary-education' },
|
||||||
active: route.name?.startsWith('missionary-education'),
|
active: route.name?.startsWith('missionary-education'),
|
||||||
dedupeKey: 'education',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'تیکت',
|
title: 'تیکت',
|
||||||
icon: 'envelope-open',
|
icon: 'envelope-open',
|
||||||
to: { name: 'missionary-tickets' },
|
to: { name: 'missionary-tickets' },
|
||||||
active: route.name === 'missionary-tickets',
|
active: route.name === 'missionary-tickets',
|
||||||
dedupeKey: 'ticket',
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -97,14 +91,12 @@ export const buildCounselorMenuItems = (route) => [
|
|||||||
icon: 'menu',
|
icon: 'menu',
|
||||||
to: { name: 'counselor-dashboard' },
|
to: { name: 'counselor-dashboard' },
|
||||||
active: route.name === 'counselor-dashboard',
|
active: route.name === 'counselor-dashboard',
|
||||||
dedupeKey: 'dashboard',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'مشاوره',
|
title: 'مشاوره',
|
||||||
icon: 'chat',
|
icon: 'chat',
|
||||||
to: { name: 'counselor-consultations' },
|
to: { name: 'counselor-consultations' },
|
||||||
active: route.name === 'counselor-consultations',
|
active: route.name === 'counselor-consultations',
|
||||||
dedupeKey: 'consultation',
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -118,22 +110,15 @@ const MENU_BUILDERS = {
|
|||||||
* Union of every panel section unlocked by `roles`, in role order (roles
|
* Union of every panel section unlocked by `roles`, in role order (roles
|
||||||
* come back from the API ordered by role id, so a user's base role's
|
* come back from the API ordered by role id, so a user's base role's
|
||||||
* sections lead and each additional role's sections are appended after).
|
* sections lead and each additional role's sections are appended after).
|
||||||
* Items sharing a dedupeKey across roles are merged — only the first
|
|
||||||
* occurrence is kept (the primary role's version wins).
|
|
||||||
*/
|
*/
|
||||||
export const buildMergedMenuItems = (roles, route) => {
|
export const buildMergedMenuItems = (roles, route) => {
|
||||||
const seenRoles = new Set()
|
const seen = new Set()
|
||||||
const seenKeys = new Set()
|
|
||||||
const items = []
|
const items = []
|
||||||
for (const roleName of roles ?? []) {
|
for (const roleName of roles ?? []) {
|
||||||
const builder = MENU_BUILDERS[roleName]
|
const builder = MENU_BUILDERS[roleName]
|
||||||
if (!builder || seenRoles.has(roleName)) continue
|
if (!builder || seen.has(roleName)) continue
|
||||||
seenRoles.add(roleName)
|
seen.add(roleName)
|
||||||
for (const item of builder(route)) {
|
items.push(...builder(route))
|
||||||
if (item.dedupeKey && seenKeys.has(item.dedupeKey)) continue
|
|
||||||
if (item.dedupeKey) seenKeys.add(item.dedupeKey)
|
|
||||||
items.push(item)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,11 +196,6 @@ export const endpoints = {
|
|||||||
listAdminMissionaryRequests: '/admin/missionary-requests',
|
listAdminMissionaryRequests: '/admin/missionary-requests',
|
||||||
showAdminMissionaryRequest: '/admin/missionary-requests/:id',
|
showAdminMissionaryRequest: '/admin/missionary-requests/:id',
|
||||||
changeAdminMissionaryRequestStatus: '/admin/missionary-requests/:id/status',
|
changeAdminMissionaryRequestStatus: '/admin/missionary-requests/:id/status',
|
||||||
|
|
||||||
// ─── Admin: certificates ───────────────────────────────────────────────────
|
|
||||||
getAdminCertificates: '/admin/certificates',
|
|
||||||
addAdminCertificate: '/admin/certificates',
|
|
||||||
deleteAdminCertificate: '/admin/certificates/:id',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildUrl = (template, params = {}) =>
|
export const buildUrl = (template, params = {}) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user