fix: user changes

This commit is contained in:
sajjadtalkhabi
2026-05-28 13:34:30 +03:30
parent cb86920015
commit 7ed6ec9ae1
46 changed files with 328 additions and 1114 deletions
@@ -215,26 +215,16 @@ const statusLabel = computed(
font-family: var(--font-family-fa);
font-size: 0.7rem;
&--watching {
&--active {
background: rgba(0, 112, 116, 8%);
color: #007074;
}
&--waitForExam {
background: rgba(204, 154, 40, 8%);
color: #cc6f00;
}
&--completed {
background: rgba(0, 154, 18, 8%);
color: #009a12;
}
&--failed {
background: rgba(204, 40, 49, 8%);
color: var(--color-error);
}
&--neutral {
background: rgba(180, 180, 180, 8%);
color: #686868;
@@ -11,12 +11,7 @@
</BoxedIconTitleBlock>
<SkeletonLoaderBlock v-if="isLoading && !exam" :rows="2" :cols-per-row="1" />
<StudentExamSummary
v-else-if="exam"
:exam="exam"
:starting="startMutation.isPending.value"
@start="onStart"
/>
<StudentExamSummary v-else-if="exam" :exam="exam" @start="onStart" />
<SimpleTitleIconBlock title="تعداد تلاش ها برای این آزمون" class="se__list-title">
<template #header-icon>
@@ -38,7 +33,6 @@
<script setup>
import { computed } from 'vue'
import { toast } from 'vue3-toastify'
import { useRoute, useRouter } from 'vue-router'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
@@ -46,10 +40,7 @@ import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import SimpleTitleIconBlock from '@/components/blocks/SimpleTitleIconBlock.vue'
import StudentExamSummary from '@/features/student/exams/components/StudentExamSummary.vue'
import StudentExamAttemptItem from '@/features/student/exams/components/StudentExamAttemptItem.vue'
import {
useStartStudentExamAttemptMutation,
useStudentExamQuery,
} from '@/services/query/student-exams'
import { useStudentExamQuery } from '@/services/query/student-exams'
const route = useRoute()
const router = useRouter()
@@ -62,15 +53,8 @@ const { data: exam, isLoading } = useStudentExamQuery(examId, {
const attempts = computed(() => exam.value?.attempts ?? [])
const startMutation = useStartStudentExamAttemptMutation()
const onStart = async () => {
try {
await startMutation.mutateAsync({ id: examId.value })
router.push({ name: 'student-take-exam', params: { id: examId.value } })
} catch {
toast.error('شروع آزمون با خطا مواجه شد.')
}
const onStart = () => {
router.push({ name: 'student-take-exam', params: { id: examId.value } }).catch(() => {})
}
</script>
@@ -121,10 +121,13 @@ const submitMutation = useSubmitStudentExamAttemptMutation()
const submit = async () => {
try {
await submitMutation.mutateAsync({
id: examId.value,
payload: { answers: { ...answers } },
})
const payload = {
answers: Object.entries(answers).map(([questionId, selectedOptionId]) => ({
questionId: Number(questionId),
selectedOptionId,
})),
}
await submitMutation.mutateAsync({ id: examId.value, payload })
toast.success('پاسخ‌های شما با موفقیت ثبت شد.')
router.push({ name: 'student-exam', params: { id: examId.value } })
} catch {
@@ -84,7 +84,7 @@ import SvgIcon from '@/components/icons/SvgIcon.vue'
import NoItems from '@/components/blocks/NoItems.vue'
import { formatJalaaliDate } from '@/utils/date-utils'
import TabsBlock from '@/components/blocks/TabsBlock.vue'
import { useStudentLessonQuery } from '@/services/query/student-lessons'
import { useStudentCourseQuery } from '@/services/query/student-courses'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import StudentExamItem from '@/features/student/exams/components/StudentExamItem.vue'
@@ -95,7 +95,7 @@ const router = useRouter()
const lessonId = computed(() => route.params.id)
const { data: lesson, isLoading } = useStudentLessonQuery(lessonId, {
const { data: lesson, isLoading } = useStudentCourseQuery(lessonId, {
enabled: () => !!lessonId.value,
})
+1 -1
View File
@@ -27,7 +27,7 @@ export default [
path: '/my-terms',
name: 'student-terms',
component: () => import('@/features/student/terms/pages/StudentTermsPage.vue'),
meta: { layout: 'student', role: 'student', title: 'دوره‌های آموزشی' },
meta: { layout: 'student', role: 'student', title: 'ترم‌های آموزشی' },
},
{
path: '/my-terms/:id',
@@ -111,11 +111,12 @@ import { objectToFormData } from '@/utils/object-to-formdata'
import VoiceRecorder from '@/components/form/VoiceRecorder.vue'
import ImageUploader from '@/components/form/ImageUploader.vue'
import VideoPlayerBlock from '@/components/blocks/VideoPlayerBlock.vue'
import { useUploadMediaMutation } from '@/services/query/auth'
import BoxedIconTitleBlock from '@/components/blocks/BoxedIconTitleBlock.vue'
import SkeletonLoaderBlock from '@/components/blocks/SkeletonLoaderBlock.vue'
import {
useStudentSessionQuery,
useSubmitStudentSessionHomeworkMutation,
useSubmitStudentHomeworkMutation,
} from '@/services/query/student-sessions'
const route = useRoute()
@@ -149,21 +150,39 @@ const errors = reactive({ audio: '', image: '' })
const canSubmit = computed(() => !!(homeworkAudio.value || homeworkImage.value))
const submitMutation = useSubmitStudentSessionHomeworkMutation()
const submitMutation = useSubmitStudentHomeworkMutation()
const uploadMediaMutation = useUploadMediaMutation()
const homeworkId = computed(() => session.value?.homeworkId ?? session.value?.homework?.id ?? null)
const uploadHomeworkFile = async (file) => {
const fd = objectToFormData({ file, purpose: 'homework_file', context: 'homework' })
const response = await uploadMediaMutation.mutateAsync(fd)
const payload = response?.data ?? response
return payload?.id ?? null
}
const onSubmit = async () => {
if (!canSubmit.value) {
errors.audio = 'حداقل یک فایل صوتی یا تصویر ارسال کنید.'
return
}
if (!homeworkId.value) {
toast.error('تکلیفی برای این جلسه ثبت نشده است.')
return
}
errors.audio = ''
errors.image = ''
try {
const payload = { subType: 'homework' }
if (homeworkAudio.value) payload.audio = homeworkAudio.value
if (homeworkImage.value) payload.image = homeworkImage.value
const fd = objectToFormData(payload)
await submitMutation.mutateAsync({ id: sessionId.value, payload: fd })
const files = [homeworkAudio.value, homeworkImage.value].filter(Boolean)
for (const file of files) {
const mediaId = await uploadHomeworkFile(file)
if (!mediaId) throw new Error('upload failed')
await submitMutation.mutateAsync({
homeworkId: homeworkId.value,
payload: { mediaId },
})
}
toast.success('تکلیف با موفقیت ارسال شد.')
homeworkAudio.value = null
homeworkImage.value = null
@@ -1,50 +1,42 @@
<template>
<div class="student-term-item">
<div class="student-term-item__box">
<div class="student-term-item__accent" />
<div class="student-term-item__info">
<p class="student-term-item__title">{{ term.title || '—' }}</p>
<div class="student-term-item__teacher">
<SvgIcon name="user" :size="10" color="#bcbcbc" />
<span class="student-term-item__teacher-label">استـــــــاد:</span>
<span class="student-term-item__teacher-name">{{ teacherName }}</span>
<div class="sti">
<div class="sti__box">
<div class="sti__box__image">
<img v-if="term.coverUrl" :src="term.coverUrl" :alt="term.title" />
<SvgIcon v-else name="book" :size="32" color="#bcbcbc" />
</div>
<div class="sti__info">
<p class="sti__title">{{ term.title || '—' }}</p>
<div class="sti__teacher">
<span class="sti__teacher-label">تعداد دوره ها:</span>
<span class="sti__teacher-name">{{ term.coursesCount ?? 0 }}</span>
</div>
</div>
</div>
<div class="student-term-item__meta">
<div class="student-term-item__dates">
<div class="student-term-item__date">
<span class="student-term-item__date-label">تاریخ پایان :</span>
<span class="student-term-item__date-value">{{ endDate }}</span>
<div class="sti__meta">
<div class="sti__dates">
<div class="sti__date">
<span class="sti__date-label">تاریخ پایان :</span>
<span class="sti__date-value">{{ endDate }}</span>
</div>
<span class="student-term-item__date-divider" />
<div class="student-term-item__date">
<span class="student-term-item__date-label">تاریخ شروع :</span>
<span class="student-term-item__date-value">{{ startDate }}</span>
<span class="sti__date-divider" />
<div class="sti__date">
<span class="sti__date-label">تاریخ شروع :</span>
<span class="sti__date-value">{{ startDate }}</span>
</div>
</div>
<div v-if="hasGpa" class="student-term-item__status student-term-item__status--gpa">
<span class="student-term-item__status-label">معدل دوره :</span>
<span class="student-term-item__status-value student-term-item__status-value--en">
{{ gpaText }}
</span>
</div>
<div
v-else
class="student-term-item__status"
:class="`student-term-item__status--${term.status || 'watching'}`"
>
<span class="student-term-item__status-label">وضعیت دوره :</span>
<span class="student-term-item__status-value">{{ statusLabel }}</span>
<div class="sti__status" :class="`sti__status--${status}`">
<span class="sti__status-label">وضعیت دوره :</span>
<span class="sti__status-value">{{ statusLabel }}</span>
</div>
</div>
<div v-if="showDetails" class="student-term-item__actions">
<div v-if="showDetails" class="sti__actions">
<BaseButton
text="جزئیات دوره"
custom-class="student-term-item__details-btn"
@click="emit('show-details', term)"
text="جزئیات ترم"
custom-class="sti__details-btn"
@click="emit('show-details', enrollment)"
>
<template #appendIcon>
<SvgIcon name="arrow-left" :size="14" color="#fff" />
@@ -62,40 +54,25 @@ import SvgIcon from '@/components/icons/SvgIcon.vue'
import { formatJalaaliDate } from '@/utils/date-utils'
const props = defineProps({
term: { type: Object, required: true },
enrollment: { type: Object, required: true },
showDetails: { type: Boolean, default: true },
})
const emit = defineEmits(['show-details'])
const teacherName = computed(() => {
const t = props.term.teacher
if (t) return `${t.firstName || ''} ${t.lastName || ''}`.trim() || t.fullName || '—'
return props.term.teacherName || '—'
})
const term = computed(() => props.enrollment.term ?? {})
const startDate = computed(
() => props.term.faStartDate || formatJalaaliDate(props.term.startDate) || '—'
)
const status = computed(() => props.enrollment.status || 'active')
const endDate = computed(() => props.term.faEndDate || formatJalaaliDate(props.term.endDate) || '—')
const statusLabel = computed(() => STUDENT_COURSE_STATUS[status.value] || '—')
const statusLabel = computed(
() => props.term.statusLabel || STUDENT_COURSE_STATUS[props.term.status] || '—'
)
const startDate = computed(() => formatJalaaliDate(term.value.startsAt) || '—')
const hasGpa = computed(
() => props.term.gpa !== undefined && props.term.gpa !== null && props.term.gpa !== ''
)
const gpaText = computed(() => {
const v = Number(props.term.gpa)
return Number.isFinite(v) ? v.toFixed(3).replace(/0+$/, '').replace(/\.$/, '') : props.term.gpa
})
const endDate = computed(() => formatJalaaliDate(term.value.endsAt) || '—')
</script>
<style lang="scss" scoped>
.student-term-item {
.sti {
display: flex;
flex-direction: column-reverse;
gap: 0.625rem;
@@ -114,6 +91,24 @@ const gpaText = computed(() => {
&__box {
display: flex;
align-items: center;
&__image {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.5rem;
background: #f5f5f5;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
&__info {
@@ -136,6 +131,7 @@ const gpaText = computed(() => {
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
text-align: right;
}
&__teacher {
@@ -175,34 +171,15 @@ const gpaText = computed(() => {
font-size: 0.75rem;
white-space: nowrap;
&--watching {
&--active {
background: rgba(0, 112, 116, 4%);
color: #007074;
}
&--waitForExam {
background: rgba(116, 102, 0, 4%);
color: #746a00;
}
&--completed {
background: rgba(0, 154, 18, 6%);
color: #009a12;
}
&--failed {
background: rgba(204, 40, 49, 6%);
color: #cc2831;
}
&--gpa {
background: rgba(0, 112, 116, 6%);
color: #007074;
}
}
&__status-value--en {
font-family: var(--font-family-en);
}
&__status-label {
@@ -1,13 +1,13 @@
<template>
<div class="student-term-summary">
<div class="student-term-summary__cover">
<img v-if="term.image" :src="term.image" :alt="term.title" />
<img v-if="term.coverUrl" :src="term.coverUrl" :alt="term.title" />
</div>
<div class="student-term-summary__info">
<div
class="student-term-summary__status"
:class="`student-term-summary__status--${term.status || 'watching'}`"
:class="`student-term-summary__status--${status}`"
>
<span class="student-term-summary__status-label">وضعیت دوره :</span>
<span class="student-term-summary__status-value">{{ statusLabel }}</span>
@@ -43,24 +43,24 @@ import SvgIcon from '@/components/icons/SvgIcon.vue'
import { formatJalaaliDate } from '@/utils/date-utils'
const props = defineProps({
term: { type: Object, required: true },
enrollment: { type: Object, required: true },
})
const term = computed(() => props.enrollment.term ?? {})
const status = computed(() => props.enrollment.status || 'active')
const statusLabel = computed(() => STUDENT_COURSE_STATUS[status.value] || '—')
const teacherName = computed(() => {
const t = props.term.teacher
const t = term.value.teacher
if (t) return `${t.firstName || ''} ${t.lastName || ''}`.trim() || t.fullName || '—'
return props.term.teacherName || '—'
return term.value.teacherName || '—'
})
const startDate = computed(
() => props.term.faStartDate || formatJalaaliDate(props.term.startDate) || '—'
)
const startDate = computed(() => formatJalaaliDate(term.value.startsAt) || '—')
const endDate = computed(() => props.term.faEndDate || formatJalaaliDate(props.term.endDate) || '—')
const statusLabel = computed(
() => props.term.statusLabel || STUDENT_COURSE_STATUS[props.term.status] || '—'
)
const endDate = computed(() => formatJalaaliDate(term.value.endsAt) || '—')
</script>
<style lang="scss" scoped>
@@ -124,25 +124,15 @@ const statusLabel = computed(
font-family: var(--font-family-fa);
white-space: nowrap;
&--watching {
&--active {
background: rgba(0, 112, 116, 4%);
color: #007074;
}
&--waitForExam {
background: rgba(116, 102, 0, 4%);
color: #746a00;
}
&--completed {
background: rgba(0, 154, 18, 6%);
color: #009a12;
}
&--failed {
background: rgba(204, 40, 49, 6%);
color: #cc2831;
}
}
&__status-label {
@@ -10,8 +10,8 @@
</template>
</BoxedIconTitleBlock>
<SkeletonLoaderBlock v-if="isLoading && !term" :rows="2" :cols-per-row="1" />
<StudentTermSummary v-else-if="term" :term="term" />
<SkeletonLoaderBlock v-if="isLoading && !enrollment" :rows="2" :cols-per-row="1" />
<StudentTermSummary v-else-if="enrollment" :enrollment="enrollment" />
<SimpleTitleIconBlock title="همه درس ها" class="student-term-details__list-title">
<template #header-icon>
@@ -49,11 +49,11 @@ const router = useRouter()
const termId = computed(() => route.params.id)
const { data: term, isLoading } = useStudentTermQuery(termId, {
const { data: enrollment, isLoading } = useStudentTermQuery(termId, {
enabled: () => !!termId.value,
})
const lessons = computed(() => term.value?.courses ?? term.value?.lessons ?? [])
const lessons = computed(() => enrollment.value?.term?.courses ?? [])
const onShowLesson = (lesson) => {
router.push({ name: 'student-lesson-details', params: { id: lesson.id } }).catch(() => {})
@@ -2,8 +2,8 @@
<div class="student-terms">
<BoxedIconTitleBlock
class="student-terms__heading"
title="دوره‌های آموزشی"
desc="دوره‌ها و آموزش‌های گذرانده‌شده خود را مشاهده کنید."
title="ترم‌های آموزشی"
desc="ترم‌ها و آموزش‌های گذرانده‌شده خود را مشاهده کنید."
>
<template #icon>
<SvgIcon name="book" :size="24" color="var(--color-primary)" />
@@ -15,13 +15,13 @@
<SkeletonLoaderBlock v-if="currentPending" :rows="4" :cols-per-row="1" />
<div v-else-if="currentTerms.length > 0">
<StudentTermItem
v-for="term in currentTerms"
:key="term.id"
:term="term"
v-for="enrollment in currentTerms"
:key="enrollment.id"
:enrollment="enrollment"
@show-details="onShowDetails"
/>
</div>
<NoItems v-else title="دوره‌ای نیست" desc="در حال حاضر دوره‌ای در حال گذراندن ندارید." />
<NoItems v-else title="ترم‌ای نیست" desc="در حال حاضر ترم‌ای در حال گذراندن ندارید." />
<PaginationBlock :pagination="currentMeta" @update:page="setCurrentPage" />
</template>
@@ -29,14 +29,14 @@
<SkeletonLoaderBlock v-if="endedPending" :rows="4" :cols-per-row="1" />
<div v-else-if="endedTerms.length > 0">
<StudentTermItem
v-for="term in endedTerms"
:key="term.id"
:term="term"
v-for="enrollment in endedTerms"
:key="enrollment.id"
:enrollment="enrollment"
:showDetails="false"
@show-details="onShowDetails"
/>
</div>
<NoItems v-else title="موردی نیست" desc="هنوز دوره‌ای را تمام نکرده‌اید." />
<NoItems v-else title="موردی نیست" desc="هنوز ترم‌ای را تمام نکرده‌اید." />
<PaginationBlock :pagination="endedMeta" @update:page="setEndedPage" />
</template>
</TabsBlock>
@@ -59,13 +59,13 @@ import StudentTermItem from '@/features/student/terms/components/StudentTermItem
const router = useRouter()
const tabs = [
{ name: 'current', label: 'دوره های در حال گذراندن', icon: 'list-bullets' },
{ name: 'ended', label: 'دوره های تکمیل شده', icon: 'list-bullets' },
{ name: 'current', label: 'ترم های در حال گذراندن', icon: 'list-bullets' },
{ name: 'ended', label: 'ترم های تکمیل شده', icon: 'list-bullets' },
]
const activeTab = ref('current')
const currentFilters = ref({ status: 'current' })
const endedFilters = ref({ status: 'ended' })
const currentFilters = ref({ status: 'active' })
const endedFilters = ref({ status: 'completed' })
const { pagination: currentPagination, setPage: setCurrentPage } = usePagination({
page: 1,
@@ -97,6 +97,7 @@ const { data: endedData, isLoading: endedPending } = useStudentTermsListQuery(
const currentTerms = computed(() => currentData.value?.data ?? [])
const endedTerms = computed(() => endedData.value?.data ?? [])
console.log({ endedTerms: endedData.value })
const currentMeta = computed(() => ({
page: currentPagination.value.page,