mirror of
https://github.com/frappe/lms.git
synced 2026-05-02 13:39:31 +03:00
Merge pull request #2308 from pateljannat/misc-issues
refactor: jobs and certified participants view
This commit is contained in:
@@ -69,7 +69,7 @@
|
|||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="quiz.data.duration" class="flex flex-col space-x-1 my-4">
|
<div v-if="quiz.data.duration" class="flex flex-col space-x-1 my-4 px-2">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<span class="text-ink-gray-9"> {{ __('Time') }}: </span>
|
<span class="text-ink-gray-9"> {{ __('Time') }}: </span>
|
||||||
<span class="font-semibold text-ink-gray-9">
|
<span class="font-semibold text-ink-gray-9">
|
||||||
|
|||||||
@@ -20,8 +20,10 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="py-5 mx-5">
|
<div class="py-5">
|
||||||
<div class="flex items-center justify-between mb-5">
|
<div
|
||||||
|
class="flex flex-col md:flex-row md:items-center space-y-4 md:space-y-0 justify-between mb-5 mx-5"
|
||||||
|
>
|
||||||
<div class="text-lg font-semibold text-ink-gray-9">
|
<div class="text-lg font-semibold text-ink-gray-9">
|
||||||
{{ __('{0} Assignments').format(assignments.data?.length) }}
|
{{ __('{0} Assignments').format(assignments.data?.length) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -52,7 +54,7 @@
|
|||||||
showAssignmentForm = true
|
showAssignmentForm = true
|
||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
class="h-[79vh] border-b"
|
class="h-[71vh] lg:h-[79vh] px-5"
|
||||||
>
|
>
|
||||||
<ListHeader
|
<ListHeader
|
||||||
class="mb-2 grid items-center rounded bg-surface-white border-b rounded-none p-2"
|
class="mb-2 grid items-center rounded bg-surface-white border-b rounded-none p-2"
|
||||||
@@ -104,8 +106,10 @@
|
|||||||
</template>
|
</template>
|
||||||
</ListSelectBanner>
|
</ListSelectBanner>
|
||||||
</ListView>
|
</ListView>
|
||||||
<EmptyState v-else type="Assignments" />
|
<div v-else class="h-[53vh]">
|
||||||
<div class="flex items-center justify-end space-x-3 mt-3">
|
<EmptyState type="Assignments" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-end space-x-3 pt-3 border-t px-5">
|
||||||
<Button v-if="assignments.hasNextPage" @click="assignments.next()">
|
<Button v-if="assignments.hasNextPage" @click="assignments.next()">
|
||||||
{{ __('Load More') }}
|
{{ __('Load More') }}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -182,6 +186,7 @@ watch([titleFilter, typeFilter], () => {
|
|||||||
totalAssignments.update({
|
totalAssignments.update({
|
||||||
filters: assignmentFilter.value,
|
filters: assignmentFilter.value,
|
||||||
})
|
})
|
||||||
|
totalAssignments.reload()
|
||||||
})
|
})
|
||||||
|
|
||||||
const reloadAssignments = () => {
|
const reloadAssignments = () => {
|
||||||
@@ -247,7 +252,7 @@ const assignmentColumns = computed(() => {
|
|||||||
icon: 'tag',
|
icon: 'tag',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __('Modified'),
|
label: __('Updated On'),
|
||||||
key: 'modified',
|
key: 'modified',
|
||||||
width: 1,
|
width: 1,
|
||||||
align: 'right',
|
align: 'right',
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</router-link>
|
</router-link>
|
||||||
</header>
|
</header>
|
||||||
<div class="mx-auto w-full max-w-4xl pt-6 pb-10">
|
<div class="mx-auto w-full">
|
||||||
<div class="flex flex-col md:flex-row justify-between mb-8 px-3">
|
<div class="flex flex-col md:flex-row justify-between mb-5 px-5 pt-5">
|
||||||
<div class="text-xl font-semibold text-ink-gray-9 mb-4 md:mb-0">
|
<div class="text-lg font-semibold text-ink-gray-9 mb-4 md:mb-0">
|
||||||
{{ memberCount }} {{ __('Certified Members') }}
|
{{ memberCount }} {{ __('Certified Members') }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -56,73 +56,74 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="participants.data?.length" class="">
|
<div
|
||||||
<template v-for="(participant, index) in participants.data">
|
v-if="participants.data?.length"
|
||||||
<router-link
|
class="h-[63vh] lg:h-[77vh] overflow-y-auto mb-5 px-5"
|
||||||
:to="{
|
>
|
||||||
name: 'ProfileAbout',
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5">
|
||||||
params: {
|
<div
|
||||||
username: participant.username,
|
v-for="participant in participants.data"
|
||||||
},
|
class="flex flex-col border hover:border-outline-gray-3 rounded-lg p-3 text-ink-gray-9 cursor-pointer"
|
||||||
}"
|
@click="
|
||||||
|
router.push({
|
||||||
|
name: 'ProfileAbout',
|
||||||
|
params: { username: participant.username },
|
||||||
|
})
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<div class="rounded-md hover:bg-surface-gray-2 px-3">
|
<div class="flex space-x-4">
|
||||||
<div
|
<UserAvatar :user="participant" size="2xl" />
|
||||||
class="flex items-center w-full space-x-3 py-2"
|
<div class="flex flex-col">
|
||||||
:class="{
|
<div class="font-semibold">
|
||||||
'border-b': index < participants.data.length - 1,
|
{{ participant.full_name }}
|
||||||
}"
|
</div>
|
||||||
>
|
<div class="text-sm leading-5 line-clamp-1 mb-4">
|
||||||
<UserAvatar :user="participant" size="2xl" />
|
{{
|
||||||
|
participant.headline ||
|
||||||
<div class="flex flex-col md:flex-row w-full">
|
'Joined ' + dayjs(participant.creation).fromNow()
|
||||||
<div class="flex-1">
|
}}
|
||||||
<div class="text-base font-medium text-ink-gray-8">
|
|
||||||
{{ participant.full_name }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="participant.headline"
|
|
||||||
class="mt-1.5 text-base text-ink-gray-5"
|
|
||||||
>
|
|
||||||
{{ participant.headline }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex items-center space-x-3 md:space-x-24 text-sm md:text-base mt-1.5"
|
|
||||||
>
|
|
||||||
<div class="text-ink-gray-5">
|
|
||||||
{{ participant.certificate_count }}
|
|
||||||
{{
|
|
||||||
participant.certificate_count > 1
|
|
||||||
? __('certificates')
|
|
||||||
: __('certificate')
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<span class="text-ink-gray-4 md:hidden">·</span>
|
|
||||||
<div class="text-ink-gray-5">
|
|
||||||
{{ dayjs(participant.issue_date).format('DD MMM YYYY') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
<div class="mt-auto space-y-2 text-ink-gray-7">
|
||||||
</template>
|
<div class="flex items-center space-x-1">
|
||||||
|
<GraduationCap class="h-4 w-4 stroke-1.5 mr-1" />
|
||||||
|
<span>
|
||||||
|
{{ participant.certificate_count }}
|
||||||
|
{{
|
||||||
|
participant.certificate_count > 1
|
||||||
|
? __('certificates')
|
||||||
|
: __('certificate')
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-1">
|
||||||
|
<Calendar class="h-4 w-4 stroke-1.5 mr-1" />
|
||||||
|
<span>{{
|
||||||
|
dayjs(participant.issue_date).format('DD MMM YYYY')
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<EmptyState v-else type="Certified Members" />
|
<div v-else class="h-[40vh] lg:h-[53vh] px-5">
|
||||||
<div
|
<EmptyState type="Certified Members" />
|
||||||
v-if="!participants.list.loading && participants.hasNextPage"
|
</div>
|
||||||
class="flex justify-center mt-5"
|
<div class="flex items-center justify-end space-x-3 border-t pt-3 px-5">
|
||||||
>
|
<Button v-if="participants.hasNextPage" @click="participants.next()">
|
||||||
<Button @click="participants.next()">
|
|
||||||
{{ __('Load More') }}
|
{{ __('Load More') }}
|
||||||
</Button>
|
</Button>
|
||||||
|
<div v-if="participants.hasNextPage" class="h-8 border-l"></div>
|
||||||
|
<div class="text-ink-gray-5">
|
||||||
|
{{ participants.data?.length }} {{ __('of') }}
|
||||||
|
{{ memberCount }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
Avatar,
|
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
Button,
|
Button,
|
||||||
call,
|
call,
|
||||||
@@ -132,7 +133,7 @@ import {
|
|||||||
usePageMeta,
|
usePageMeta,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { computed, inject, onMounted, ref } from 'vue'
|
import { computed, inject, onMounted, ref } from 'vue'
|
||||||
import { GraduationCap } from 'lucide-vue-next'
|
import { GraduationCap, Calendar } from 'lucide-vue-next'
|
||||||
import { sessionStore } from '../stores/session'
|
import { sessionStore } from '../stores/session'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import EmptyState from '@/components/EmptyState.vue'
|
import EmptyState from '@/components/EmptyState.vue'
|
||||||
@@ -163,7 +164,6 @@ const participants = createListResource({
|
|||||||
url: 'lms.lms.api.get_certified_participants',
|
url: 'lms.lms.api.get_certified_participants',
|
||||||
start: 0,
|
start: 0,
|
||||||
cache: ['certified_participants'],
|
cache: ['certified_participants'],
|
||||||
pageLength: 100,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const getMemberCount = () => {
|
const getMemberCount = () => {
|
||||||
|
|||||||
@@ -15,12 +15,21 @@
|
|||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<div class="max-w-4xl mx-auto pt-5 p-4">
|
<div class="mx-auto pt-5 p-4">
|
||||||
<div class="mb-6">
|
<div class="flex items-center justify-between mb-5">
|
||||||
<h1 class="text-xl font-semibold text-ink-gray-7 mb-4 md:mb-0">
|
<div class="text-lg font-semibold text-ink-gray-9 mb-4 md:mb-0">
|
||||||
{{ applicationCount }}
|
{{ totalApplications.data }}
|
||||||
{{ applicationCount === 1 ? __('Application') : __('Applications') }}
|
{{
|
||||||
</h1>
|
totalApplications.data === 1
|
||||||
|
? __('Application')
|
||||||
|
: __('Applications')
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<FormControl v-model="search" type="text" placeholder="Search">
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="search" class="size-4 text-ink-gray-5" />
|
||||||
|
</template>
|
||||||
|
</FormControl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="applications.data?.length">
|
<div v-if="applications.data?.length">
|
||||||
@@ -32,9 +41,10 @@
|
|||||||
showTooltip: false,
|
showTooltip: false,
|
||||||
selectable: false,
|
selectable: false,
|
||||||
}"
|
}"
|
||||||
|
class="h-[79vh] border-b"
|
||||||
>
|
>
|
||||||
<ListHeader
|
<ListHeader
|
||||||
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
class="mb-2 grid items-center rounded bg-surface-white border-b rounded-none p-2"
|
||||||
>
|
>
|
||||||
<ListHeaderItem
|
<ListHeaderItem
|
||||||
:item="item"
|
:item="item"
|
||||||
@@ -70,10 +80,7 @@
|
|||||||
|
|
||||||
<span>{{ item }}</span>
|
<span>{{ item }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-else-if="column.key === 'actions'">
|
||||||
v-else-if="column.key === 'actions'"
|
|
||||||
class="flex justify-center"
|
|
||||||
>
|
|
||||||
<Dropdown :options="getActionOptions(row)">
|
<Dropdown :options="getActionOptions(row)">
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<FeatherIcon name="more-horizontal" class="w-4 h-4" />
|
<FeatherIcon name="more-horizontal" class="w-4 h-4" />
|
||||||
@@ -93,13 +100,15 @@
|
|||||||
</ListRow>
|
</ListRow>
|
||||||
</ListRows>
|
</ListRows>
|
||||||
</ListView>
|
</ListView>
|
||||||
<div class="flex justify-center mt-5">
|
<div class="flex items-center justify-end space-x-3 mt-3">
|
||||||
<Button v-if="applications.hasNextPage" @click="applications.next()">
|
<Button v-if="applications.hasNextPage" @click="applications.next()">
|
||||||
<template #prefix>
|
|
||||||
<RefreshCw class="size-4 stroke-1.5" />
|
|
||||||
</template>
|
|
||||||
{{ __('Load More') }}
|
{{ __('Load More') }}
|
||||||
</Button>
|
</Button>
|
||||||
|
<div v-if="applications.hasNextPage" class="h-8 border-l"></div>
|
||||||
|
<div class="text-ink-gray-5">
|
||||||
|
{{ applications.data?.length }} {{ __('of') }}
|
||||||
|
{{ totalApplications.data }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<EmptyState v-else-if="!applications.loading" type="Job Applications" />
|
<EmptyState v-else-if="!applications.loading" type="Job Applications" />
|
||||||
@@ -172,8 +181,7 @@ import {
|
|||||||
usePageMeta,
|
usePageMeta,
|
||||||
toast,
|
toast,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { RefreshCw } from 'lucide-vue-next'
|
import { computed, inject, ref, reactive, watch } from 'vue'
|
||||||
import { computed, inject, onMounted, ref, reactive } from 'vue'
|
|
||||||
import { sessionStore } from '../stores/session'
|
import { sessionStore } from '../stores/session'
|
||||||
import EmptyState from '@/components/EmptyState.vue'
|
import EmptyState from '@/components/EmptyState.vue'
|
||||||
|
|
||||||
@@ -181,7 +189,7 @@ const dayjs = inject('$dayjs')
|
|||||||
const { brand } = sessionStore()
|
const { brand } = sessionStore()
|
||||||
const showEmailModal = ref(false)
|
const showEmailModal = ref(false)
|
||||||
const selectedApplicant = ref(null)
|
const selectedApplicant = ref(null)
|
||||||
const applicationCount = ref(0)
|
const search = ref('')
|
||||||
const emailForm = reactive({
|
const emailForm = reactive({
|
||||||
subject: '',
|
subject: '',
|
||||||
message: '',
|
message: '',
|
||||||
@@ -195,19 +203,6 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getApplicationCount()
|
|
||||||
})
|
|
||||||
|
|
||||||
const getApplicationCount = () => {
|
|
||||||
call('frappe.client.get_count', {
|
|
||||||
doctype: 'LMS Job Application',
|
|
||||||
filters: { job: props.job },
|
|
||||||
}).then((count) => {
|
|
||||||
applicationCount.value = count
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const applications = createListResource({
|
const applications = createListResource({
|
||||||
doctype: 'LMS Job Application',
|
doctype: 'LMS Job Application',
|
||||||
fields: [
|
fields: [
|
||||||
@@ -225,6 +220,37 @@ const applications = createListResource({
|
|||||||
auto: true,
|
auto: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const totalApplications = createResource({
|
||||||
|
url: 'frappe.client.get_count',
|
||||||
|
params: {
|
||||||
|
doctype: 'LMS Job Application',
|
||||||
|
filters: {
|
||||||
|
job: props.job,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
auto: true,
|
||||||
|
cache: ['totalApplications', props.job],
|
||||||
|
onError(err) {
|
||||||
|
toast.error(err.messages?.[0] || err)
|
||||||
|
console.error('Error fetching total applications:', err)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(search, () => {
|
||||||
|
let filters = {
|
||||||
|
job: props.job,
|
||||||
|
user: ['like', `%${search.value}%`],
|
||||||
|
}
|
||||||
|
applications.update({
|
||||||
|
filters: filters,
|
||||||
|
})
|
||||||
|
applications.reload()
|
||||||
|
totalApplications.update({
|
||||||
|
filters: filters,
|
||||||
|
})
|
||||||
|
totalApplications.reload()
|
||||||
|
})
|
||||||
|
|
||||||
const emailResource = createResource({
|
const emailResource = createResource({
|
||||||
url: 'frappe.core.doctype.communication.email.make',
|
url: 'frappe.core.doctype.communication.email.make',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
@@ -298,25 +324,26 @@ const applicationColumns = computed(() => {
|
|||||||
{
|
{
|
||||||
label: __('Full Name'),
|
label: __('Full Name'),
|
||||||
key: 'full_name',
|
key: 'full_name',
|
||||||
width: 2,
|
width: 3,
|
||||||
icon: 'user',
|
icon: 'user',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __('Email'),
|
label: __('Email'),
|
||||||
key: 'email',
|
key: 'email',
|
||||||
width: 2,
|
width: 3,
|
||||||
icon: 'at-sign',
|
icon: 'at-sign',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __('Applied On'),
|
label: __('Applied On'),
|
||||||
key: 'applied_on',
|
key: 'applied_on',
|
||||||
width: 1,
|
width: 2,
|
||||||
icon: 'calendar',
|
icon: 'calendar',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '',
|
label: '',
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
width: 1,
|
width: 1,
|
||||||
|
align: 'right',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
@@ -326,7 +353,7 @@ const applicantRows = computed(() => {
|
|||||||
return applications.data.map((application) => ({
|
return applications.data.map((application) => ({
|
||||||
...application,
|
...application,
|
||||||
full_name: application.full_name,
|
full_name: application.full_name,
|
||||||
applied_on: dayjs(application.creation).fromNow(),
|
applied_on: dayjs(application.creation).format('DD MMM YYYY'),
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -13,13 +13,13 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="py-5">
|
<div class="">
|
||||||
<div class="container border-b mb-4 pb-5">
|
<div class="grid grid-cols-[70%,30%] gap-5 px-5">
|
||||||
<div class="text-lg font-semibold mb-4 text-ink-gray-9">
|
<div class="space-y-5 pt-5">
|
||||||
{{ __('Job Details') }}
|
<div class="text-ink-gray-9 font-semibold">
|
||||||
</div>
|
{{ __('Job Details') }}
|
||||||
<div class="grid grid-cols-2 gap-5">
|
</div>
|
||||||
<div class="space-y-4">
|
<div class="grid grid-cols-3 gap-5">
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="job.job_title"
|
v-model="job.job_title"
|
||||||
:label="__('Title')"
|
:label="__('Title')"
|
||||||
@@ -40,7 +40,34 @@
|
|||||||
:required="true"
|
:required="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-4">
|
<div>
|
||||||
|
<label class="block text-ink-gray-5 text-xs mb-1">
|
||||||
|
{{ __('Description') }}
|
||||||
|
<span class="text-ink-red-3">*</span>
|
||||||
|
</label>
|
||||||
|
<TextEditor
|
||||||
|
:content="job.description"
|
||||||
|
@change="(val) => (job.description = val)"
|
||||||
|
:editable="true"
|
||||||
|
:fixedMenu="true"
|
||||||
|
editorClass="prose-sm max-w-none border-b border-x border-outline-gray-modals bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[20rem] max-h-[70vh] overflow-y-auto mb-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="border-l h-[93vh]">
|
||||||
|
<div v-if="jobName != 'new'" class="p-5 space-y-5 border-b">
|
||||||
|
<FormControl
|
||||||
|
v-model="job.status"
|
||||||
|
:label="__('Status')"
|
||||||
|
type="select"
|
||||||
|
:options="jobStatuses"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="p-5 space-y-5 border-b">
|
||||||
|
<div class="text-ink-gray-9 font-semibold">
|
||||||
|
{{ __('Location') }}
|
||||||
|
</div>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="job.location"
|
v-model="job.location"
|
||||||
:label="__('City')"
|
:label="__('City')"
|
||||||
@@ -52,23 +79,11 @@
|
|||||||
:label="__('Country')"
|
:label="__('Country')"
|
||||||
:required="true"
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
|
||||||
v-if="jobName != 'new'"
|
|
||||||
v-model="job.status"
|
|
||||||
:label="__('Status')"
|
|
||||||
type="select"
|
|
||||||
:options="jobStatuses"
|
|
||||||
:required="true"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="p-5 space-y-5">
|
||||||
</div>
|
<div class="text-ink-gray-9 font-semibold">
|
||||||
<div class="container border-b mb-4 pb-5">
|
{{ __('Company Details') }}
|
||||||
<div class="text-lg font-semibold mb-4 text-ink-gray-9">
|
</div>
|
||||||
{{ __('Company Details') }}
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 gap-5">
|
|
||||||
<div>
|
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="job.company_name"
|
v-model="job.company_name"
|
||||||
:label="__('Company Name')"
|
:label="__('Company Name')"
|
||||||
@@ -80,8 +95,6 @@
|
|||||||
:label="__('Company Website')"
|
:label="__('Company Website')"
|
||||||
:required="true"
|
:required="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="job.company_email_address"
|
v-model="job.company_email_address"
|
||||||
:label="__('Company Email Address')"
|
:label="__('Company Email Address')"
|
||||||
@@ -96,19 +109,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container mt-4">
|
|
||||||
<label class="block text-ink-gray-5 text-xs mb-1">
|
|
||||||
{{ __('Description') }}
|
|
||||||
<span class="text-ink-red-3">*</span>
|
|
||||||
</label>
|
|
||||||
<TextEditor
|
|
||||||
:content="job.description"
|
|
||||||
@change="(val) => (job.description = val)"
|
|
||||||
:editable="true"
|
|
||||||
:fixedMenu="true"
|
|
||||||
editorClass="prose-sm max-w-none border-b border-x border-outline-gray-modals bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] mb-4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+74
-37
@@ -22,17 +22,17 @@
|
|||||||
<template #prefix>
|
<template #prefix>
|
||||||
<Plus class="h-4 w-4" />
|
<Plus class="h-4 w-4" />
|
||||||
</template>
|
</template>
|
||||||
{{ __('New Job') }}
|
{{ __('Create') }}
|
||||||
</Button>
|
</Button>
|
||||||
</router-link>
|
</router-link>
|
||||||
</header>
|
</header>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between w-full md:w-4/5 mx-auto mb-2 p-5"
|
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between w-full mx-auto mb-2 p-5"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="text-xl font-semibold text-ink-gray-9 md:mb-0">
|
<div class="text-lg font-semibold text-ink-gray-9 md:mb-0">
|
||||||
{{ __('{0} {1} Jobs').format(jobCount, activeTab) }}
|
{{ __('{0} {1} Jobs').format(jobCount.data, activeTab) }}
|
||||||
</div>
|
</div>
|
||||||
<TabButtons
|
<TabButtons
|
||||||
v-if="tabs.length > 1"
|
v-if="tabs.length > 1"
|
||||||
@@ -96,8 +96,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="jobs.data?.length" class="w-full md:w-4/5 mx-auto p-5 pt-0">
|
<div
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
v-if="jobs.data?.length"
|
||||||
|
class="w-full h-[61vh] lg:h-[78vh] overflow-y-auto mx-auto p-5 pt-0"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<router-link
|
<router-link
|
||||||
v-for="job in jobs.data"
|
v-for="job in jobs.data"
|
||||||
:to="{
|
:to="{
|
||||||
@@ -110,7 +113,19 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<EmptyState v-else type="Job Openings" />
|
<div v-else class="h-[32vh] lg:h-[50vh] px-5">
|
||||||
|
<EmptyState type="Job Openings" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-end space-x-3 border-t pt-3 px-5">
|
||||||
|
<Button v-if="jobs.hasNextPage" @click="jobs.next()">
|
||||||
|
{{ __('Load More') }}
|
||||||
|
</Button>
|
||||||
|
<div v-if="jobs.hasNextPage" class="h-8 border-l"></div>
|
||||||
|
<div class="text-ink-gray-5">
|
||||||
|
{{ jobs.data?.length }} {{ __('of') }}
|
||||||
|
{{ jobCount.data }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -119,6 +134,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
call,
|
call,
|
||||||
|
createListResource,
|
||||||
createResource,
|
createResource,
|
||||||
FormControl,
|
FormControl,
|
||||||
TabButtons,
|
TabButtons,
|
||||||
@@ -141,7 +157,6 @@ const searchQuery = ref('')
|
|||||||
const country = ref(null)
|
const country = ref(null)
|
||||||
const filters = ref({})
|
const filters = ref({})
|
||||||
const orFilters = ref({})
|
const orFilters = ref({})
|
||||||
const jobCount = ref(0)
|
|
||||||
const closedJobs = ref(0)
|
const closedJobs = ref(0)
|
||||||
const activeTab = ref('Open')
|
const activeTab = ref('Open')
|
||||||
const readOnlyMode = window.read_only_mode
|
const readOnlyMode = window.read_only_mode
|
||||||
@@ -157,9 +172,7 @@ const isModerator = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const getClosedJobCount = () => {
|
const getClosedJobCount = () => {
|
||||||
if (!user.data?.name) {
|
if (!user.data?.name) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const filters = {
|
const filters = {
|
||||||
status: 'Closed',
|
status: 'Closed',
|
||||||
@@ -177,6 +190,14 @@ const getClosedJobCount = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const jobCount = createResource({
|
||||||
|
url: 'frappe.client.get_count',
|
||||||
|
params: {
|
||||||
|
doctype: 'Job Opportunity',
|
||||||
|
filters: filters.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const setFiltersFromURL = () => {
|
const setFiltersFromURL = () => {
|
||||||
let queries = new URLSearchParams(location.search)
|
let queries = new URLSearchParams(location.search)
|
||||||
if (queries.has('type')) {
|
if (queries.has('type')) {
|
||||||
@@ -187,53 +208,53 @@ const setFiltersFromURL = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabs = computed(() => {
|
const jobs = createListResource({
|
||||||
const tabsArray = [
|
|
||||||
{
|
|
||||||
label: __('Open'),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
if (closedJobs.value) {
|
|
||||||
tabsArray.push({
|
|
||||||
label: __('Closed'),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return tabsArray
|
|
||||||
})
|
|
||||||
|
|
||||||
const jobs = createResource({
|
|
||||||
url: 'lms.lms.api.get_job_opportunities',
|
url: 'lms.lms.api.get_job_opportunities',
|
||||||
|
doctype: 'Job Opportunity',
|
||||||
|
start: 0,
|
||||||
cache: ['jobs'],
|
cache: ['jobs'],
|
||||||
})
|
})
|
||||||
|
|
||||||
const updateJobs = () => {
|
const updateJobs = () => {
|
||||||
updateFilters()
|
updateFilters()
|
||||||
jobs.update({
|
jobs.update({
|
||||||
params: {
|
filters: filters.value,
|
||||||
filters: filters.value,
|
orFilters: orFilters.value,
|
||||||
orFilters: orFilters.value,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
jobs.reload()
|
jobs.reload()
|
||||||
|
jobCount.update({
|
||||||
|
filters: filters.value,
|
||||||
|
orFilters: orFilters.value,
|
||||||
|
})
|
||||||
|
jobCount.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateFilters = () => {
|
const updateFilters = () => {
|
||||||
filters.value.status = 'Open'
|
filters.value.status = 'Open'
|
||||||
|
updateJobTypeFilter()
|
||||||
|
updateWorkModeFilter()
|
||||||
|
updateSearchQueryFilter()
|
||||||
|
updateCountryFilter()
|
||||||
|
updateTabFilter()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateJobTypeFilter = () => {
|
||||||
if (jobType.value && jobType.value !== ' ') {
|
if (jobType.value && jobType.value !== ' ') {
|
||||||
filters.value.type = jobType.value
|
filters.value.type = jobType.value
|
||||||
} else {
|
} else {
|
||||||
delete filters.value.type
|
delete filters.value.type
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateWorkModeFilter = () => {
|
||||||
if (workMode.value && workMode.value !== ' ') {
|
if (workMode.value && workMode.value !== ' ') {
|
||||||
filters.value.work_mode = workMode.value
|
filters.value.work_mode = workMode.value
|
||||||
} else {
|
} else {
|
||||||
delete filters.value.work_mode
|
delete filters.value.work_mode
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSearchQueryFilter = () => {
|
||||||
if (searchQuery.value) {
|
if (searchQuery.value) {
|
||||||
orFilters.value = {
|
orFilters.value = {
|
||||||
job_title: ['like', `%${searchQuery.value}%`],
|
job_title: ['like', `%${searchQuery.value}%`],
|
||||||
@@ -243,13 +264,17 @@ const updateFilters = () => {
|
|||||||
} else {
|
} else {
|
||||||
orFilters.value = {}
|
orFilters.value = {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCountryFilter = () => {
|
||||||
if (country.value) {
|
if (country.value) {
|
||||||
filters.value.country = country.value
|
filters.value.country = country.value
|
||||||
} else {
|
} else {
|
||||||
delete filters.value.country
|
delete filters.value.country
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateTabFilter = () => {
|
||||||
if (activeTab.value === 'Closed') {
|
if (activeTab.value === 'Closed') {
|
||||||
filters.value.status = 'Closed'
|
filters.value.status = 'Closed'
|
||||||
if (!isModerator.value) {
|
if (!isModerator.value) {
|
||||||
@@ -269,8 +294,20 @@ watch(country, (val) => {
|
|||||||
updateJobs()
|
updateJobs()
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(jobs, () => {
|
const tabs = computed(() => {
|
||||||
jobCount.value = jobs.data?.length || 0
|
const tabsArray = [
|
||||||
|
{
|
||||||
|
label: __('Open'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (closedJobs.value) {
|
||||||
|
tabsArray.push({
|
||||||
|
label: __('Closed'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabsArray
|
||||||
})
|
})
|
||||||
|
|
||||||
const jobTypes = computed(() => {
|
const jobTypes = computed(() => {
|
||||||
@@ -286,9 +323,9 @@ const jobTypes = computed(() => {
|
|||||||
const workModes = computed(() => {
|
const workModes = computed(() => {
|
||||||
return [
|
return [
|
||||||
{ label: ' ', value: ' ' },
|
{ label: ' ', value: ' ' },
|
||||||
{ label: 'On site', value: 'On-site' },
|
{ label: __('On-site'), value: 'On-site' },
|
||||||
{ label: 'Hybrid', value: 'Hybrid' },
|
{ label: __('Hybrid'), value: 'Hybrid' },
|
||||||
{ label: 'Remote', value: 'Remote' },
|
{ label: __('Remote'), value: 'Remote' },
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -39,9 +39,9 @@
|
|||||||
/>
|
/>
|
||||||
<div class="space-y-1.5 sm:space-y-2 w-full">
|
<div class="space-y-1.5 sm:space-y-2 w-full">
|
||||||
<div class="flex items-start sm:items-center justify-between gap-2">
|
<div class="flex items-start sm:items-center justify-between gap-2">
|
||||||
<div class="flex-1 flex flex-row justify-between">
|
<div class="flex-1 flex flex-row space-x-2 justify-between">
|
||||||
<div
|
<div
|
||||||
class="text-ink-gray-9 text-sm sm:text-base"
|
class="text-ink-gray-9 text-sm sm:text-base leading-5"
|
||||||
v-html="log.subject"
|
v-html="log.subject"
|
||||||
></div>
|
></div>
|
||||||
<div class="text-xs text-ink-gray-5 whitespace-nowrap">
|
<div class="text-xs text-ink-gray-5 whitespace-nowrap">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<div class="space-x-2">
|
<div class="space-x-2">
|
||||||
<router-link
|
<router-link
|
||||||
v-if="exercises.data?.length"
|
v-if="exercises.data?.length"
|
||||||
|
class="hidden md:block"
|
||||||
:to="{
|
:to="{
|
||||||
name: 'ProgrammingExerciseSubmissions',
|
name: 'ProgrammingExerciseSubmissions',
|
||||||
}"
|
}"
|
||||||
@@ -34,8 +35,10 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="p-5">
|
<div class="py-5">
|
||||||
<div class="flex items-center justify-between mb-5">
|
<div
|
||||||
|
class="flex flex-col md:flex-row md:items-center space-y-4 md:space-y-0 justify-between mb-5 px-5"
|
||||||
|
>
|
||||||
<div class="text-lg font-semibold text-ink-gray-9">
|
<div class="text-lg font-semibold text-ink-gray-9">
|
||||||
{{ __('{0} Exercises').format(exercises.data?.length) }}
|
{{ __('{0} Exercises').format(exercises.data?.length) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -69,7 +72,7 @@
|
|||||||
showForm = true
|
showForm = true
|
||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
class="h-[79vh] border-b"
|
class="h-[71vh] lg:h-[79vh] px-5"
|
||||||
>
|
>
|
||||||
<ListHeader
|
<ListHeader
|
||||||
class="mb-2 grid items-center rounded bg-surface-white border-b rounded-none p-2"
|
class="mb-2 grid items-center rounded bg-surface-white border-b rounded-none p-2"
|
||||||
@@ -115,8 +118,10 @@
|
|||||||
</ListSelectBanner>
|
</ListSelectBanner>
|
||||||
</ListView>
|
</ListView>
|
||||||
</div>
|
</div>
|
||||||
<EmptyState v-else type="Programming Exercises" />
|
<div v-else class="h-[45vh] lg:h-[53vh] px-5">
|
||||||
<div class="flex items-center justify-end space-x-3 mt-3">
|
<EmptyState type="Programming Exercises" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-end space-x-3 px-5 pt-3 border-t">
|
||||||
<Button v-if="exercises.hasNextPage" @click="exercises.next()">
|
<Button v-if="exercises.hasNextPage" @click="exercises.next()">
|
||||||
{{ __('Load More') }}
|
{{ __('Load More') }}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -203,6 +208,7 @@ const updateList = () => {
|
|||||||
totalExercises.update({
|
totalExercises.update({
|
||||||
filters: filters,
|
filters: filters,
|
||||||
})
|
})
|
||||||
|
totalExercises.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFilters = () => {
|
const getFilters = () => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
{{ __('New') }}
|
{{ __('New') }}
|
||||||
</Button>
|
</Button>
|
||||||
</header>
|
</header>
|
||||||
<div v-if="programs.data?.length && !isStudent" class="py-10 w-3/4 mx-auto">
|
<div v-if="programs.data?.length && !isStudent" class="py-10 px-5">
|
||||||
<div class="text-lg font-semibold text-ink-gray-9 mb-5">
|
<div class="text-lg font-semibold text-ink-gray-9 mb-5">
|
||||||
{{
|
{{
|
||||||
__('{0} {1}').format(
|
__('{0} {1}').format(
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-5">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5">
|
||||||
<div
|
<div
|
||||||
v-for="program in programs.data"
|
v-for="program in programs.data"
|
||||||
@click="openForm(program.name)"
|
@click="openForm(program.name)"
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
{{ __('Create') }}
|
{{ __('Create') }}
|
||||||
</Button>
|
</Button>
|
||||||
</header>
|
</header>
|
||||||
<div class="pt-5 mx-5">
|
<div class="pt-5">
|
||||||
<div class="flex items-center justify-between mb-5">
|
<div class="flex items-center justify-between mb-5 mx-5">
|
||||||
<div class="text-lg font-semibold text-ink-gray-9">
|
<div class="text-lg font-semibold text-ink-gray-9">
|
||||||
{{ __('{0} Quizzes').format(quizzes.data?.length) }}
|
{{ __('{0} Quizzes').format(quizzes.data?.length) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
:rows="quizzes.data"
|
:rows="quizzes.data"
|
||||||
row-key="name"
|
row-key="name"
|
||||||
:options="{ showTooltip: false, selectable: true }"
|
:options="{ showTooltip: false, selectable: true }"
|
||||||
class="h-[79vh] border-b"
|
class="h-[74.5vh] lg:h-[79vh] px-5"
|
||||||
>
|
>
|
||||||
<ListHeader
|
<ListHeader
|
||||||
class="mb-2 grid items-center rounded bg-surface-white border-b rounded-none p-2"
|
class="mb-2 grid items-center rounded bg-surface-white border-b rounded-none p-2"
|
||||||
@@ -85,8 +85,10 @@
|
|||||||
</template>
|
</template>
|
||||||
</ListSelectBanner>
|
</ListSelectBanner>
|
||||||
</ListView>
|
</ListView>
|
||||||
<EmptyState v-else type="Quizzes" />
|
<div v-else class="h-[49vh] lg:h-[53vh] px-5">
|
||||||
<div class="flex items-center justify-end space-x-3 mt-3">
|
<EmptyState type="Quizzes" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-end space-x-3 pt-3 border-t px-5">
|
||||||
<Button v-if="quizzes.hasNextPage" @click="quizzes.next()">
|
<Button v-if="quizzes.hasNextPage" @click="quizzes.next()">
|
||||||
{{ __('Load More') }}
|
{{ __('Load More') }}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -184,6 +186,7 @@ watch(search, () => {
|
|||||||
totalQuizzes.update({
|
totalQuizzes.update({
|
||||||
filters: quizFilters.value,
|
filters: quizFilters.value,
|
||||||
})
|
})
|
||||||
|
totalQuizzes.reload()
|
||||||
})
|
})
|
||||||
|
|
||||||
const quizzes = createListResource({
|
const quizzes = createListResource({
|
||||||
@@ -295,7 +298,7 @@ const quizColumns = computed(() => {
|
|||||||
{
|
{
|
||||||
label: __('Show Answers'),
|
label: __('Show Answers'),
|
||||||
key: 'show_answers',
|
key: 'show_answers',
|
||||||
width: 1,
|
width: 0.5,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
icon: 'eye',
|
icon: 'eye',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export class Quiz {
|
|||||||
renderQuiz(quiz) {
|
renderQuiz(quiz) {
|
||||||
if (this.readOnly) {
|
if (this.readOnly) {
|
||||||
const quizPath = getLmsRoute(`quiz/${quiz}?fromLesson=1`)
|
const quizPath = getLmsRoute(`quiz/${quiz}?fromLesson=1`)
|
||||||
this.wrapper.innerHTML = `<iframe src="${quizPath}" class="w-full h-[500px]"></iframe>`
|
this.wrapper.innerHTML = `<iframe src="${quizPath}" class="w-full h-[700px]"></iframe>`
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.wrapper.innerHTML = `<div class='border rounded-md p-4 text-center bg-surface-menu-bar mb-4'>
|
this.wrapper.innerHTML = `<div class='border rounded-md p-4 text-center bg-surface-menu-bar mb-4'>
|
||||||
|
|||||||
+10
-7
@@ -232,14 +232,16 @@ def get_job_details(job: str):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def get_job_opportunities(filters: dict = None, orFilters: dict = None):
|
def get_job_opportunities(
|
||||||
|
filters: dict = None, or_filters: dict = None, start: int = 0, page_length: int = 40
|
||||||
|
):
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = {}
|
filters = {}
|
||||||
|
|
||||||
jobs = frappe.get_all(
|
jobs = frappe.get_all(
|
||||||
"Job Opportunity",
|
"Job Opportunity",
|
||||||
filters=filters,
|
filters=filters,
|
||||||
or_filters=orFilters,
|
or_filters=or_filters,
|
||||||
fields=[
|
fields=[
|
||||||
"job_title",
|
"job_title",
|
||||||
"location",
|
"location",
|
||||||
@@ -252,6 +254,8 @@ def get_job_opportunities(filters: dict = None, orFilters: dict = None):
|
|||||||
"creation",
|
"creation",
|
||||||
"description",
|
"description",
|
||||||
],
|
],
|
||||||
|
start=start,
|
||||||
|
page_length=page_length,
|
||||||
order_by="creation desc",
|
order_by="creation desc",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -344,11 +348,10 @@ def get_evaluator_details(evaluator: str):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_certified_participants(filters: dict = None, start: int = 0, page_length: int = 100):
|
def get_certified_participants(filters: dict = None, start: int = 0, page_length: int = 40):
|
||||||
query = get_certification_query(filters)
|
query = get_certification_query(filters)
|
||||||
query = query.orderby("issue_date", order=frappe.qb.desc).offset(start).limit(page_length)
|
query = query.orderby("issue_date", order=frappe.qb.desc).offset(start).limit(page_length)
|
||||||
participants = query.run(as_dict=True)
|
participants = query.run(as_dict=True)
|
||||||
|
|
||||||
for participant in participants:
|
for participant in participants:
|
||||||
details = get_certified_participant_details(participant.member)
|
details = get_certified_participant_details(participant.member)
|
||||||
participant.update(details)
|
participant.update(details)
|
||||||
@@ -361,7 +364,7 @@ def get_certified_participant_details(member: str):
|
|||||||
details = frappe.db.get_value(
|
details = frappe.db.get_value(
|
||||||
"User",
|
"User",
|
||||||
member,
|
member,
|
||||||
["full_name", "user_image", "username", "country", "headline", "open_to"],
|
["full_name", "user_image", "username", "creation", "headline", "open_to"],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
details["certificate_count"] = count
|
details["certificate_count"] = count
|
||||||
@@ -374,12 +377,12 @@ def get_certification_query(filters: dict = None):
|
|||||||
|
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(Certificate)
|
frappe.qb.from_(Certificate)
|
||||||
.select(Certificate.member, Certificate.issue_date)
|
.select(Certificate.member, fn.Max(Certificate.issue_date).as_("issue_date"))
|
||||||
.distinct()
|
|
||||||
.join(User)
|
.join(User)
|
||||||
.on(Certificate.member == User.name)
|
.on(Certificate.member == User.name)
|
||||||
.where(Certificate.published == 1)
|
.where(Certificate.published == 1)
|
||||||
.where(User.enabled == 1)
|
.where(User.enabled == 1)
|
||||||
|
.groupby(Certificate.member)
|
||||||
)
|
)
|
||||||
|
|
||||||
if filters:
|
if filters:
|
||||||
|
|||||||
Reference in New Issue
Block a user