Merge pull request #2308 from pateljannat/misc-issues

refactor: jobs and certified participants view
This commit is contained in:
Jannat Patel
2026-04-11 22:06:26 +05:30
committed by GitHub
12 changed files with 282 additions and 201 deletions
+1 -1
View File
@@ -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">
+11 -6
View File
@@ -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',
+60 -60
View File
@@ -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 = () => {
+62 -35
View File
@@ -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'),
})) }))
}) })
+39 -39
View File
@@ -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
View File
@@ -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' },
] ]
}) })
+2 -2
View File
@@ -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 = () => {
+2 -2
View File
@@ -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)"
+9 -6
View File
@@ -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',
}, },
+1 -1
View File
@@ -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
View File
@@ -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: