chore: linters

This commit is contained in:
Jannat Patel
2025-07-03 13:09:45 +05:30
parent 85da4f6d85
commit 991ebe09a2
3 changed files with 245 additions and 180 deletions

View File

@@ -88,10 +88,15 @@
</template> </template>
{{ __('Get Certificate') }} {{ __('Get Certificate') }}
</Button> </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> <template #prefix>
<TrendingUp class="size-4 stroke-1.5" /> <TrendingUp class="size-4 stroke-1.5" />
{{ __("Progress Summary") }} {{ __('Progress Summary') }}
</template> </template>
</Button> </Button>
<router-link <router-link
@@ -170,7 +175,16 @@
/> />
</template> </template>
<script setup> <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 { computed, inject, ref } from 'vue'
import { Badge, Button, call, createResource, toast } from 'frappe-ui' import { Badge, Button, call, createResource, toast } from 'frappe-ui'
import { formatAmount } from '@/utils/' import { formatAmount } from '@/utils/'

View File

@@ -1,182 +1,221 @@
<template> <template>
<Dialog <Dialog
v-model="show" v-model="show"
:options="{ :options="{
title: __('Course Progress Summary'), title: __('Course Progress Summary'),
size: '5xl', size: '5xl',
}" }"
> >
<template #body-content> <template #body-content>
<div class="flex justify-between space-x-10 text-base"> <div class="flex justify-between space-x-10 text-base">
<div class="w-full">
<div class="w-full "> <div class="flex items-center justify-between space-x-5 mb-4">
<div class="flex items-center justify-between space-x-5 mb-4"> <div class="text-xl font-semibold text-ink-gray-6">
<div class="text-xl font-semibold text-ink-gray-6"> {{ __('{0} Members').format(memberCount) }}
{{ __("{0} Members").format(memberCount) }} </div>
</div> <FormControl
<FormControl v-model="searchFilter"
v-model="searchFilter" :label="__('Search by Member Name')"
:label="__('Search by Member Name')" type="text"
type="text" class="w-1/2"
class="w-1/2" />
/> </div>
</div> <div class="max-h-[70vh] overflow-y-auto">
<div class="max-h-[70vh] overflow-y-auto"> <ListView
<ListView v-if="progressList.loading || progressList.data?.length"
v-if="progressList.loading || progressList.data?.length" :columns="progressColumns"
:columns="progressColumns" :rows="progressList.data"
:rows="progressList.data" rowKey="name"
rowKey="name" :options="{
:options="{ selectable: false,
selectable: false, showTooltip: false,
showTooltip: false, }"
}" >
> <ListHeader
<ListHeader class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2" >
> <ListHeaderItem
<ListHeaderItem :item="item"
:item="item" v-for="item in progressColumns"
v-for="item in progressColumns" :key="item.key"
:key="item.key" >
> <template #prefix="{ item }">
<template #prefix="{ item }"> <FeatherIcon
<FeatherIcon :name="item.icon?.toString()" class="h-4 w-4" /> :name="item.icon?.toString()"
</template> class="h-4 w-4"
</ListHeaderItem> />
</ListHeader> </template>
<ListRows v-for="row in progressList.data"> </ListHeaderItem>
<router-link :to="{ </ListHeader>
name: 'Profile', <ListRows v-for="row in progressList.data">
params: { username: row.member_username }, <router-link
}"> :to="{
<ListRow :row="row"> name: 'Profile',
<template #default="{ column, item }"> params: { username: row.member_username },
<ListRowItem :item="row[column.key]" :align="column.align"> }"
<template #prefix> >
<div v-if="column.key == 'member_name'"> <ListRow :row="row">
<Avatar <template #default="{ column, item }">
class="flex items-center" <ListRowItem
:image="row['member_image']" :item="row[column.key]"
:label="item" :align="column.align"
size="sm" >
/> <template #prefix>
</div> <div v-if="column.key == 'member_name'">
</template> <Avatar
<div> class="flex items-center"
{{ row[column.key].toString() }} :image="row['member_image']"
</div> :label="item"
</ListRowItem> size="sm"
</template> />
</ListRow> </div>
</router-link> </template>
</ListRows> <div>
</ListView> {{ row[column.key].toString() }}
<div </div>
v-if="progressList.data && progressList.hasNextPage" </ListRowItem>
class="flex justify-center my-5" </template>
> </ListRow>
<Button @click="progressList.next()"> </router-link>
{{ __('Load More') }} </ListRows>
</Button> </ListView>
</div> <div
</div> v-if="progressList.data && progressList.hasNextPage"
</div> class="flex justify-center my-5"
<div class="mb-4 self-start w-full space-y-5"> >
<NumberChart <Button @click="progressList.next()">
class="border rounded-md" {{ __('Load More') }}
:config="{ </Button>
title: __('Average Progress %'), </div>
value: chartDetails.data?.average_progress || 0, </div>
}" </div>
/> <div class="mb-4 self-start w-full space-y-5">
<DonutChart <NumberChart
:config="{ class="border rounded-md"
data: chartDetails.data?.progress_distribution || [], :config="{
title: __('Progress Distribution'), title: __('Average Progress %'),
categoryColumn: 'category', value: chartDetails.data?.average_progress || 0,
valueColumn: 'count', }"
colors: [theme.colors.red['400'], theme.colors.amber['400'], theme.colors.pink['400'], theme.colors.blue['400'], theme.colors.green['400']], />
}" <DonutChart
/> :config="{
</div> data: chartDetails.data?.progress_distribution || [],
</div> title: __('Progress Distribution'),
</template> categoryColumn: 'category',
</Dialog> 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> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Avatar, Button, createListResource, createResource, Dialog, DonutChart, FeatherIcon, FormControl, ListView, ListHeader, ListHeaderItem, ListRows, ListRow, ListRowItem, NumberChart } from 'frappe-ui'; import {
import { computed, ref, watch } from 'vue'; 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' import { theme } from '@/utils/theme'
const show = defineModel<boolean | undefined>() const show = defineModel<boolean | undefined>()
const searchFilter = ref<string | null>(null); const searchFilter = ref<string | null>(null)
const props = defineProps<{ const props = defineProps<{
courseName?: string; courseName?: string
enrollments?: number; enrollments?: number
}>(); }>()
const memberCount = ref<number>(props.enrollments || 0); const memberCount = ref<number>(props.enrollments || 0)
const chartDetails = createResource({ const chartDetails = createResource({
url: "lms.lms.api.get_course_progress_distribution", url: 'lms.lms.api.get_course_progress_distribution',
params: { params: {
course: props.courseName, course: props.courseName,
}, },
auto: true, auto: true,
}) })
const progressList = createListResource({ const progressList = createListResource({
doctype: "LMS Enrollment", doctype: 'LMS Enrollment',
filters: { filters: {
course: props.courseName, course: props.courseName,
}, },
fields: ["name", "member", "member_name", "member_image", "member_username", "progress"], fields: [
pageLength: 50, 'name',
auto: true, 'member',
'member_name',
'member_image',
'member_username',
'progress',
],
pageLength: 50,
auto: true,
}) })
watch([searchFilter], () => { watch([searchFilter], () => {
let filterApplied = false; let filterApplied = false
type Filters = { type Filters = {
course: string | undefined; course: string | undefined
member_name?: string[]; member_name?: string[]
} }
let filters: Filters = { let filters: Filters = {
course: props.courseName, course: props.courseName,
} }
if (searchFilter.value) { if (searchFilter.value) {
filters.member_name = ["like", `%${searchFilter.value}%`]; filters.member_name = ['like', `%${searchFilter.value}%`]
filterApplied = true; filterApplied = true
} }
progressList.update({ progressList.update({
filters: filters, filters: filters,
}) })
progressList.reload({}, { progressList.reload(
onSuccess(data: any[]) { {},
memberCount.value = filterApplied ? data.length : props.enrollments || 0; {
} onSuccess(data: any[]) {
}); memberCount.value = filterApplied ? data.length : props.enrollments || 0
}); },
}
)
})
const progressColumns = computed(() => { const progressColumns = computed(() => {
return [ return [
{ {
label: __('Member'), label: __('Member'),
key: 'member_name', key: 'member_name',
width: '50%', width: '50%',
icon: 'user', icon: 'user',
}, },
{ {
label: __('Progress'), label: __('Progress'),
key: 'progress', key: 'progress',
width: '30%', width: '30%',
align: 'right', align: 'right',
icon: 'trending-up', icon: 'trending-up',
} },
] ]
}) })
</script> </script>

View File

@@ -1593,11 +1593,16 @@ def track_new_watch_time(lesson, video):
doc.member = frappe.session.user doc.member = frappe.session.user
doc.save() doc.save()
@frappe.whitelist() @frappe.whitelist()
def get_course_progress_distribution(course): def get_course_progress_distribution(course):
all_progress = frappe.get_all("LMS Enrollment", { all_progress = frappe.get_all(
"course": course, "LMS Enrollment",
}, pluck="progress") {
"course": course,
},
pluck="progress",
)
average_progress = get_average_course_progress(all_progress) average_progress = get_average_course_progress(all_progress)
progress_distribution = get_progress_distribution(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) average_progress = sum(progress_list) / len(progress_list)
return flt(average_progress, frappe.get_system_settings("float_precision") or 3) 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