Files
enlight-lms/frontend/src/pages/Jobs.vue
2025-12-29 19:14:22 +05:30

298 lines
6.1 KiB
Vue

<template>
<div class="">
<header
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs
class="h-7"
:items="[{ label: __('Jobs'), route: { name: 'Jobs' } }]"
/>
<router-link
v-if="user.data?.name"
:to="{
name: 'JobForm',
params: {
jobName: 'new',
},
}"
>
<Button v-if="!readOnlyMode" variant="solid">
<template #prefix>
<Plus class="h-4 w-4" />
</template>
{{ __('New Job') }}
</Button>
</router-link>
</header>
<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"
>
<div class="flex items-center justify-between">
<div class="text-xl font-semibold text-ink-gray-9 md:mb-0">
{{ __('{0} {1} Jobs').format(jobCount, activeTab) }}
</div>
<TabButtons
v-if="tabs.length > 1"
v-model="activeTab"
:buttons="tabs"
class="lg:hidden"
@change="updateJobs"
/>
</div>
<div
class="flex flex-col md:flex-row md:items-center md:space-x-4 space-y-4 md:space-y-0"
>
<TabButtons
v-if="tabs.length > 1"
v-model="activeTab"
:buttons="tabs"
class="hidden lg:block"
@change="updateJobs"
/>
<div class="grid grid-cols-2 gap-4">
<FormControl
type="text"
:placeholder="__('Search')"
v-model="searchQuery"
class="w-full max-w-40"
@input="updateJobs"
>
<template #prefix>
<Search
class="w-4 h-4 stroke-1.5 text-ink-gray-5"
name="search"
/>
</template>
</FormControl>
<Link
v-if="user.data"
doctype="Country"
v-model="country"
:placeholder="__('Country')"
class="w-full"
/>
</div>
<div class="grid grid-cols-2 gap-4">
<FormControl
v-model="jobType"
type="select"
:options="jobTypes"
class="w-full"
:placeholder="__('Type')"
@change="updateJobs"
/>
<FormControl
v-model="workMode"
type="select"
:options="workModes"
class="w-full"
:placeholder="__('Work Mode')"
@change="updateJobs"
/>
</div>
</div>
</div>
<div v-if="jobs.data?.length" class="w-full md:w-4/5 mx-auto p-5 pt-0">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<router-link
v-for="job in jobs.data"
:to="{
name: 'JobDetail',
params: { job: job.name },
}"
:key="job.name"
>
<JobCard :job="job" />
</router-link>
</div>
</div>
<EmptyState v-else type="Job Openings" />
</div>
</div>
</template>
<script setup>
import {
Button,
Breadcrumbs,
call,
createResource,
FormControl,
TabButtons,
usePageMeta,
} from 'frappe-ui'
import { Plus, Search } from 'lucide-vue-next'
import { sessionStore } from '../stores/session'
import { inject, computed, ref, onMounted, watch } from 'vue'
import JobCard from '@/components/JobCard.vue'
import Link from '@/components/Controls/Link.vue'
import EmptyState from '@/components/EmptyState.vue'
const user = inject('$user')
const jobType = ref(null)
const workMode = ref(null)
const { brand } = sessionStore()
const searchQuery = ref('')
const country = ref(null)
const filters = ref({})
const orFilters = ref({})
const jobCount = ref(0)
const closedJobs = ref(0)
const activeTab = ref('Open')
const readOnlyMode = window.read_only_mode
onMounted(() => {
getClosedJobCount()
setFiltersFromURL()
updateJobs()
})
const isModerator = computed(() => {
return user.data?.is_moderator
})
const getClosedJobCount = () => {
if (!user.data?.name) {
return
}
const filters = {
status: 'Closed',
}
if (!isModerator.value) {
filters.owner = user.data?.name
}
call('frappe.client.get_count', {
doctype: 'Job Opportunity',
filters: filters,
}).then((count) => {
closedJobs.value = count
})
}
const setFiltersFromURL = () => {
let queries = new URLSearchParams(location.search)
if (queries.has('type')) {
jobType.value = queries.get('type')
}
if (queries.has('work_mode')) {
workMode.value = queries.get('work_mode')
}
}
const tabs = computed(() => {
const tabsArray = [
{
label: __('Open'),
},
]
if (closedJobs.value) {
tabsArray.push({
label: __('Closed'),
})
}
return tabsArray
})
const jobs = createResource({
url: 'lms.lms.api.get_job_opportunities',
cache: ['jobs'],
})
const updateJobs = () => {
updateFilters()
jobs.update({
params: {
filters: filters.value,
orFilters: orFilters.value,
},
})
jobs.reload()
}
const updateFilters = () => {
filters.value.status = 'Open'
if (jobType.value) {
filters.value.type = jobType.value
} else {
delete filters.value.type
}
if (workMode.value) {
filters.value.work_mode = workMode.value
} else {
delete filters.value.work_mode
}
if (searchQuery.value) {
orFilters.value = {
job_title: ['like', `%${searchQuery.value}%`],
company_name: ['like', `%${searchQuery.value}%`],
location: ['like', `%${searchQuery.value}%`],
}
} else {
orFilters.value = {}
}
if (country.value) {
filters.value.country = country.value
} else {
delete filters.value.country
}
if (activeTab.value === 'Closed') {
filters.value.status = 'Closed'
if (!isModerator.value) {
filters.value.owner = user.data?.name
}
} else {
filters.value.status = 'Open'
delete filters.value.owner
}
}
watch(activeTab, (val) => {
updateJobs()
})
watch(country, (val) => {
updateJobs()
})
watch(jobs, () => {
jobCount.value = jobs.data?.length || 0
})
const jobTypes = computed(() => {
return [
{ label: '', value: '' },
{ label: __('Full Time'), value: 'Full Time' },
{ label: __('Part Time'), value: 'Part Time' },
{ label: __('Contract'), value: 'Contract' },
{ label: __('Freelance'), value: 'Freelance' },
]
})
const workModes = computed(() => {
return [
{ label: '', value: '' },
{ label: 'On site', value: 'On-site' },
{ label: 'Hybrid', value: 'Hybrid' },
{ label: 'Remote', value: 'Remote' },
]
})
usePageMeta(() => {
return {
title: __('Jobs'),
icon: brand.favicon,
}
})
</script>