feat: list of certified participants
This commit is contained in:
@@ -12,51 +12,118 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</router-link>
|
</router-link>
|
||||||
</header>
|
</header>
|
||||||
<div class="p-5 lg:w-3/4 mx-auto">
|
<div
|
||||||
|
v-if="participants.data?.length"
|
||||||
|
class="mx-auto w-full max-w-4xl pt-6 pb-10"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between mb-4">
|
||||||
|
<div class="text-xl font-semibold text-ink-gray-7">
|
||||||
|
{{ memberCount }} {{ __('certified members') }}
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<FormControl
|
||||||
|
v-model="nameFilter"
|
||||||
|
:placeholder="__('Search by Name')"
|
||||||
|
type="text"
|
||||||
|
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
|
||||||
|
@input="updateParticipants()"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="categories.data?.length"
|
||||||
|
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
v-model="currentCategory"
|
||||||
|
:options="categories.data"
|
||||||
|
:placeholder="__('Category')"
|
||||||
|
@change="updateParticipants()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="divide-y">
|
||||||
<template v-for="participant in participants.data">
|
<template v-for="participant in participants.data">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: 'Profile',
|
name: 'ProfileCertificates',
|
||||||
params: {
|
params: {
|
||||||
username: participant.username,
|
username: participant.username,
|
||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
|
class="flex sm:rounded px-3 py-2 sm:h-15 hover:bg-surface-gray-2"
|
||||||
>
|
>
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center w-full space-x-3">
|
||||||
<Avatar
|
<Avatar
|
||||||
:image="participant.user_image"
|
:image="participant.user_image"
|
||||||
class="size-8 rounded-full object-contain"
|
class="size-8 rounded-full object-contain"
|
||||||
:label="participant.full_name"
|
:label="participant.full_name"
|
||||||
size="2xl"
|
size="2xl"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div class="flex-1">
|
||||||
<div>
|
<div class="text-base font-medium text-ink-gray-8">
|
||||||
{{ participant.full_name }}
|
{{ participant.full_name }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div
|
||||||
|
class="mt-1.5 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-base text-ink-gray-5"
|
||||||
|
>
|
||||||
{{ participant.headline }}
|
{{ participant.headline }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex items-center space-x-20 justify-self-end">
|
||||||
{{ participant.certificate_count }} {{ __('certificates') }}
|
<div class="text-ink-gray-5">
|
||||||
|
{{ participant.certificate_count }}
|
||||||
|
{{
|
||||||
|
participant.certificate_count > 1
|
||||||
|
? __('certificates')
|
||||||
|
: __('certificate')
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<div class="text-ink-gray-5">
|
||||||
|
{{ dayjs(participant.issue_date).format('DD MMM YYYY') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</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
|
||||||
|
class="flex flex-col items-center justify-center text-sm text-ink-gray-5 mt-48"
|
||||||
|
>
|
||||||
|
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
|
||||||
|
<div class="text-lg font-medium mb-1">
|
||||||
|
{{ __('No certified members') }}
|
||||||
|
</div>
|
||||||
|
<div class="leading-5 w-2/5 text-center">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'No certified members found. Please check again later or get certified yourself.'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
Button,
|
Button,
|
||||||
|
call,
|
||||||
createListResource,
|
createListResource,
|
||||||
FormControl,
|
FormControl,
|
||||||
Select,
|
Select,
|
||||||
usePageMeta,
|
usePageMeta,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, inject, onMounted, ref } from 'vue'
|
||||||
import { updateDocumentTitle } from '@/utils'
|
|
||||||
import { BookOpen, GraduationCap } from 'lucide-vue-next'
|
import { BookOpen, GraduationCap } from 'lucide-vue-next'
|
||||||
import { sessionStore } from '../stores/session'
|
import { sessionStore } from '../stores/session'
|
||||||
|
|
||||||
@@ -64,6 +131,8 @@ const currentCategory = ref('')
|
|||||||
const filters = ref({})
|
const filters = ref({})
|
||||||
const nameFilter = ref('')
|
const nameFilter = ref('')
|
||||||
const { brand } = sessionStore()
|
const { brand } = sessionStore()
|
||||||
|
const memberCount = ref(0)
|
||||||
|
const dayjs = inject('$dayjs')
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
updateParticipants()
|
updateParticipants()
|
||||||
@@ -78,7 +147,9 @@ const participants = createListResource({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const count = call('lms.lms.api.get_count_of_certified_members').then(
|
const count = call('lms.lms.api.get_count_of_certified_members').then(
|
||||||
(data) => {}
|
(data) => {
|
||||||
|
memberCount.value = data
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const categories = createListResource({
|
const categories = createListResource({
|
||||||
@@ -116,14 +187,14 @@ const updateFilters = () => {
|
|||||||
|
|
||||||
const breadcrumbs = computed(() => [
|
const breadcrumbs = computed(() => [
|
||||||
{
|
{
|
||||||
label: __('Certified Participants'),
|
label: __('Certified Members'),
|
||||||
route: { name: 'CertifiedParticipants' },
|
route: { name: 'CertifiedParticipants' },
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
usePageMeta(() => {
|
usePageMeta(() => {
|
||||||
return {
|
return {
|
||||||
title: __('Certified Participants'),
|
title: __('Certified Members'),
|
||||||
icon: brand.favicon,
|
icon: brand.favicon,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -479,7 +479,7 @@ export function getSidebarLinks() {
|
|||||||
activeFor: ['Batches', 'BatchDetail', 'Batch', 'BatchForm'],
|
activeFor: ['Batches', 'BatchDetail', 'Batch', 'BatchForm'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Certified Participants',
|
label: 'Certified Members',
|
||||||
icon: 'GraduationCap',
|
icon: 'GraduationCap',
|
||||||
to: 'CertifiedParticipants',
|
to: 'CertifiedParticipants',
|
||||||
activeFor: ['CertifiedParticipants'],
|
activeFor: ['CertifiedParticipants'],
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ from frappe.utils import (
|
|||||||
format_date,
|
format_date,
|
||||||
date_diff,
|
date_diff,
|
||||||
)
|
)
|
||||||
|
from frappe.query_builder import DocType
|
||||||
|
from pypika.functions import DistinctOptionFunction
|
||||||
from lms.lms.utils import get_average_rating, get_lesson_count
|
from lms.lms.utils import get_average_rating, get_lesson_count
|
||||||
from xml.dom.minidom import parseString
|
from xml.dom.minidom import parseString
|
||||||
from lms.lms.doctype.course_lesson.course_lesson import save_progress
|
from lms.lms.doctype.course_lesson.course_lesson import save_progress
|
||||||
@@ -415,25 +417,46 @@ def get_certified_participants(filters=None, start=0, page_length=30):
|
|||||||
"LMS Certificate",
|
"LMS Certificate",
|
||||||
filters=filters,
|
filters=filters,
|
||||||
or_filters=or_filters,
|
or_filters=or_filters,
|
||||||
fields=["member", "COUNT(*) as certificate_count"],
|
fields=["member", "issue_date"],
|
||||||
group_by="member",
|
group_by="member",
|
||||||
order_by="MAX(creation) desc",
|
order_by="issue_date desc",
|
||||||
start=start,
|
start=start,
|
||||||
page_length=page_length,
|
page_length=page_length,
|
||||||
)
|
)
|
||||||
|
|
||||||
for participant in participants:
|
for participant in participants:
|
||||||
|
count = frappe.db.count("LMS Certificate", {"member": participant.member})
|
||||||
details = frappe.db.get_value(
|
details = frappe.db.get_value(
|
||||||
"User",
|
"User",
|
||||||
participant.member,
|
participant.member,
|
||||||
["full_name", "user_image", "username", "country", "headline"],
|
["full_name", "user_image", "username", "country", "headline"],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
details["certificate_count"] = count
|
||||||
participant.update(details)
|
participant.update(details)
|
||||||
|
|
||||||
return participants
|
return participants
|
||||||
|
|
||||||
|
|
||||||
|
class CountDistinct(DistinctOptionFunction):
|
||||||
|
def __init__(self, field):
|
||||||
|
super().__init__("COUNT", field, distinct=True)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def get_count_of_certified_members():
|
||||||
|
Certificate = DocType("LMS Certificate")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(Certificate)
|
||||||
|
.select(CountDistinct(Certificate.member).as_("total"))
|
||||||
|
.where(Certificate.published == 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = query.run(as_dict=True)
|
||||||
|
return result[0]["total"] if result else 0
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def get_certification_categories():
|
def get_certification_categories():
|
||||||
categories = []
|
categories = []
|
||||||
|
|||||||
Reference in New Issue
Block a user