feat: student progress on course dashboard

This commit is contained in:
Jannat Patel
2026-02-03 21:31:31 +05:30
parent 754d3cf2ca
commit 582540e7f0
7 changed files with 413 additions and 56 deletions

View File

@@ -112,6 +112,14 @@
v-else-if="lesson.icon === 'icon-quiz'"
class="h-4 w-4 stroke-1 mr-2"
/>
<NotebookPen
v-else-if="lesson.icon === 'icon-assignment'"
class="h-4 w-4 stroke-1 mr-2"
/>
<SquareCode
v-else-if="lesson.icon === 'icon-code'"
class="h-4 w-4 stroke-1 mr-2"
/>
<FileText
v-else-if="lesson.icon === 'icon-list'"
class="h-4 w-4 text-ink-gray-9 stroke-1 mr-2"
@@ -177,8 +185,11 @@ import {
FilePenLine,
HelpCircle,
MonitorPlay,
NotebookPen,
Plus,
SquareCode,
Trash2,
Notebook,
} from 'lucide-vue-next'
import { useRoute, useRouter } from 'vue-router'
import ChapterModal from '@/components/Modals/ChapterModal.vue'

View File

@@ -63,50 +63,52 @@
</ListHeaderItem>
</ListHeader>
<ListRows v-for="row in progressList.data" class="max-h-[500px]">
<router-link
:to="{
name: 'Profile',
params: { username: row.member_username },
}"
<ListRow
:row="row"
@click="
() => {
showProgressModal = true
currentStudent = row
}
"
class="cursor-pointer"
>
<ListRow :row="row">
<template #default="{ column, item }">
<ListRowItem
:item="row[column.key]"
:align="column.align"
class="w-full"
>
<template #prefix>
<div v-if="column.key == 'member_name'">
<Avatar
class="flex items-center"
:image="row['member_image']"
:label="item"
size="sm"
/>
</div>
<ProgressBar
v-else-if="column.key == 'progress'"
:progress="Math.ceil(row[column.key])"
class="!mx-0 !mr-4"
<template #default="{ column, item }">
<ListRowItem
:item="row[column.key]"
:align="column.align"
class="w-full"
>
<template #prefix>
<div v-if="column.key == 'member_name'">
<Avatar
class="flex items-center"
:image="row['member_image']"
:label="item"
size="sm"
/>
</template>
<div v-if="column.key == 'creation'">
{{ dayjs(row[column.key]).format('DD MMM YYYY') }}
</div>
<div
<ProgressBar
v-else-if="column.key == 'progress'"
class="text-xs !mx-0 w-5"
>
{{ Math.ceil(row[column.key]) }}%
</div>
<div v-else>
{{ row[column.key].toString() }}
</div>
</ListRowItem>
</template>
</ListRow>
</router-link>
:progress="Math.ceil(row[column.key])"
class="!mx-0 !mr-4"
/>
</template>
<div v-if="column.key == 'creation'">
{{ dayjs(row[column.key]).format('DD MMM YYYY') }}
</div>
<div
v-else-if="column.key == 'progress'"
class="text-xs !mx-0 w-5"
>
{{ Math.ceil(row[column.key]) }}%
</div>
<div v-else>
{{ row[column.key].toString() }}
</div>
</ListRowItem>
</template>
</ListRow>
</ListRows>
</ListView>
<div
@@ -142,6 +144,8 @@
? 'red'
: row.name.startsWith('In')
? 'amber'
: row.name.startsWith('Adv')
? 'blue'
: 'green'
][400],
}"
@@ -151,11 +155,13 @@
{{ row.name.split('(')[0] }}
</div>
</Tooltip>
<div class="ml-auto">
{{
Math.round((row.value / course.data?.enrollments) * 100)
}}%
</div>
<Tooltip :text="row.value">
<div class="ml-auto">
{{
Math.round((row.value / course.data?.enrollments) * 100)
}}%
</div>
</Tooltip>
</div>
</div>
<ECharts
@@ -239,6 +245,13 @@
v-model="showEnrollmentModal"
:course="course"
/>
<StudentCourseProgress
v-if="showProgressModal"
v-model="showProgressModal"
:course="course"
:student="currentStudent"
:lessons="lessonProgress"
/>
</template>
<script setup lang="ts">
import {
@@ -260,12 +273,13 @@ import {
Tooltip,
} from 'frappe-ui'
import { computed, ref, watch } from 'vue'
import { ChevronDown, Plus, Star } from 'lucide-vue-next'
import { Plus, Star } from 'lucide-vue-next'
import { formatAmount } from '@/utils'
import colors from '@/utils/frappe-ui-colors.json'
import CourseEnrollmentModal from '@/pages/Courses/CourseEnrollmentModal.vue'
import NumberChartGraph from '@/components/NumberChartGraph.vue'
import ProgressBar from '@/components/ProgressBar.vue'
import StudentCourseProgress from '@/pages/Courses/StudentCourseProgress.vue'
const props = defineProps<{
course: any
@@ -273,6 +287,8 @@ const props = defineProps<{
const showEnrollmentModal = ref(false)
const searchFilter = ref<string | null>(null)
const showProgressModal = ref(false)
const currentStudent = ref<any>(null)
const theme = ref<'darkMode' | 'lightMode'>(
localStorage.getItem('theme') == 'dark' ? 'darkMode' : 'lightMode'
)
@@ -307,6 +323,7 @@ const progressList = createListResource({
],
pageLength: 100,
auto: true,
cache: ['courseProgress', props.course.data?.name],
})
const lessonProgress = createResource({
@@ -357,6 +374,7 @@ const progressColors = computed(() => {
let colorList = []
colorList.push(colors[theme.value]['red'][400])
colorList.push(colors[theme.value]['amber'][400])
colorList.push(colors[theme.value]['blue'][400])
colorList.push(colors[theme.value]['green'][400])
return colorList
})

View File

@@ -0,0 +1,194 @@
<template>
<Dialog
v-model="show"
:options="{
title: __('Course Progress'),
size: '4xl',
}"
>
<template #body-content>
<div class="text-base">
<div class="flex justify-between mb-5">
<div class="flex space-x-2">
<Avatar
:image="student?.member_image"
:label="student?.member_name"
size="lg"
/>
<div>
<div class="font-semibold">
{{ student?.member_name }}
</div>
<div class="text-ink-gray-5">
{{ student.member }}
</div>
</div>
</div>
<div class="w-25 space-y-2">
<div class="text-ink-gray-5 text-sm">
{{ Math.round(student.progress) }}% {{ __('completed') }}
</div>
<ProgressBar
:label="__('Course Progress')"
:progress="student.progress"
/>
</div>
</div>
<div class="grid grid-cols-2 gap-5">
<div v-if="lessons.data" class="border rounded-lg px-3 pt-3">
<div>
<div class="text-ink-gray-5 mb-5">
{{ __('Lesson Progress') }}
</div>
</div>
<div
v-for="progress in lessons.data"
class="flex justify-between text-sm py-2 my-1"
>
<div class="">
<span class="mr-3 text-xs">
{{ progress.chapter_idx }}.{{ progress.idx }}
</span>
<span>
{{ progress.title }}
</span>
</div>
<Badge :theme="getLessonStatusTheme(progress)">
{{ getLessonStatus(progress) }}
</Badge>
</div>
</div>
<div class="space-y-3">
<div
v-if="assessmentProgress.data?.quizzes?.length"
class="border rounded-lg px-3 pt-3 h-fit"
>
<div>
<div class="text-ink-gray-5 mb-5">
{{ __('Quiz Progress') }}
</div>
</div>
<div
v-for="quiz in assessmentProgress.data.quizzes"
class="flex justify-between text-sm py-2 my-1"
>
<div>
{{ quiz.quiz_title }}
</div>
<div>
{{ quiz.score }}
</div>
<div>{{ quiz.percentage }}%</div>
</div>
</div>
<div
v-if="assessmentProgress.data?.assignments?.length"
class="border rounded-lg px-3 pt-3 h-fit"
>
<div>
<div class="text-ink-gray-5 mb-5">
{{ __('Assignment Progress') }}
</div>
</div>
<div
v-for="assignment in assessmentProgress.data.assignments"
class="flex justify-between text-sm py-2 my-1"
>
<div>
{{ assignment.assignment_title }}
</div>
<Badge :theme="getAssessmentStatusTheme(assignment.status)">
{{ assignment.status }}
</Badge>
</div>
</div>
<div
v-if="assessmentProgress.data?.exercises?.length"
class="border rounded-lg px-3 pt-3 h-fit"
>
<div>
<div class="text-ink-gray-5 mb-5">
{{ __('Programming Exercise Progress') }}
</div>
</div>
<div
v-for="exercise in assessmentProgress.data.exercises"
class="flex justify-between text-sm py-2 my-1"
>
<div>
{{ exercise.exercise_title }}
</div>
<Badge :theme="getAssessmentStatusTheme(exercise.status)">
{{ exercise.status }}
</Badge>
</div>
</div>
</div>
</div>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import {
Avatar,
Badge,
createListResource,
createResource,
Dialog,
} from 'frappe-ui'
import ProgressBar from '@/components/ProgressBar.vue'
const show = defineModel<boolean>({ required: true, default: false })
const props = defineProps<{
course: any
student: any
lessons: any
}>()
const lessonProgress = createListResource({
doctype: 'LMS Course Progress',
filters: {
course: ['=', props.course.data?.name],
member: ['=', props.student?.member],
},
fields: ['name', 'lesson', 'status'],
auto: true,
})
const assessmentProgress = createResource({
url: 'lms.lms.api.get_course_assessment_progress',
params: {
course: props.course.data?.name,
member: props.student?.member,
},
auto: true,
})
const getLessonStatus = (lesson: any) => {
return (
lessonProgress.data?.find((lp: any) => lp.lesson === lesson.lesson)
?.status || __('Pending')
)
}
const getLessonStatusTheme = (lesson: any) => {
const status = getLessonStatus(lesson)
if (status === 'Complete') {
return 'green'
} else {
return 'orange'
}
}
const getAssessmentStatusTheme = (status: string) => {
if (status.includes('Pass')) return 'green'
else if (status.includes('Fail')) return 'red'
else return 'orange'
}
</script>

View File

@@ -72,7 +72,7 @@
</div>
</div>
<div v-if="myCourses.data?.length" class="mt-10">
<div v-if="myCourses.data?.length">
<div class="flex items-center justify-between mb-3">
<span class="font-semibold text-lg text-ink-gray-9">
{{