refactor: improved ui and performance for certified participants page
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -1,93 +1,173 @@
|
|||||||
<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="px-5 py-10 w-3/4 mx-auto">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 m-5">
|
<div class="flex items-center justify-between mb-5">
|
||||||
<div
|
<div class="text-lg font-semibold">
|
||||||
v-if="participants.data?.length"
|
{{ __('All Certified Participants') }}
|
||||||
v-for="participant in participantsList"
|
</div>
|
||||||
>
|
<div class="flex items-center space-x-2">
|
||||||
<router-link
|
<FormControl
|
||||||
:to="{
|
v-model="nameFilter"
|
||||||
name: 'Profile',
|
:placeholder="__('Search by Name')"
|
||||||
params: { username: participant.username },
|
type="text"
|
||||||
}"
|
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
|
||||||
>
|
@input="updateParticipants()"
|
||||||
<div class="flex shadow rounded-md h-full p-2">
|
/>
|
||||||
<UserAvatar :user="participant" size="3xl" class="mr-2" />
|
<div
|
||||||
<div>
|
v-if="categories.data?.length"
|
||||||
<router-link
|
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
|
||||||
:to="{
|
>
|
||||||
name: 'Profile',
|
<Select
|
||||||
params: { username: participant.username },
|
v-model="currentCategory"
|
||||||
}"
|
:options="categories.data"
|
||||||
>
|
:placeholder="__('Category')"
|
||||||
<div class="text-lg font-semibold mb-2">
|
@change="updateParticipants()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="participants.data?.length">
|
||||||
|
<div class="grid 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>
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user