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 */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AdminBatchDashboard: typeof import('./src/components/AdminBatchDashboard.vue')['default']
|
||||
Annoucements: typeof import('./src/components/Annoucements.vue')['default']
|
||||
AnnouncementModal: typeof import('./src/components/Modals/AnnouncementModal.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>
|
||||
<div>
|
||||
<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') }}
|
||||
</div>
|
||||
<Button v-if="canSeeAddButton()" @click="openCourseModal()">
|
||||
|
||||
@@ -1,70 +1,8 @@
|
||||
<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 class="flex items-center justify-between mb-4">
|
||||
<div class="text-ink-gray-7 font-medium">
|
||||
{{ __('Students') }}
|
||||
<div class="text-ink-gray-9 font-medium">
|
||||
{{ students.data?.length }} {{ __('Students') }}
|
||||
</div>
|
||||
<Button v-if="!readOnlyMode" @click="openStudentModal()">
|
||||
<template #prefix>
|
||||
@@ -76,6 +14,7 @@
|
||||
|
||||
<div v-if="students.data?.length">
|
||||
<ListView
|
||||
class="max-h-[75vh]"
|
||||
:columns="getStudentColumns()"
|
||||
:rows="students.data"
|
||||
row-key="name"
|
||||
@@ -151,7 +90,7 @@
|
||||
</ListSelectBanner>
|
||||
</ListView>
|
||||
</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.') }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -170,7 +109,6 @@
|
||||
<script setup>
|
||||
import {
|
||||
Avatar,
|
||||
AxisChart,
|
||||
Button,
|
||||
createResource,
|
||||
FeatherIcon,
|
||||
@@ -181,30 +119,17 @@ import {
|
||||
ListRows,
|
||||
ListView,
|
||||
ListRowItem,
|
||||
NumberChart,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import {
|
||||
BookOpen,
|
||||
GraduationCap,
|
||||
Plus,
|
||||
ShieldCheck,
|
||||
Trash2,
|
||||
User,
|
||||
} from 'lucide-vue-next'
|
||||
import { ref, watch } from 'vue'
|
||||
import { Plus, Trash2 } from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
import StudentModal from '@/components/Modals/StudentModal.vue'
|
||||
import ProgressBar from '@/components/ProgressBar.vue'
|
||||
import BatchStudentProgress from '@/components/Modals/BatchStudentProgress.vue'
|
||||
import ApexChart from 'vue3-apexcharts'
|
||||
import { theme } from '@/utils/theme'
|
||||
|
||||
const showStudentModal = ref(false)
|
||||
const showStudentProgressModal = ref(false)
|
||||
const selectedStudent = ref(null)
|
||||
const chartData = ref(null)
|
||||
const showProgressChart = ref(false)
|
||||
const assessmentCount = ref(0)
|
||||
const readOnlyMode = window.read_only_mode
|
||||
|
||||
const props = defineProps({
|
||||
@@ -220,12 +145,6 @@ const students = createResource({
|
||||
batch: props.batch?.data?.name,
|
||||
},
|
||||
auto: true,
|
||||
onSuccess(data) {
|
||||
chartData.value = getChartData()
|
||||
showProgressChart.value =
|
||||
data.length &&
|
||||
(props.batch?.data?.courses?.length || assessmentCount.value)
|
||||
},
|
||||
})
|
||||
|
||||
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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
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" />
|
||||
<span>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
:onCreate="
|
||||
(value, close) => {
|
||||
openSettings('Members', close)
|
||||
show = false
|
||||
}
|
||||
"
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div v-if="user.data?.is_moderator || isStudent" class="">
|
||||
<div v-if="isAdmin || isStudent" class="">
|
||||
<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"
|
||||
>
|
||||
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
||||
<div class="flex items-center space-x-2">
|
||||
<Button
|
||||
v-if="user.data?.is_moderator && batch.data?.certification"
|
||||
v-if="isAdmin && batch.data?.certification"
|
||||
@click="openCertificateDialog = true"
|
||||
>
|
||||
{{ __('Generate Certificates') }}
|
||||
@@ -67,6 +67,9 @@
|
||||
<BatchDashboard :batch="batch" :isStudent="isStudent" />
|
||||
</div>
|
||||
<div v-else-if="tab.label == 'Dashboard'">
|
||||
<AdminBatchDashboard :batch="batch" />
|
||||
</div>
|
||||
<div v-else-if="tab.label == 'Students'">
|
||||
<BatchStudents :batch="batch" />
|
||||
</div>
|
||||
<div v-else-if="tab.label == 'Classes'">
|
||||
@@ -235,6 +238,7 @@ import BatchDashboard from '@/components/BatchDashboard.vue'
|
||||
import BatchCourses from '@/components/BatchCourses.vue'
|
||||
import LiveClass from '@/components/LiveClass.vue'
|
||||
import BatchStudents from '@/components/BatchStudents.vue'
|
||||
import AdminBatchDashboard from '@/components/AdminBatchDashboard.vue'
|
||||
import Assessments from '@/components/Assessments.vue'
|
||||
import Announcements from '@/components/Annoucements.vue'
|
||||
import AnnouncementModal from '@/components/Modals/AnnouncementModal.vue'
|
||||
@@ -260,6 +264,13 @@ const tabs = computed(() => {
|
||||
icon: LayoutDashboard,
|
||||
})
|
||||
|
||||
if (isAdmin.value) {
|
||||
batchTabs.push({
|
||||
label: 'Students',
|
||||
icon: ClipboardPen,
|
||||
})
|
||||
}
|
||||
|
||||
batchTabs.push({
|
||||
label: 'Courses',
|
||||
icon: BookOpen,
|
||||
@@ -270,7 +281,7 @@ const tabs = computed(() => {
|
||||
icon: Laptop,
|
||||
})
|
||||
|
||||
if (user.data?.is_moderator) {
|
||||
if (isAdmin.value) {
|
||||
batchTabs.push({
|
||||
label: 'Assessments',
|
||||
icon: BookOpenCheck,
|
||||
@@ -367,6 +378,10 @@ const canMakeAnnouncement = () => {
|
||||
return user.data?.is_moderator || user.data?.is_evaluator
|
||||
}
|
||||
|
||||
const isAdmin = computed(() => {
|
||||
return user.data?.is_moderator || user.data?.is_evaluator
|
||||
})
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: batch?.data?.title,
|
||||
|
||||
@@ -244,12 +244,11 @@ const setQueryParams = () => {
|
||||
}
|
||||
})
|
||||
|
||||
let queryString = ''
|
||||
if (queries.toString()) {
|
||||
queryString = `?${queries.toString()}`
|
||||
}
|
||||
|
||||
history.replaceState({}, '', `${location.pathname}${queryString}`)
|
||||
history.replaceState(
|
||||
{},
|
||||
'',
|
||||
`${location.pathname}${queries.size > 0 ? `?${queries.toString()}` : ''}`
|
||||
)
|
||||
}
|
||||
|
||||
const updateCategories = (data) => {
|
||||
|
||||
@@ -124,7 +124,7 @@ const memberCount = ref(0)
|
||||
const dayjs = inject('$dayjs')
|
||||
|
||||
onMounted(() => {
|
||||
getMemberCount()
|
||||
setFiltersFromQuery()
|
||||
updateParticipants()
|
||||
})
|
||||
|
||||
@@ -158,6 +158,8 @@ const categories = createListResource({
|
||||
const updateParticipants = () => {
|
||||
updateFilters()
|
||||
getMemberCount()
|
||||
setQueryParams()
|
||||
|
||||
participants.update({
|
||||
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(() => [
|
||||
{
|
||||
label: __('Certified Members'),
|
||||
|
||||
@@ -94,10 +94,10 @@
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
<div
|
||||
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()"
|
||||
>
|
||||
<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 }}
|
||||
</div>
|
||||
<div class="text-ink-gray-7 text-sm">
|
||||
@@ -128,8 +128,11 @@
|
||||
{{ __('Upcoming Live Classes') }}
|
||||
</div>
|
||||
<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 class="font-semibold text-ink-gray-9 text-lg mb-1">
|
||||
<div
|
||||
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 }}
|
||||
</div>
|
||||
<div class="text-ink-gray-7 text-sm leading-5 mb-4">
|
||||
|
||||
@@ -71,8 +71,11 @@
|
||||
{{ __('Upcoming Live Classes') }}
|
||||
</div>
|
||||
<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 class="font-semibold text-ink-gray-9 text-lg mb-1">
|
||||
<div
|
||||
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 }}
|
||||
</div>
|
||||
<div class="text-ink-gray-7 text-sm leading-5 mb-4">
|
||||
|
||||
Reference in New Issue
Block a user