feat: batch student progress modal
This commit is contained in:
@@ -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>
|
||||
|
||||
113
frontend/src/components/Modals/BatchStudentProgress.vue
Normal file
113
frontend/src/components/Modals/BatchStudentProgress.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user