chore: linters
This commit is contained in:
@@ -88,10 +88,15 @@
|
||||
</template>
|
||||
{{ __('Get Certificate') }}
|
||||
</Button>
|
||||
<Button v-if="user.data?.is_moderator || is_instructor()" class="w-full mt-2" size="md" @click="showProgressSummary">
|
||||
<Button
|
||||
v-if="user.data?.is_moderator || is_instructor()"
|
||||
class="w-full mt-2"
|
||||
size="md"
|
||||
@click="showProgressSummary"
|
||||
>
|
||||
<template #prefix>
|
||||
<TrendingUp class="size-4 stroke-1.5" />
|
||||
{{ __("Progress Summary") }}
|
||||
{{ __('Progress Summary') }}
|
||||
</template>
|
||||
</Button>
|
||||
<router-link
|
||||
@@ -170,7 +175,16 @@
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { BookOpen, BookText, CreditCard, GraduationCap, Pencil, Star, TrendingUp, Users } from 'lucide-vue-next'
|
||||
import {
|
||||
BookOpen,
|
||||
BookText,
|
||||
CreditCard,
|
||||
GraduationCap,
|
||||
Pencil,
|
||||
Star,
|
||||
TrendingUp,
|
||||
Users,
|
||||
} from 'lucide-vue-next'
|
||||
import { computed, inject, ref } from 'vue'
|
||||
import { Badge, Button, call, createResource, toast } from 'frappe-ui'
|
||||
import { formatAmount } from '@/utils/'
|
||||
|
||||
@@ -1,182 +1,221 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
title: __('Course Progress Summary'),
|
||||
size: '5xl',
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="flex justify-between space-x-10 text-base">
|
||||
|
||||
<div class="w-full ">
|
||||
<div class="flex items-center justify-between space-x-5 mb-4">
|
||||
<div class="text-xl font-semibold text-ink-gray-6">
|
||||
{{ __("{0} Members").format(memberCount) }}
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="searchFilter"
|
||||
:label="__('Search by Member Name')"
|
||||
type="text"
|
||||
class="w-1/2"
|
||||
/>
|
||||
</div>
|
||||
<div class="max-h-[70vh] overflow-y-auto">
|
||||
<ListView
|
||||
v-if="progressList.loading || progressList.data?.length"
|
||||
:columns="progressColumns"
|
||||
:rows="progressList.data"
|
||||
rowKey="name"
|
||||
:options="{
|
||||
selectable: false,
|
||||
showTooltip: false,
|
||||
}"
|
||||
>
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
||||
>
|
||||
<ListHeaderItem
|
||||
:item="item"
|
||||
v-for="item in progressColumns"
|
||||
:key="item.key"
|
||||
>
|
||||
<template #prefix="{ item }">
|
||||
<FeatherIcon :name="item.icon?.toString()" class="h-4 w-4" />
|
||||
</template>
|
||||
</ListHeaderItem>
|
||||
</ListHeader>
|
||||
<ListRows v-for="row in progressList.data">
|
||||
<router-link :to="{
|
||||
name: 'Profile',
|
||||
params: { username: row.member_username },
|
||||
}">
|
||||
<ListRow :row="row">
|
||||
<template #default="{ column, item }">
|
||||
<ListRowItem :item="row[column.key]" :align="column.align">
|
||||
<template #prefix>
|
||||
<div v-if="column.key == 'member_name'">
|
||||
<Avatar
|
||||
class="flex items-center"
|
||||
:image="row['member_image']"
|
||||
:label="item"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
{{ row[column.key].toString() }}
|
||||
</div>
|
||||
</ListRowItem>
|
||||
</template>
|
||||
</ListRow>
|
||||
</router-link>
|
||||
</ListRows>
|
||||
</ListView>
|
||||
<div
|
||||
v-if="progressList.data && progressList.hasNextPage"
|
||||
class="flex justify-center my-5"
|
||||
>
|
||||
<Button @click="progressList.next()">
|
||||
{{ __('Load More') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4 self-start w-full space-y-5">
|
||||
<NumberChart
|
||||
class="border rounded-md"
|
||||
:config="{
|
||||
title: __('Average Progress %'),
|
||||
value: chartDetails.data?.average_progress || 0,
|
||||
}"
|
||||
/>
|
||||
<DonutChart
|
||||
:config="{
|
||||
data: chartDetails.data?.progress_distribution || [],
|
||||
title: __('Progress Distribution'),
|
||||
categoryColumn: 'category',
|
||||
valueColumn: 'count',
|
||||
colors: [theme.colors.red['400'], theme.colors.amber['400'], theme.colors.pink['400'], theme.colors.blue['400'], theme.colors.green['400']],
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
title: __('Course Progress Summary'),
|
||||
size: '5xl',
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="flex justify-between space-x-10 text-base">
|
||||
<div class="w-full">
|
||||
<div class="flex items-center justify-between space-x-5 mb-4">
|
||||
<div class="text-xl font-semibold text-ink-gray-6">
|
||||
{{ __('{0} Members').format(memberCount) }}
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="searchFilter"
|
||||
:label="__('Search by Member Name')"
|
||||
type="text"
|
||||
class="w-1/2"
|
||||
/>
|
||||
</div>
|
||||
<div class="max-h-[70vh] overflow-y-auto">
|
||||
<ListView
|
||||
v-if="progressList.loading || progressList.data?.length"
|
||||
:columns="progressColumns"
|
||||
:rows="progressList.data"
|
||||
rowKey="name"
|
||||
:options="{
|
||||
selectable: false,
|
||||
showTooltip: false,
|
||||
}"
|
||||
>
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
||||
>
|
||||
<ListHeaderItem
|
||||
:item="item"
|
||||
v-for="item in progressColumns"
|
||||
:key="item.key"
|
||||
>
|
||||
<template #prefix="{ item }">
|
||||
<FeatherIcon
|
||||
:name="item.icon?.toString()"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</template>
|
||||
</ListHeaderItem>
|
||||
</ListHeader>
|
||||
<ListRows v-for="row in progressList.data">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'Profile',
|
||||
params: { username: row.member_username },
|
||||
}"
|
||||
>
|
||||
<ListRow :row="row">
|
||||
<template #default="{ column, item }">
|
||||
<ListRowItem
|
||||
:item="row[column.key]"
|
||||
:align="column.align"
|
||||
>
|
||||
<template #prefix>
|
||||
<div v-if="column.key == 'member_name'">
|
||||
<Avatar
|
||||
class="flex items-center"
|
||||
:image="row['member_image']"
|
||||
:label="item"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
{{ row[column.key].toString() }}
|
||||
</div>
|
||||
</ListRowItem>
|
||||
</template>
|
||||
</ListRow>
|
||||
</router-link>
|
||||
</ListRows>
|
||||
</ListView>
|
||||
<div
|
||||
v-if="progressList.data && progressList.hasNextPage"
|
||||
class="flex justify-center my-5"
|
||||
>
|
||||
<Button @click="progressList.next()">
|
||||
{{ __('Load More') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4 self-start w-full space-y-5">
|
||||
<NumberChart
|
||||
class="border rounded-md"
|
||||
:config="{
|
||||
title: __('Average Progress %'),
|
||||
value: chartDetails.data?.average_progress || 0,
|
||||
}"
|
||||
/>
|
||||
<DonutChart
|
||||
:config="{
|
||||
data: chartDetails.data?.progress_distribution || [],
|
||||
title: __('Progress Distribution'),
|
||||
categoryColumn: 'category',
|
||||
valueColumn: 'count',
|
||||
colors: [
|
||||
theme.colors.red['400'],
|
||||
theme.colors.amber['400'],
|
||||
theme.colors.pink['400'],
|
||||
theme.colors.blue['400'],
|
||||
theme.colors.green['400'],
|
||||
],
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { Avatar, Button, createListResource, createResource, Dialog, DonutChart, FeatherIcon, FormControl, ListView, ListHeader, ListHeaderItem, ListRows, ListRow, ListRowItem, NumberChart } from 'frappe-ui';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
createListResource,
|
||||
createResource,
|
||||
Dialog,
|
||||
DonutChart,
|
||||
FeatherIcon,
|
||||
FormControl,
|
||||
ListView,
|
||||
ListHeader,
|
||||
ListHeaderItem,
|
||||
ListRows,
|
||||
ListRow,
|
||||
ListRowItem,
|
||||
NumberChart,
|
||||
} from 'frappe-ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { theme } from '@/utils/theme'
|
||||
|
||||
const show = defineModel<boolean | undefined>()
|
||||
const searchFilter = ref<string | null>(null);
|
||||
const searchFilter = ref<string | null>(null)
|
||||
|
||||
const props = defineProps<{
|
||||
courseName?: string;
|
||||
enrollments?: number;
|
||||
}>();
|
||||
courseName?: string
|
||||
enrollments?: number
|
||||
}>()
|
||||
|
||||
const memberCount = ref<number>(props.enrollments || 0);
|
||||
const memberCount = ref<number>(props.enrollments || 0)
|
||||
|
||||
const chartDetails = createResource({
|
||||
url: "lms.lms.api.get_course_progress_distribution",
|
||||
params: {
|
||||
course: props.courseName,
|
||||
},
|
||||
auto: true,
|
||||
url: 'lms.lms.api.get_course_progress_distribution',
|
||||
params: {
|
||||
course: props.courseName,
|
||||
},
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const progressList = createListResource({
|
||||
doctype: "LMS Enrollment",
|
||||
filters: {
|
||||
course: props.courseName,
|
||||
},
|
||||
fields: ["name", "member", "member_name", "member_image", "member_username", "progress"],
|
||||
pageLength: 50,
|
||||
auto: true,
|
||||
doctype: 'LMS Enrollment',
|
||||
filters: {
|
||||
course: props.courseName,
|
||||
},
|
||||
fields: [
|
||||
'name',
|
||||
'member',
|
||||
'member_name',
|
||||
'member_image',
|
||||
'member_username',
|
||||
'progress',
|
||||
],
|
||||
pageLength: 50,
|
||||
auto: true,
|
||||
})
|
||||
|
||||
watch([searchFilter], () => {
|
||||
let filterApplied = false;
|
||||
type Filters = {
|
||||
course: string | undefined;
|
||||
member_name?: string[];
|
||||
}
|
||||
let filters: Filters = {
|
||||
course: props.courseName,
|
||||
}
|
||||
let filterApplied = false
|
||||
type Filters = {
|
||||
course: string | undefined
|
||||
member_name?: string[]
|
||||
}
|
||||
let filters: Filters = {
|
||||
course: props.courseName,
|
||||
}
|
||||
|
||||
if (searchFilter.value) {
|
||||
filters.member_name = ["like", `%${searchFilter.value}%`];
|
||||
filterApplied = true;
|
||||
}
|
||||
if (searchFilter.value) {
|
||||
filters.member_name = ['like', `%${searchFilter.value}%`]
|
||||
filterApplied = true
|
||||
}
|
||||
|
||||
progressList.update({
|
||||
filters: filters,
|
||||
})
|
||||
progressList.reload({}, {
|
||||
onSuccess(data: any[]) {
|
||||
memberCount.value = filterApplied ? data.length : props.enrollments || 0;
|
||||
}
|
||||
});
|
||||
});
|
||||
progressList.update({
|
||||
filters: filters,
|
||||
})
|
||||
progressList.reload(
|
||||
{},
|
||||
{
|
||||
onSuccess(data: any[]) {
|
||||
memberCount.value = filterApplied ? data.length : props.enrollments || 0
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const progressColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
return [
|
||||
{
|
||||
label: __('Member'),
|
||||
key: 'member_name',
|
||||
width: '50%',
|
||||
icon: 'user',
|
||||
},
|
||||
{
|
||||
label: __('Progress'),
|
||||
key: 'progress',
|
||||
width: '30%',
|
||||
align: 'right',
|
||||
icon: 'trending-up',
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: __('Progress'),
|
||||
key: 'progress',
|
||||
width: '30%',
|
||||
align: 'right',
|
||||
icon: 'trending-up',
|
||||
},
|
||||
]
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1593,11 +1593,16 @@ def track_new_watch_time(lesson, video):
|
||||
doc.member = frappe.session.user
|
||||
doc.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_course_progress_distribution(course):
|
||||
all_progress = frappe.get_all("LMS Enrollment", {
|
||||
"course": course,
|
||||
}, pluck="progress")
|
||||
all_progress = frappe.get_all(
|
||||
"LMS Enrollment",
|
||||
{
|
||||
"course": course,
|
||||
},
|
||||
pluck="progress",
|
||||
)
|
||||
|
||||
average_progress = get_average_course_progress(all_progress)
|
||||
progress_distribution = get_progress_distribution(all_progress)
|
||||
@@ -1614,22 +1619,29 @@ def get_average_course_progress(progress_list):
|
||||
average_progress = sum(progress_list) / len(progress_list)
|
||||
return flt(average_progress, frappe.get_system_settings("float_precision") or 3)
|
||||
|
||||
def get_progress_distribution(progressList):
|
||||
distribution = [{
|
||||
"category": "0-20%",
|
||||
"count": len([p for p in progressList if 0 <= p < 20]),
|
||||
}, {
|
||||
"category": "20-40%",
|
||||
"count": len([p for p in progressList if 20 <= p < 40]),
|
||||
}, {
|
||||
"category": "40-60%",
|
||||
"count": len([p for p in progressList if 40 <= p < 60]),
|
||||
}, {
|
||||
"category": "60-80%",
|
||||
"count": len([p for p in progressList if 60 <= p < 80]),
|
||||
}, {
|
||||
"category": "80-100%",
|
||||
"count": len([p for p in progressList if 80 <= p <= 100]),
|
||||
}]
|
||||
|
||||
return distribution
|
||||
def get_progress_distribution(progressList):
|
||||
distribution = [
|
||||
{
|
||||
"category": "0-20%",
|
||||
"count": len([p for p in progressList if 0 <= p < 20]),
|
||||
},
|
||||
{
|
||||
"category": "20-40%",
|
||||
"count": len([p for p in progressList if 20 <= p < 40]),
|
||||
},
|
||||
{
|
||||
"category": "40-60%",
|
||||
"count": len([p for p in progressList if 40 <= p < 60]),
|
||||
},
|
||||
{
|
||||
"category": "60-80%",
|
||||
"count": len([p for p in progressList if 60 <= p < 80]),
|
||||
},
|
||||
{
|
||||
"category": "80-100%",
|
||||
"count": len([p for p in progressList if 80 <= p <= 100]),
|
||||
},
|
||||
]
|
||||
|
||||
return distribution
|
||||
|
||||
Reference in New Issue
Block a user