fix: misc issues
This commit is contained in:
1
frontend/components.d.ts
vendored
1
frontend/components.d.ts
vendored
@@ -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']
|
||||||
|
|||||||
159
frontend/src/components/AdminBatchDashboard.vue
Normal file
159
frontend/src/components/AdminBatchDashboard.vue
Normal 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>
|
||||||
@@ -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()">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
:onCreate="
|
:onCreate="
|
||||||
(value, close) => {
|
(value, close) => {
|
||||||
openSettings('Members', close)
|
openSettings('Members', close)
|
||||||
|
show = false
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
106
lms/lms/utils.py
106
lms/lms/utils.py
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user