Merge pull request #1252 from pateljannat/refactor-certified-participants-page

refactor: improved ui and performance for certified participants page
This commit is contained in:
Jannat Patel
2025-01-16 17:01:28 +05:30
committed by GitHub
3 changed files with 192 additions and 85 deletions

View File

@@ -68,8 +68,8 @@
v-else-if="!batches.list.loading" v-else-if="!batches.list.loading"
class="flex flex-col items-center justify-center text-sm text-gray-600 italic mt-48" class="flex flex-col items-center justify-center text-sm text-gray-600 italic mt-48"
> >
<BookOpen class="size-10 mx-auto stroke-1.5 text-gray-500" /> <BookOpen class="size-10 mx-auto stroke-1 text-gray-500" />
<div class="text-xl font-medium mb-2"> <div class="text-lg font-medium mb-1">
{{ __('No batches found') }} {{ __('No batches found') }}
</div> </div>
<div class="leading-5 w-2/5 text-center"> <div class="leading-5 w-2/5 text-center">

View File

@@ -1,93 +1,175 @@
<template> <template>
<header <header
class="sticky top-0 z-10 flex items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5" class="sticky flex items-center justify-between top-0 z-10 border-b bg-white px-3 py-2.5 sm:px-5"
> >
<Breadcrumbs :items="breadcrumbs" /> <Breadcrumbs :items="breadcrumbs" />
<div>
<FormControl
type="text"
placeholder="Search"
v-model="searchQuery"
@input="participants.reload()"
class="w-40"
>
<template #prefix>
<Search class="w-4 stroke-1.5 text-gray-600" name="search" />
</template>
</FormControl>
</div>
</header> </header>
<div class="p-5 lg:w-3/4 mx-auto">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 m-5">
<div <div
v-if="participants.data?.length" class="flex flex-col lg:flex-row lg:items-center space-y-4 lg:space-y-0 justify-between mb-5"
v-for="participant in participantsList"
> >
<router-link <div class="text-lg font-semibold">
:to="{ {{ __('All Certified Participants') }}
name: 'Profile', </div>
params: { username: participant.username }, <div class="grid grid-cols-2 gap-2">
}" <FormControl
> v-model="nameFilter"
<div class="flex shadow rounded-md h-full p-2"> :placeholder="__('Search by Name')"
<UserAvatar :user="participant" size="3xl" class="mr-2" /> type="text"
<div> class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
<router-link @input="updateParticipants()"
:to="{ />
name: 'Profile', <div
params: { username: participant.username }, v-if="categories.data?.length"
}" class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
> >
<div class="text-lg font-semibold mb-2"> <Select
v-model="currentCategory"
:options="categories.data"
:placeholder="__('Category')"
@change="updateParticipants()"
/>
</div>
</div>
</div>
<div v-if="participants.data?.length">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
<router-link
v-for="participant in participants.data"
:to="{
name: 'ProfileCertificates',
params: { username: participant.username },
}"
>
<div
class="flex items-center space-x-2 border rounded-md hover:bg-gray-50 p-2"
>
<Avatar
:image="participant.user_image"
:label="participant.full_name"
size="2xl"
/>
<div class="flex flex-col space-y-2">
<div class="font-medium">
{{ participant.full_name }} {{ participant.full_name }}
</div> </div>
</router-link> <div
<div class="leading-5" v-for="course in participant.courses"> v-if="participant.headline"
{{ course }} class="headline text-sm text-gray-700"
>
{{ participant.headline }}
</div>
</div> </div>
</div> </div>
</div> </router-link>
</router-link> </div>
<div
v-if="!participants.list.loading && participants.hasNextPage"
class="flex justify-center mt-5"
>
<Button @click="participants.next()">
{{ __('Load More') }}
</Button>
</div>
</div>
<div
v-else-if="!participants.list.loading"
class="flex flex-col items-center justify-center text-sm text-gray-600 italic mt-48"
>
<BookOpen class="size-10 mx-auto stroke-1 text-gray-500" />
<div class="text-lg font-medium mb-1">
{{ __('No participants found') }}
</div>
<div class="leading-5 w-2/5 text-center">
{{ __('There are no participants matching this criteria.') }}
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { Breadcrumbs, FormControl, createResource } from 'frappe-ui' import {
import { ref, computed } from 'vue' Avatar,
import UserAvatar from '@/components/UserAvatar.vue' Breadcrumbs,
import { Search } from 'lucide-vue-next' Button,
createListResource,
FormControl,
Select,
} from 'frappe-ui'
import { computed, onMounted, ref } from 'vue'
import { updateDocumentTitle } from '@/utils' import { updateDocumentTitle } from '@/utils'
import { BookOpen } from 'lucide-vue-next'
const searchQuery = ref('') const currentCategory = ref('')
const filters = ref({})
const nameFilter = ref('')
const participants = createResource({ onMounted(() => {
updateParticipants()
})
const participants = createListResource({
doctype: 'LMS Certificate',
url: 'lms.lms.api.get_certified_participants', url: 'lms.lms.api.get_certified_participants',
method: 'GET', cache: ['certified_participants'],
cache: 'certified-participants', start: 0,
auto: true, pageLength: 30,
}) })
const breadcrumbs = computed(() => { const categories = createListResource({
return [{ label: 'Certified Participants', to: '/certified-participants' }] doctype: 'LMS Certificate',
url: 'lms.lms.api.get_certification_categories',
cache: ['certification_categories'],
auto: true,
transform(data) {
data.unshift({ label: __(''), value: '' })
return data
},
}) })
const updateParticipants = () => {
updateFilters()
participants.update({
filters: filters.value,
})
participants.reload()
}
const updateFilters = () => {
if (currentCategory.value) {
filters.value.category = currentCategory.value
} else {
delete filters.value.category
}
if (nameFilter.value) {
filters.value.member_name = ['like', `%${nameFilter.value}%`]
} else {
delete filters.value.member_name
}
}
const breadcrumbs = computed(() => [
{
label: __('Certified Participants'),
route: { name: 'CertifiedParticipants' },
},
])
const pageMeta = computed(() => { const pageMeta = computed(() => {
return { return {
title: 'Certified Participants', title: 'Certified Participants',
description: 'All participants that have been certified.', description: 'All participants that have been certified.',
} }
}) })
const participantsList = computed(() => {
if (searchQuery.value) {
return participants.data.filter((participant) => {
return participant.full_name
.toLowerCase()
.includes(searchQuery.value.toLowerCase())
})
}
return participants.data
})
updateDocumentTitle(pageMeta) updateDocumentTitle(pageMeta)
</script> </script>
<style>
.headline {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
width: 100%;
overflow: hidden;
}
</style>

View File

@@ -361,34 +361,59 @@ def get_evaluator_details(evaluator):
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def get_certified_participants(): def get_certified_participants(filters=None, start=0, page_length=30, search=None):
LMSCertificate = DocType("LMS Certificate") or_filters = {}
participants = ( if not filters:
frappe.qb.from_(LMSCertificate) filters = {}
.select(LMSCertificate.member)
.distinct() filters.update({"published": 1})
.where(LMSCertificate.published == 1)
.orderby(LMSCertificate.creation, order=frappe.qb.desc) category = filters.get("category")
.run(as_dict=1) if category:
del filters["category"]
or_filters["course_title"] = ["like", f"%{category}%"]
or_filters["batch_title"] = ["like", f"%{category}%"]
participants = frappe.get_all(
"LMS Certificate",
filters=filters,
or_filters=or_filters,
fields=["member"],
group_by="member",
order_by="creation desc",
start=start,
page_length=page_length,
) )
participant_details = []
for participant in participants: for participant in participants:
details = frappe.db.get_value( details = frappe.db.get_value(
"User", "User",
participant.member, participant.member,
["name", "full_name", "username", "user_image"], ["full_name", "user_image", "username", "country", "headline"],
as_dict=True, as_dict=1,
) )
course_names = frappe.get_all( participant.update(details)
"LMS Certificate", {"member": participant.member}, pluck="course"
) return participants
courses = []
for course in course_names:
courses.append(frappe.db.get_value("LMS Course", course, "title")) @frappe.whitelist()
details["courses"] = courses def get_certification_categories():
participant_details.append(details) categories = []
return participant_details docs = frappe.get_all(
"LMS Certificate",
filters={
"published": 1,
},
fields=["course_title", "batch_title"],
)
for doc in docs:
category = doc.course_title if doc.course_title else doc.batch_title
if category not in categories:
categories.append(category)
return categories
@frappe.whitelist() @frappe.whitelist()