fix: misc issues

This commit is contained in:
Jannat Patel
2025-11-14 12:48:46 +05:30
parent 8bfc2a5297
commit d86fd0f6f6
12 changed files with 299 additions and 209 deletions

View File

@@ -8,6 +8,7 @@ export {}
/* prettier-ignore */ /* prettier-ignore */
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AdminBatchDashboard: typeof import('./src/components/AdminBatchDashboard.vue')['default']
Annoucements: typeof import('./src/components/Annoucements.vue')['default'] Annoucements: typeof import('./src/components/Annoucements.vue')['default']
AnnouncementModal: typeof import('./src/components/Modals/AnnouncementModal.vue')['default'] AnnouncementModal: typeof import('./src/components/Modals/AnnouncementModal.vue')['default']
Apps: typeof import('./src/components/Apps.vue')['default'] Apps: typeof import('./src/components/Apps.vue')['default']

View File

@@ -0,0 +1,159 @@
<template>
<div v-if="batch?.data" class="">
<div class="w-full flex items-center justify-between pb-4">
<div class="font-medium text-ink-gray-7">
{{ __('Statistics') }}
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-5 mb-8">
<NumberChart
class="border rounded-md"
:config="{ title: __('Students'), value: students.data?.length || 0 }"
/>
<NumberChart
class="border rounded-md"
:config="{
title: __('Certified'),
value: certificationCount.data || 0,
}"
/>
<NumberChart
class="border rounded-md"
:config="{
title: __('Courses'),
value: batch?.data?.courses?.length || 0,
}"
/>
<NumberChart
class="border rounded-md"
:config="{ title: __('Assessments'), value: assessmentCount || 0 }"
/>
</div>
<AxisChart
v-if="showProgressChart"
class="border"
:config="{
data: chartData || [],
title: __('Batch Summary'),
subtitle: __('Progress of students in courses and assessments'),
xAxis: {
key: 'task',
title: 'Tasks',
type: 'category',
},
yAxis: {
title: __('Number of Students'),
echartOptions: {
minInterval: 1,
},
},
swapXY: true,
series: [
{
name: 'value',
type: 'bar',
},
],
}"
/>
</div>
</template>
<script setup lang="ts">
import { AxisChart, createResource, NumberChart } from 'frappe-ui'
import { ref, watch } from 'vue'
const chartData = ref<null | any[]>(null)
const showProgressChart = ref(false)
const assessmentCount = ref(0)
const props = defineProps<{
batch: { [key: string]: any } | null
}>()
const students = createResource({
url: 'lms.lms.utils.get_batch_students',
params: {
batch: props.batch?.data?.name,
},
auto: true,
onSuccess(data: any[]) {
chartData.value = getChartData()
showProgressChart.value =
data.length &&
(props.batch?.data?.courses?.length || assessmentCount.value)
},
})
const getChartData = () => {
let tasks: any[] = []
let data: { task: any; value: any }[] = []
students.data.forEach((row: any) => {
tasks = countAssessments(row, tasks)
tasks = countCourses(row, tasks)
})
tasks.forEach((task) => {
data.push({
task: task.label,
value: task.value,
})
})
return data
}
const countAssessments = (
row: { assessments: { [x: string]: { result: string } } },
tasks: any[]
) => {
Object.keys(row.assessments).forEach((assessment) => {
if (row.assessments[assessment].result === 'Pass') {
tasks.filter((task) => task.label === assessment).length
? tasks.filter((task) => task.label === assessment)[0].value++
: tasks.push({
value: 1,
label: assessment,
})
}
})
return tasks
}
const countCourses = (
row: { courses: { [x: string]: number } },
tasks: any[]
) => {
Object.keys(row.courses).forEach((course) => {
if (row.courses[course] === 100) {
tasks.filter((task) => task.label === course).length
? tasks.filter((task) => task.label === course)[0].value++
: tasks.push({
value: 1,
label: course,
})
}
})
return tasks
}
const certificationCount = createResource({
url: 'frappe.client.get_count',
params: {
doctype: 'LMS Certificate',
filters: {
batch_name: props.batch?.data?.name,
},
},
auto: true,
})
watch(students, () => {
if (students.data?.length) {
assessmentCount.value = Object.keys(students.data?.[0].assessments).length
}
})
</script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<div class="text-lg font-semibold text-ink-gray-9"> <div class="font-medium text-ink-gray-9">
{{ __('Courses') }} {{ __('Courses') }}
</div> </div>
<Button v-if="canSeeAddButton()" @click="openCourseModal()"> <Button v-if="canSeeAddButton()" @click="openCourseModal()">

View File

@@ -1,70 +1,8 @@
<template> <template>
<div v-if="batch.data" class="">
<div class="w-full flex items-center justify-between pb-4">
<div class="font-medium text-ink-gray-7">
{{ __('Statistics') }}
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-5 mb-8">
<NumberChart
class="border rounded-md"
:config="{ title: __('Students'), value: students.data?.length || 0 }"
/>
<NumberChart
class="border rounded-md"
:config="{
title: __('Certified'),
value: certificationCount.data || 0,
}"
/>
<NumberChart
class="border rounded-md"
:config="{
title: __('Courses'),
value: batch.data.courses?.length || 0,
}"
/>
<NumberChart
class="border rounded-md"
:config="{ title: __('Assessments'), value: assessmentCount || 0 }"
/>
</div>
<AxisChart
v-if="showProgressChart"
:config="{
data: chartData,
title: __('Batch Summary'),
subtitle: __('Progress of students in courses and assessments'),
xAxis: {
key: 'task',
title: 'Tasks',
type: 'category',
},
yAxis: {
title: __('Number of Students'),
echartOptions: {
minInterval: 1,
},
},
swapXY: true,
series: [
{
name: 'value',
type: 'bar',
},
],
}"
/>
</div>
<div> <div>
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<div class="text-ink-gray-7 font-medium"> <div class="text-ink-gray-9 font-medium">
{{ __('Students') }} {{ students.data?.length }} {{ __('Students') }}
</div> </div>
<Button v-if="!readOnlyMode" @click="openStudentModal()"> <Button v-if="!readOnlyMode" @click="openStudentModal()">
<template #prefix> <template #prefix>
@@ -76,6 +14,7 @@
<div v-if="students.data?.length"> <div v-if="students.data?.length">
<ListView <ListView
class="max-h-[75vh]"
:columns="getStudentColumns()" :columns="getStudentColumns()"
:rows="students.data" :rows="students.data"
row-key="name" row-key="name"
@@ -151,7 +90,7 @@
</ListSelectBanner> </ListSelectBanner>
</ListView> </ListView>
</div> </div>
<div v-else class="text-sm italic text-ink-gray-5"> <div v-else-if="!students.loading" class="text-sm italic text-ink-gray-5">
{{ __('There are no students in this batch.') }} {{ __('There are no students in this batch.') }}
</div> </div>
</div> </div>
@@ -170,7 +109,6 @@
<script setup> <script setup>
import { import {
Avatar, Avatar,
AxisChart,
Button, Button,
createResource, createResource,
FeatherIcon, FeatherIcon,
@@ -181,30 +119,17 @@ import {
ListRows, ListRows,
ListView, ListView,
ListRowItem, ListRowItem,
NumberChart,
toast, toast,
} from 'frappe-ui' } from 'frappe-ui'
import { import { Plus, Trash2 } from 'lucide-vue-next'
BookOpen, import { ref } from 'vue'
GraduationCap,
Plus,
ShieldCheck,
Trash2,
User,
} from 'lucide-vue-next'
import { ref, watch } from 'vue'
import StudentModal from '@/components/Modals/StudentModal.vue' import StudentModal from '@/components/Modals/StudentModal.vue'
import ProgressBar from '@/components/ProgressBar.vue' import ProgressBar from '@/components/ProgressBar.vue'
import BatchStudentProgress from '@/components/Modals/BatchStudentProgress.vue' import BatchStudentProgress from '@/components/Modals/BatchStudentProgress.vue'
import ApexChart from 'vue3-apexcharts'
import { theme } from '@/utils/theme'
const showStudentModal = ref(false) const showStudentModal = ref(false)
const showStudentProgressModal = ref(false) const showStudentProgressModal = ref(false)
const selectedStudent = ref(null) const selectedStudent = ref(null)
const chartData = ref(null)
const showProgressChart = ref(false)
const assessmentCount = ref(0)
const readOnlyMode = window.read_only_mode const readOnlyMode = window.read_only_mode
const props = defineProps({ const props = defineProps({
@@ -220,12 +145,6 @@ const students = createResource({
batch: props.batch?.data?.name, batch: props.batch?.data?.name,
}, },
auto: true, auto: true,
onSuccess(data) {
chartData.value = getChartData()
showProgressChart.value =
data.length &&
(props.batch?.data?.courses?.length || assessmentCount.value)
},
}) })
const getStudentColumns = () => { const getStudentColumns = () => {
@@ -288,67 +207,4 @@ const removeStudents = (selections, unselectAll) => {
} }
) )
} }
const getChartData = () => {
let tasks = []
let data = []
students.data.forEach((row) => {
tasks = countAssessments(row, tasks)
tasks = countCourses(row, tasks)
})
tasks.forEach((task) => {
data.push({
task: task.label,
value: task.value,
})
})
return data
}
const countAssessments = (row, tasks) => {
Object.keys(row.assessments).forEach((assessment) => {
if (row.assessments[assessment].result === 'Pass') {
tasks.filter((task) => task.label === assessment).length
? tasks.filter((task) => task.label === assessment)[0].value++
: tasks.push({
value: 1,
label: assessment,
})
}
})
return tasks
}
const countCourses = (row, tasks) => {
Object.keys(row.courses).forEach((course) => {
if (row.courses[course] === 100) {
tasks.filter((task) => task.label === course).length
? tasks.filter((task) => task.label === course)[0].value++
: tasks.push({
value: 1,
label: course,
})
}
})
return tasks
}
watch(students, () => {
if (students.data?.length) {
assessmentCount.value = Object.keys(students.data?.[0].assessments).length
}
})
const certificationCount = createResource({
url: 'frappe.client.get_count',
params: {
doctype: 'LMS Certificate',
filters: {
batch_name: props.batch?.data?.name,
},
},
auto: true,
})
</script> </script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div <div
v-if="hasPermission() && !props.zoomAccount" v-if="hasPermission() && !props.zoomAccount"
class="flex items-center space-x-2 mb-5 bg-surface-amber-1 py-1 px-2 rounded-md text-ink-amber-3" class="flex items-center space-x-2 mb-5 bg-surface-amber-1 py-1 px-2 rounded-md text-ink-amber-3 text-xs"
> >
<AlertCircle class="size-4 stroke-1.5" /> <AlertCircle class="size-4 stroke-1.5" />
<span> <span>

View File

@@ -22,6 +22,7 @@
:onCreate=" :onCreate="
(value, close) => { (value, close) => {
openSettings('Members', close) openSettings('Members', close)
show = false
} }
" "
/> />

View File

@@ -1,12 +1,12 @@
<template> <template>
<div v-if="user.data?.is_moderator || isStudent" class=""> <div v-if="isAdmin || isStudent" class="">
<header <header
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5" class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
> >
<Breadcrumbs class="h-7" :items="breadcrumbs" /> <Breadcrumbs class="h-7" :items="breadcrumbs" />
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<Button <Button
v-if="user.data?.is_moderator && batch.data?.certification" v-if="isAdmin && batch.data?.certification"
@click="openCertificateDialog = true" @click="openCertificateDialog = true"
> >
{{ __('Generate Certificates') }} {{ __('Generate Certificates') }}
@@ -67,6 +67,9 @@
<BatchDashboard :batch="batch" :isStudent="isStudent" /> <BatchDashboard :batch="batch" :isStudent="isStudent" />
</div> </div>
<div v-else-if="tab.label == 'Dashboard'"> <div v-else-if="tab.label == 'Dashboard'">
<AdminBatchDashboard :batch="batch" />
</div>
<div v-else-if="tab.label == 'Students'">
<BatchStudents :batch="batch" /> <BatchStudents :batch="batch" />
</div> </div>
<div v-else-if="tab.label == 'Classes'"> <div v-else-if="tab.label == 'Classes'">
@@ -235,6 +238,7 @@ import BatchDashboard from '@/components/BatchDashboard.vue'
import BatchCourses from '@/components/BatchCourses.vue' import BatchCourses from '@/components/BatchCourses.vue'
import LiveClass from '@/components/LiveClass.vue' import LiveClass from '@/components/LiveClass.vue'
import BatchStudents from '@/components/BatchStudents.vue' import BatchStudents from '@/components/BatchStudents.vue'
import AdminBatchDashboard from '@/components/AdminBatchDashboard.vue'
import Assessments from '@/components/Assessments.vue' import Assessments from '@/components/Assessments.vue'
import Announcements from '@/components/Annoucements.vue' import Announcements from '@/components/Annoucements.vue'
import AnnouncementModal from '@/components/Modals/AnnouncementModal.vue' import AnnouncementModal from '@/components/Modals/AnnouncementModal.vue'
@@ -260,6 +264,13 @@ const tabs = computed(() => {
icon: LayoutDashboard, icon: LayoutDashboard,
}) })
if (isAdmin.value) {
batchTabs.push({
label: 'Students',
icon: ClipboardPen,
})
}
batchTabs.push({ batchTabs.push({
label: 'Courses', label: 'Courses',
icon: BookOpen, icon: BookOpen,
@@ -270,7 +281,7 @@ const tabs = computed(() => {
icon: Laptop, icon: Laptop,
}) })
if (user.data?.is_moderator) { if (isAdmin.value) {
batchTabs.push({ batchTabs.push({
label: 'Assessments', label: 'Assessments',
icon: BookOpenCheck, icon: BookOpenCheck,
@@ -367,6 +378,10 @@ const canMakeAnnouncement = () => {
return user.data?.is_moderator || user.data?.is_evaluator return user.data?.is_moderator || user.data?.is_evaluator
} }
const isAdmin = computed(() => {
return user.data?.is_moderator || user.data?.is_evaluator
})
usePageMeta(() => { usePageMeta(() => {
return { return {
title: batch?.data?.title, title: batch?.data?.title,

View File

@@ -244,12 +244,11 @@ const setQueryParams = () => {
} }
}) })
let queryString = '' history.replaceState(
if (queries.toString()) { {},
queryString = `?${queries.toString()}` '',
} `${location.pathname}${queries.size > 0 ? `?${queries.toString()}` : ''}`
)
history.replaceState({}, '', `${location.pathname}${queryString}`)
} }
const updateCategories = (data) => { const updateCategories = (data) => {

View File

@@ -124,7 +124,7 @@ const memberCount = ref(0)
const dayjs = inject('$dayjs') const dayjs = inject('$dayjs')
onMounted(() => { onMounted(() => {
getMemberCount() setFiltersFromQuery()
updateParticipants() updateParticipants()
}) })
@@ -158,6 +158,8 @@ const categories = createListResource({
const updateParticipants = () => { const updateParticipants = () => {
updateFilters() updateFilters()
getMemberCount() getMemberCount()
setQueryParams()
participants.update({ participants.update({
filters: filters.value, filters: filters.value,
}) })
@@ -178,6 +180,33 @@ const updateFilters = () => {
} }
} }
const setQueryParams = () => {
let queries = new URLSearchParams(location.search)
let filterKeys = {
category: currentCategory.value,
name: nameFilter.value,
}
Object.keys(filterKeys).forEach((key) => {
if (filterKeys[key]) {
queries.set(key, filterKeys[key])
} else {
queries.delete(key)
}
})
history.replaceState(
{},
'',
`${location.pathname}${queries.size > 0 ? `?${queries.toString()}` : ''}`
)
}
const setFiltersFromQuery = () => {
let queries = new URLSearchParams(location.search)
nameFilter.value = queries.get('name') || ''
currentCategory.value = queries.get('category') || ''
}
const breadcrumbs = computed(() => [ const breadcrumbs = computed(() => [
{ {
label: __('Certified Members'), label: __('Certified Members'),

View File

@@ -94,10 +94,10 @@
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
<div <div
v-for="evaluation in evals?.data" v-for="evaluation in evals?.data"
class="border rounded-md p-3 flex flex-col h-full cursor-pointer" class="border hover:border-outline-gray-3 rounded-md p-3 flex flex-col h-full cursor-pointer"
@click="redirectToProfile()" @click="redirectToProfile()"
> >
<div class="font-semibold text-ink-gray-9 text-lg mb-1"> <div class="font-semibold text-ink-gray-9 text-lg leading-5 mb-1">
{{ evaluation.course_title }} {{ evaluation.course_title }}
</div> </div>
<div class="text-ink-gray-7 text-sm"> <div class="text-ink-gray-7 text-sm">
@@ -128,8 +128,11 @@
{{ __('Upcoming Live Classes') }} {{ __('Upcoming Live Classes') }}
</div> </div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
<div v-for="cls in liveClasses?.data" class="border rounded-md p-3"> <div
<div class="font-semibold text-ink-gray-9 text-lg mb-1"> v-for="cls in liveClasses?.data"
class="border hover:border-outline-gray-3 rounded-md p-3"
>
<div class="font-semibold text-ink-gray-9 text-lg leading-5 mb-1">
{{ cls.title }} {{ cls.title }}
</div> </div>
<div class="text-ink-gray-7 text-sm leading-5 mb-4"> <div class="text-ink-gray-7 text-sm leading-5 mb-4">

View File

@@ -71,8 +71,11 @@
{{ __('Upcoming Live Classes') }} {{ __('Upcoming Live Classes') }}
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5"> <div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<div v-for="cls in myLiveClasses.data" class="border rounded-md p-2"> <div
<div class="font-semibold text-ink-gray-9 text-lg mb-1"> v-for="cls in myLiveClasses.data"
class="border rounded-md hover:border-outline-gray-3 p-2"
>
<div class="font-semibold text-ink-gray-9 text-lg leading-5 mb-1">
{{ cls.title }} {{ cls.title }}
</div> </div>
<div class="text-ink-gray-7 text-sm leading-5 mb-4"> <div class="text-ink-gray-7 text-sm leading-5 mb-4">

View File

@@ -1587,6 +1587,28 @@ def get_batch_students(batch):
"LMS Batch Enrollment", filters={"batch": batch}, fields=["member", "name"] "LMS Batch Enrollment", filters={"batch": batch}, fields=["member", "name"]
) )
for student in students_list:
details = get_batch_student_details(student)
calculate_student_progress(batch, details)
students.append(details)
students = sorted(students, key=lambda x: x.progress, reverse=True)
return students
def get_batch_student_details(student):
details = frappe.db.get_value(
"User",
student.member,
["full_name", "email", "username", "last_active", "user_image"],
as_dict=True,
)
details.last_active = format_datetime(details.last_active, "dd MMM YY")
details.name = student.name
details.assessments = frappe._dict()
return details
def calculate_student_progress(batch, details):
batch_courses = frappe.get_all("Batch Course", {"parent": batch}, ["course", "title"]) batch_courses = frappe.get_all("Batch Course", {"parent": batch}, ["course", "title"])
assessments = frappe.get_all( assessments = frappe.get_all(
"LMS Assessment", "LMS Assessment",
@@ -1594,53 +1616,55 @@ def get_batch_students(batch):
fields=["name", "assessment_type", "assessment_name"], fields=["name", "assessment_type", "assessment_name"],
) )
for student in students_list: calculate_course_progress(batch_courses, details)
courses_completed = 0 calculate_assessment_progress(assessments, details)
assessments_completed = 0
detail = frappe.db.get_value( if len(batch_courses) + len(assessments):
"User", details.progress = flt(
student.member, (
["full_name", "email", "username", "last_active", "user_image"], (details.average_course_progress * len(batch_courses))
as_dict=True, + (details.average_assessments_progress * len(assessments))
)
/ (len(batch_courses) + len(assessments)),
2,
) )
detail.last_active = format_datetime(detail.last_active, "dd MMM YY") else:
detail.name = student.name details.progress = 0
detail.courses = frappe._dict()
detail.assessments = frappe._dict()
""" Iterate through courses and track their progress """
for course in batch_courses:
progress = frappe.db.get_value(
"LMS Enrollment", {"course": course.course, "member": student.member}, "progress"
)
detail.courses[course.title] = progress
if progress == 100:
courses_completed += 1
""" Iterate through assessments and track their progress """ def calculate_course_progress(batch_courses, details):
for assessment in assessments: course_progress = []
title = frappe.db.get_value(assessment.assessment_type, assessment.assessment_name, "title") details.courses = frappe._dict()
assessment_info = has_submitted_assessment(
assessment.assessment_name, assessment.assessment_type, student.member
)
detail.assessments[title] = assessment_info
if assessment_info.result == "Pass": for course in batch_courses:
assessments_completed += 1 progress = frappe.db.get_value(
"LMS Enrollment", {"course": course.course, "member": details.email}, "progress"
)
details.courses[course.title] = progress
course_progress.append(progress)
detail.courses_completed = courses_completed details.average_course_progress = (
detail.assessments_completed = assessments_completed flt(sum(course_progress) / len(batch_courses), 2) if len(batch_courses) else 0
if len(batch_courses) + len(assessments): )
detail.progress = flt(
((courses_completed + assessments_completed) / (len(batch_courses) + len(assessments)) * 100),
2,
)
else:
detail.progress = 0
students.append(detail)
students = sorted(students, key=lambda x: x.progress, reverse=True) def calculate_assessment_progress(assessments, details):
return students assessments_completed = 0
details.assessments = frappe._dict()
for assessment in assessments:
title = frappe.db.get_value(assessment.assessment_type, assessment.assessment_name, "title")
assessment_info = has_submitted_assessment(
assessment.assessment_name, assessment.assessment_type, details.email
)
details.assessments[title] = assessment_info
if assessment_info.result == "Pass":
assessments_completed += 1
details.average_assessments_progress = (
flt((assessments_completed / len(assessments) * 100), 2) if len(assessments) else 0
)
def has_submitted_assessment(assessment, assessment_type, member=None): def has_submitted_assessment(assessment, assessment_type, member=None):