fix: missionary education
Deploy banu-front / deploy (push) Successful in 1m31s

This commit is contained in:
sajjadtalkhabi
2026-07-21 12:24:58 +03:30
parent 2a1f1b19d4
commit e8b1e4add6
10 changed files with 534 additions and 0 deletions
@@ -0,0 +1,95 @@
<template>
<div class="missionary-lesson-details">
<BoxedIconTitleBlock
class="missionary-lesson-details__heading"
title="جزئیات درس"
:desc="course?.title || 'جلسات این درس را مشاهده کنید.'"
>
<template #icon>
<SvgIcon name="book" :size="24" color="var(--color-primary)" />
</template>
</BoxedIconTitleBlock>
<SimpleTitleIconBlock title="همه جلسه ها" class="missionary-lesson-details__list-title">
<template #header-icon>
<SvgIcon name="list-bullets" :size="16" color="#bcbcbc" />
</template>
</SimpleTitleIconBlock>
<SkeletonLoaderBlock
v-if="sessionsLoading && sessions.length === 0"
:rows="3"
:cols-per-row="1"
/>
<div v-else-if="sessions.length > 0">
<StudentSessionItem
v-for="session in sessions"
:key="session.id"
:session="session"
@start="onStartSession"
/>
</div>
<NoItems v-else title="جلسه‌ای نیست" desc="جلسه‌ای برای این درس ثبت نشده است." />
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import { StudentSessionItem } from '@/features/student/sessions'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import {
useMissionaryPassedCoursesQuery,
useMissionaryPassedSessionsQuery,
} from '@/services/query/missionary-education'
const route = useRoute()
const router = useRouter()
const courseId = computed(() => route.params.id)
const termId = computed(() => route.query.termId || null)
// Course header comes from the passed-courses list cache (same key as the term
// page), so it needs no detail endpoint.
const coursesFilters = computed(() => ({ termId: termId.value }))
const coursesPagination = ref({ page: 1, perPage: 20 })
const { data: coursesData } = useMissionaryPassedCoursesQuery(coursesFilters, coursesPagination, {
enabled: () => !!termId.value,
})
const course = computed(() =>
(coursesData.value?.data ?? []).find((c) => String(c.id) === String(courseId.value))
)
const sessionsFilters = computed(() => ({ courseId: courseId.value }))
const sessionsPagination = ref({ page: 1, perPage: 20 })
const { data: sessionsData, isLoading: sessionsLoading } = useMissionaryPassedSessionsQuery(
sessionsFilters,
sessionsPagination,
{ enabled: () => !!courseId.value, keepPreviousData: true }
)
const sessions = computed(() => sessionsData.value?.data ?? [])
const onStartSession = (session) => {
router.push({ name: 'missionary-education-session', params: { id: session.id } }).catch(() => {})
}
</script>
<style lang="scss" scoped>
.missionary-lesson-details {
display: flex;
flex-direction: column;
gap: 0.5rem;
&__heading {
margin-bottom: 0.75rem;
}
&__list-title {
margin-top: 0.5rem;
}
}
</style>
@@ -0,0 +1,101 @@
<template>
<div class="missionary-term-details">
<BoxedIconTitleBlock
class="missionary-term-details__heading"
title="جزئیات ترم"
desc="اطلاعات کامل این ترم گذرانده‌شده را مشاهده کنید."
>
<template #icon>
<SvgIcon name="book" :size="24" color="var(--color-primary)" />
</template>
</BoxedIconTitleBlock>
<SkeletonLoaderBlock v-if="termLoading && !enrollment" :rows="2" :cols-per-row="1" />
<StudentTermSummary v-else-if="enrollment" :enrollment="enrollment" />
<SimpleTitleIconBlock title="همه درس ها" class="missionary-term-details__list-title">
<template #header-icon>
<SvgIcon name="list-bullets" :size="16" color="#bcbcbc" />
</template>
</SimpleTitleIconBlock>
<SkeletonLoaderBlock
v-if="coursesLoading && courses.length === 0"
:rows="3"
:cols-per-row="1"
/>
<div v-else-if="courses.length > 0">
<StudentLessonItem
v-for="lesson in courses"
:key="lesson.id"
:lesson="lesson"
@show-details="onShowLesson"
/>
</div>
<NoItems v-else title="درسی نیست" desc="درسی برای این ترم ثبت نشده است." />
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import { StudentLessonItem } from '@/features/student/lessons'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import StudentTermSummary from '@/features/student/terms/components/StudentTermSummary.vue'
import {
useMissionaryPassedTermsQuery,
useMissionaryPassedCoursesQuery,
} from '@/services/query/missionary-education'
const route = useRoute()
const router = useRouter()
const termId = computed(() => route.params.id)
// Term summary is served from the passed-terms list cache (same query key as
// the list page); it only refetches on a cold deep-link. No detail endpoint.
const termsPagination = ref({ page: 1, perPage: 20 })
const { data: termsData, isLoading: termLoading } = useMissionaryPassedTermsQuery(termsPagination)
const enrollment = computed(() =>
(termsData.value?.data ?? []).find((e) => String(e?.term?.id) === String(termId.value))
)
const coursesFilters = computed(() => ({ termId: termId.value }))
const coursesPagination = ref({ page: 1, perPage: 20 })
const { data: coursesData, isLoading: coursesLoading } = useMissionaryPassedCoursesQuery(
coursesFilters,
coursesPagination,
{ enabled: () => !!termId.value, keepPreviousData: true }
)
const courses = computed(() => coursesData.value?.data ?? [])
const onShowLesson = (lesson) => {
router
.push({
name: 'missionary-education-lesson',
params: { id: lesson.id },
query: { termId: termId.value },
})
.catch(() => {})
}
</script>
<style lang="scss" scoped>
.missionary-term-details {
display: flex;
flex-direction: column;
gap: 0.5rem;
&__heading {
margin-bottom: 0.75rem;
}
&__list-title {
margin-top: 0.5rem;
}
}
</style>
@@ -0,0 +1,70 @@
<template>
<div class="missionary-education">
<BoxedIconTitleBlock
class="missionary-education__heading"
title="آموزش من"
desc="ترم‌هایی که با موفقیت گذرانده‌اید را مشاهده کنید."
>
<template #icon>
<SvgIcon name="graduation-cap" :size="24" color="var(--color-primary)" />
</template>
</BoxedIconTitleBlock>
<SkeletonLoaderBlock v-if="isLoading" :rows="4" :cols-per-row="1" />
<div v-else-if="terms.length > 0">
<StudentTermItem
v-for="enrollment in terms"
:key="enrollment.id"
:enrollment="enrollment"
@show-details="onShowDetails"
/>
</div>
<NoItems v-else title="ترمی نیست" desc="هنوز ترمی را نگذرانده‌اید." />
<PaginationBlock :pagination="meta" @update:page="setPage" />
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import { usePagination } from '@/composables/usePagination'
import PaginationBlock from '@/components/blocks/PaginationBlock.vue'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import { useMissionaryPassedTermsQuery } from '@/services/query/missionary-education'
import StudentTermItem from '@/features/student/terms/components/StudentTermItem.vue'
const router = useRouter()
const { pagination, setPage } = usePagination({ page: 1, perPage: 20 })
const { data, isLoading } = useMissionaryPassedTermsQuery(pagination, { keepPreviousData: true })
const terms = computed(() => data.value?.data ?? [])
const meta = computed(() => ({
page: pagination.value.page,
perPage: pagination.value.perPage,
...data.value?.meta,
}))
const onShowDetails = (enrollment) => {
router
.push({ name: 'missionary-education-term', params: { id: enrollment?.term?.id } })
.catch(() => {})
}
</script>
<style lang="scss" scoped>
.missionary-education {
display: flex;
flex-direction: column;
gap: 0.5rem;
&__heading {
margin-bottom: 0.75rem;
}
}
</style>
+45
View File
@@ -32,4 +32,49 @@ export default [
subtitle: 'اینجا پل ارتباطی شما با پشتیبانی است.',
},
},
{
path: '/missionary-panel/education',
name: 'missionary-education',
component: () => import('@/features/missionary/education/pages/MissionaryTermsPage.vue'),
meta: {
layout: 'missionary',
role: 'missionary',
title: 'آموزش من',
subtitle: 'ترم‌ها و دوره‌های گذرانده‌شده خود را مرور کنید.',
},
},
{
path: '/missionary-panel/education/terms/:id',
name: 'missionary-education-term',
component: () => import('@/features/missionary/education/pages/MissionaryTermDetailsPage.vue'),
meta: {
layout: 'missionary',
role: 'missionary',
title: 'آموزش من',
subtitle: 'ترم‌ها و دوره‌های گذرانده‌شده خود را مرور کنید.',
},
},
{
path: '/missionary-panel/education/lessons/:id',
name: 'missionary-education-lesson',
component: () =>
import('@/features/missionary/education/pages/MissionaryLessonDetailsPage.vue'),
meta: {
layout: 'missionary',
role: 'missionary',
title: 'آموزش من',
subtitle: 'ترم‌ها و دوره‌های گذرانده‌شده خود را مرور کنید.',
},
},
{
path: '/missionary-panel/education/sessions/:id',
name: 'missionary-education-session',
component: () => import('@/features/student/sessions/pages/StudentSessionDetailsPage.vue'),
meta: {
layout: 'missionary',
role: 'missionary',
title: 'آموزش من',
subtitle: 'ترم‌ها و دوره‌های گذرانده‌شده خود را مرور کنید.',
},
},
]