feat: batch student progress modal

This commit is contained in:
Jannat Patel
2024-12-19 23:00:28 +05:30
parent b48e007ea8
commit 4d18580482
7 changed files with 422 additions and 473 deletions

View File

@@ -1,21 +1,36 @@
<template>
<div class="flex items-center justify-between mb-4">
<div class="text-lg font-semibold">
{{ __('Students') }}
<div>
<!-- <Bar v-if="chartData" :data="chartData" :options="chartOptions" /> -->
<ApexChart
v-if="chartData"
:options="chartOptions"
:series="chartData"
type="bar"
height="350"
/>
<div class="flex items-center justify-between mb-4">
<div class="text-lg font-semibold">
{{ __('Students') }}
</div>
<Button @click="openStudentModal()">
<template #prefix>
<Plus class="h-4 w-4" />
</template>
{{ __('Add') }}
</Button>
</div>
<Button @click="openStudentModal()">
<template #prefix>
<Plus class="h-4 w-4" />
</template>
{{ __('Add') }}
</Button>
</div>
<div v-if="students.data?.length">
<ListView
:columns="getStudentColumns()"
:rows="students.data"
row-key="name"
:options="{ showTooltip: false }"
:options="{
showTooltip: false,
onRowClick: (row) => {
openStudentProgressModal(row)
},
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-gray-100 p-2"
@@ -48,10 +63,16 @@
/>
</div>
</template>
<div v-if="column.key == 'courses'">
<div
v-if="column.key == 'progress'"
class="flex items-center space-x-4 w-full"
>
<ProgressBar :progress="row[column.key]" size="sm" />
</div>
<div v-else>
{{ row[column.key] }}
</div>
<div v-else-if="column.icon == 'book-open'">
<!-- <div v-else-if="column.icon == 'book-open'">
{{ Math.ceil(row.courses[column.key]) }}
</div>
<div v-else-if="column.icon == 'help-circle'">
@@ -63,7 +84,7 @@
{{ row.assessments[column.key] }}
</Badge>
<div v-else>{{ parseInt(row.assessments[column.key]) }}</div>
</div>
</div> -->
</ListRowItem>
</template>
</ListRow>
@@ -90,11 +111,14 @@
v-model="showStudentModal"
v-model:reloadStudents="students"
/>
<BatchStudentProgress
:student="selectedStudent"
v-model="showStudentProgressModal"
/>
</template>
<script setup>
import {
Avatar,
Badge,
Button,
createResource,
FeatherIcon,
@@ -107,11 +131,38 @@ import {
ListRowItem,
} from 'frappe-ui'
import { Trash2, Plus } from 'lucide-vue-next'
import { ref } from 'vue'
import { computed, ref } from 'vue'
import StudentModal from '@/components/Modals/StudentModal.vue'
import { showToast } from '@/utils'
import ProgressBar from '@/components/ProgressBar.vue'
import BatchStudentProgress from '@/components/Modals/BatchStudentProgress.vue'
import { Bar } from 'vue-chartjs'
import {
Chart as ChartJS,
Title,
Tooltip,
Legend,
BarElement,
CategoryScale,
LinearScale,
Filler,
} from 'chart.js'
ChartJS.register(
Title,
Tooltip,
Legend,
BarElement,
CategoryScale,
LinearScale,
Filler
)
import ApexChart from 'vue3-apexcharts'
const showStudentModal = ref(false)
const showStudentProgressModal = ref(false)
const selectedStudent = ref(null)
const chartData = ref(null)
const chartOptions = ref(null)
const props = defineProps({
batch: {
@@ -127,6 +178,10 @@ const students = createResource({
batch: props.batch,
},
auto: true,
onSuccess(data) {
chartData.value = getChartData()
console.log(chartData.value)
},
})
const getStudentColumns = () => {
@@ -134,36 +189,24 @@ const getStudentColumns = () => {
{
label: 'Full Name',
key: 'full_name',
width: '20rem',
icon: 'user',
},
{
label: 'Progress',
key: 'progress',
width: '10rem',
icon: 'activity',
},
{
label: 'Last Active',
key: 'last_active',
width: '15rem',
align: 'center',
icon: 'clock',
},
]
if (students.data?.[0].assessments) {
Object.keys(students.data?.[0].assessments).forEach((assessment) => {
columns.push({
label: assessment,
key: assessment,
width: '10rem',
icon: 'help-circle',
align: isAssignment(students.data?.[0].assessments[assessment])
? 'left'
: 'center',
})
})
}
if (students.data?.[0].courses) {
Object.keys(students.data?.[0].courses).forEach((course) => {
columns.push({
label: course,
key: course,
width: '10rem',
icon: 'book-open',
align: 'center',
})
})
}
return columns
}
@@ -171,6 +214,11 @@ const openStudentModal = () => {
showStudentModal.value = true
}
const openStudentProgressModal = (row) => {
showStudentProgressModal.value = true
selectedStudent.value = row
}
const deleteStudents = createResource({
url: 'lms.lms.api.delete_documents',
makeParams(values) {
@@ -196,17 +244,141 @@ const removeStudents = (selections, unselectAll) => {
)
}
const getStatusTheme = (status) => {
if (status === 'Pass') {
return 'green'
} else if (status == 'Not Graded') {
return 'orange'
} else {
return 'red'
}
const getChartData = () => {
console.log('called')
let categories = {}
// Initialize categories with categories
Object.keys(students.data?.[0].courses).forEach((course) => {
categories[course] = {
value: 0,
type: 'course',
}
})
Object.keys(students.data?.[0].assessments).forEach((assessment) => {
categories[assessment] = {
value: 0,
type: 'assessment',
}
})
// Populate data
students.data.forEach((student) => {
Object.keys(student.courses).forEach((course) => {
if (student.courses[course] === 100) {
categories[course].value += 1
}
})
Object.keys(student.assessments).forEach((assessment) => {
if (student.assessments[assessment] === 100) {
categories[assessment].value += 1
}
})
})
// Transform data for ApexCharts
console.log(Object.values(categories).map((item) => item.value))
chartOptions.value = getChartOptions(categories)
return [
{
name: __('Student Progress'),
data: Object.values(categories).map((item) => item.value),
/* colors: Object.values(categories).map(item =>
item.type === 'course' ? courseColor : assessmentColor
), */
},
]
}
const isAssignment = (value) => {
return isNaN(value)
/* const chartOptions = computed(() => {
return {
responsive: true,
fill: true,
scales: {
x: {
ticks: {
maxRotation: 0,
minRotation: 0,
autoSkip: false,
}
},
y: {
beginAtZero: true,
max: students.data?.length,
ticks: {
stepSize: 5,
},
},
},
plugins: {
legends: {
display: false,
title: {
text: __("Student Progress 1111"),
}
},
title: {
display: true,
text: __("Student Progress"),
font: {
size: 14,
weight: '500',
},
color: '#171717',
}
}
}
}) */
const chartSeries = ref([
{
name: 'Courses',
data: [20, 30, 50], // Example data for courses
},
{
name: 'Assessments',
data: [10, 40, 60], // Example data for assessments
},
])
const getChartOptions = (categories) => {
const courseColor = '#3498db' // Blue for courses
const assessmentColor = '#e74c3c' // Red for assessments
return {
chart: {
type: 'bar',
height: 350,
},
plotOptions: {
bar: {
distributed: true, // Allows individual bar colors
borderRadius: 0,
horizontal: false, // Set to true for horizontal bars
columnWidth: '30%',
},
},
colors: Object.values(categories).map((item) =>
item.type === 'course' ? courseColor : assessmentColor
),
legends: {
show: true,
},
xaxis: {
categories: Object.keys(categories),
labels: {
style: {
fontSize: '10px',
},
rotate: 0,
formatter: function (value) {
console.log(value)
return value.length > 20 ? `${value.substring(0, 20)}...` : value // Trim long labels
},
},
},
}
}
</script>

View File

@@ -0,0 +1,113 @@
<template>
<Dialog v-model="show" :options="{}">
<template #body>
<div class="p-5 space-y-8">
<div class="flex items-center space-x-2">
<Avatar :image="student.user_image" size="3xl" />
<div class="space-y-1">
<div class="flex items-center space-x-2">
<div class="text-xl font-semibold">
{{ student.full_name }}
</div>
<Badge :theme="student.progress === 100 ? 'green' : 'red'">
{{ student.progress }}% {{ __('Complete') }}
</Badge>
</div>
<div class="text-sm text-gray-700">
{{ student.email }}
</div>
</div>
</div>
<!-- Assessments -->
<div>
<div>
<div
class="grid grid-cols-[70%,30%] border-b pl-2 pb-1 mb-2 text-xs text-gray-700 font-medium"
>
<span>
{{ __('Assessment') }}
</span>
<span>
{{ __('Progress') }}
</span>
</div>
<div
v-for="assessment in Object.keys(student.assessments)"
class="grid grid-cols-[70%,30%] pl-2 mb-2 text-gray-700 font-medium"
>
<span>
{{ assessment }}
</span>
<span v-if="isAssignment(student.assessments[assessment])">
<Badge :theme="getStatusTheme(student.assessments[assessment])">
{{ student.assessments[assessment] }}
</Badge>
</span>
<span v-else>
{{ student.assessments[assessment] }}
</span>
</div>
</div>
</div>
<!-- Courses -->
<div>
<div>
<div
class="grid grid-cols-[70%,30%] mb-2 text-xs text-gray-700 border-b pl-2 pb-1 font-medium"
>
<span>
{{ __('Courses') }}
</span>
<span>
{{ __('Progress') }}
</span>
</div>
<div
v-for="course in Object.keys(student.courses)"
class="grid grid-cols-[70%,30%] pl-2 mb-2 text-gray-700 font-medium"
>
<span>
{{ course }}
</span>
<span>
{{ Math.floor(student.courses[course]) }}
</span>
</div>
</div>
</div>
<!-- <span class="mt-4">
{{ student }}
</span> -->
</div>
</template>
</Dialog>
</template>
<script setup>
import { Avatar, Badge, Dialog } from 'frappe-ui'
import ProgressBar from '@/components/ProgressBar.vue'
const show = defineModel()
const props = defineProps({
student: {
type: Object,
default: null,
},
})
const isAssignment = (value) => {
return isNaN(value)
}
const getStatusTheme = (status) => {
if (status === 'Pass') {
return 'green'
} else if (status == 'Not Graded') {
return 'orange'
} else {
return 'red'
}
}
</script>

View File

@@ -1,24 +1,44 @@
<template>
<div class="w-full bg-gray-200 rounded-full h-1 my-2">
<div
class="bg-gray-900 h-1 rounded-full"
:style="{ width: progressBarWidth }"
></div>
</div>
<Tooltip :text="`${props.progress}%`">
<div class="w-full bg-gray-200 rounded-full h-1 my-2">
<div
class="bg-gray-900 rounded-full"
:class="progressBarHeight"
:style="{ width: progressBarWidth }"
></div>
</div>
</Tooltip>
</template>
<script setup>
import { computed } from 'vue'
import { Tooltip } from 'frappe-ui'
const props = defineProps({
progress: {
type: Number,
default: 0,
},
size: {
type: String,
default: 'sm',
},
})
const progressBarWidth = computed(() => {
const formattedPercentage = Math.min(Math.ceil(props.progress), 100)
return `${formattedPercentage}%`
})
const progressBarHeight = computed(() => {
if (props.size === 'sm') {
return 'h-1'
}
if (props.size === 'md') {
return 'h-2'
}
if (props.size === 'lg') {
return 'h-3'
}
})
</script>