feat: country filter in job list

This commit is contained in:
Jannat Patel
2025-04-24 18:22:00 +05:30
parent 097d541391
commit 26a278c5f4
8 changed files with 122 additions and 58 deletions

View File

@@ -1,6 +1,6 @@
<template> <template>
<div <div
class="flex flex-col border-2 hover:bg-surface-gray-2 rounded-md p-4 h-full" class="flex flex-col border hover:border-outline-gray-4 rounded-md p-4 h-full"
style="min-height: 150px" style="min-height: 150px"
> >
<div class="text-lg leading-5 font-semibold mb-2 text-ink-gray-9"> <div class="text-lg leading-5 font-semibold mb-2 text-ink-gray-9">

View File

@@ -1,19 +1,19 @@
<template> <template>
<div <div
class="flex flex-col shadow-sm border rounded-md p-3 h-full hover:bg-surface-gray-2" class="flex flex-col border rounded-md p-3 h-full hover:border-outline-gray-4"
> >
<div class="flex space-x-4 mb-2"> <div class="flex space-x-4 mb-4">
<div class="flex flex-col space-y-2 flex-1"> <div class="flex flex-col space-y-2 flex-1">
<div class="text-lg font-semibold text-ink-gray-9"> <div class="text-lg font-semibold text-ink-gray-9">
{{ job.company_name }} {{ job.company_name }}
</div> </div>
<span class="font-medium text-ink-gray-7"> <span class="font-medium text-ink-gray-7 leading-5">
{{ job.job_title }} {{ job.job_title }}
</span> </span>
<div class="flex items-center space-x-1 text-sm text-ink-gray-7"> <div class="flex items-center space-x-1 text-sm text-ink-gray-7">
<MapPin class="size-3" /> <MapPin class="size-3" />
<span> <span>
{{ job.location }} {{ job.location }}{{ job.country ? `, ${job.country}` : '' }}
</span> </span>
</div> </div>
<div <div
@@ -27,9 +27,9 @@
</span> </span>
</div> </div>
</div> </div>
<!-- <img :src="job.company_logo" alt="Company Logo" class="h-8 object-contain bg-white" /> --> <!-- <img :src="job.company_logo" alt="Company Logo" class="size-8 rounded-full object-contain bg-white" /> -->
</div> </div>
<div class="space-x-4 mt-2 mb-4"> <div class="space-x-2 mt-auto">
<Badge> <Badge>
{{ job.type }} {{ job.type }}
</Badge> </Badge>
@@ -37,10 +37,10 @@
{{ dayjs(job.creation).fromNow() }} {{ dayjs(job.creation).fromNow() }}
</Badge> </Badge>
</div> </div>
<div <!-- <div
class="description text-ink-gray-9 text-sm" class="description text-ink-gray-9 text-sm"
v-html="job.description" v-html="job.description"
></div> ></div> -->
</div> </div>
</template> </template>
<script setup> <script setup>

View File

@@ -16,7 +16,7 @@
}, },
]" ]"
/> />
<div v-if="user.data?.name" class="flex space-x-2"> <div v-if="user.data?.name" class="flex items-center space-x-2">
<router-link <router-link
v-if="user.data.name == job.data?.owner" v-if="user.data.name == job.data?.owner"
:to="{ :to="{
@@ -47,6 +47,9 @@
</template> </template>
{{ __('Apply') }} {{ __('Apply') }}
</Button> </Button>
<Badge v-else theme="green">
{{ __('You have applied') }}
</Badge>
</div> </div>
<div v-else> <div v-else>
<Button @click="redirectToLogin(job.data?.name)"> <Button @click="redirectToLogin(job.data?.name)">
@@ -56,13 +59,13 @@
</Button> </Button>
</div> </div>
</header> </header>
<div v-if="job.data" class="max-w-3xl mx-auto"> <div v-if="job.data" class="max-w-3xl mx-auto pt-5">
<div class="p-4"> <div class="p-4">
<div class="space-y-5 mb-10"> <div class="space-y-5 mb-10">
<div class="flex items-center"> <div class="flex items-center">
<img <img
:src="job.data.company_logo" :src="job.data.company_logo"
class="size-15 rounded-lg object-contain cursor-pointer mr-4" class="size-10 rounded-lg object-contain cursor-pointer mr-4"
:alt="job.data.company_name" :alt="job.data.company_name"
@click="redirectToWebsite(job.data.company_website)" @click="redirectToWebsite(job.data.company_website)"
/> />
@@ -149,7 +152,13 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { Button, Breadcrumbs, createResource, usePageMeta } from 'frappe-ui' import {
Badge,
Button,
Breadcrumbs,
createResource,
usePageMeta,
} from 'frappe-ui'
import { inject, ref } from 'vue' import { inject, ref } from 'vue'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import JobApplicationModal from '@/components/Modals/JobApplicationModal.vue' import JobApplicationModal from '@/components/Modals/JobApplicationModal.vue'

View File

@@ -25,43 +25,48 @@
</router-link> </router-link>
</header> </header>
<div> <div>
<div v-if="jobs.data?.length" class="p-5"> <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 p-5"
>
<div <div
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between mb-5" v-if="jobCount"
class="text-xl font-semibold text-ink-gray-7 mb-4 md:mb-0"
> >
<div {{ __('{0} Open Jobs').format(jobCount) }}
v-if="jobCount"
class="text-xl font-semibold text-ink-gray-7 mb-4 md:mb-0"
>
{{ __('{0} Open Jobs').format(jobCount) }}
</div>
<div class="grid grid-cols-2 gap-2">
<FormControl
type="text"
:placeholder="__('Search')"
v-model="searchQuery"
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
@input="updateJobs"
>
<template #prefix>
<Search
class="w-4 h-4 stroke-1.5 text-ink-gray-5"
name="search"
/>
</template>
</FormControl>
<FormControl
v-model="jobType"
type="select"
:options="jobTypes"
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
:placeholder="__('Type')"
@change="updateJobs"
/>
</div>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> <FormControl
type="text"
:placeholder="__('Search')"
v-model="searchQuery"
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
@input="updateJobs"
>
<template #prefix>
<Search
class="w-4 h-4 stroke-1.5 text-ink-gray-5"
name="search"
/>
</template>
</FormControl>
<Link
doctype="Country"
v-model="country"
:placeholder="__('Country')"
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
/>
<FormControl
v-model="jobType"
type="select"
:options="jobTypes"
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
:placeholder="__('Type')"
@change="updateJobs"
/>
</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 <router-link
v-for="job in jobs.data" v-for="job in jobs.data"
:to="{ :to="{
@@ -76,18 +81,17 @@
</div> </div>
<div <div
v-else v-else
class="flex flex-col items-center justify-center text-sm text-ink-gray-5 mt-48" class="flex flex-col items-center justify-center text-sm text-ink-gray-5 mt-56"
> >
<Laptop class="size-10 mx-auto stroke-1 text-ink-gray-4" /> <Laptop class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-lg font-medium mb-1"> <div class="text-lg font-medium mb-1">
{{ __('No jobs found') }} {{ __('No jobs found') }}
</div> </div>
<div class="leading-5 w-2/5 text-center"> <div class="leading-5 w-2/5 text-center">
{{ {{ __('There are no jobs available at the moment.') }}
__( </div>
'There are no jobs available at the moment. Open a job opportunity or check here again later.' <div class="leading-5 w-1/5 text-center">
) {{ __('Post a new job or check again later.') }}
}}
</div> </div>
</div> </div>
</div> </div>
@@ -104,13 +108,15 @@ import {
} from 'frappe-ui' } from 'frappe-ui'
import { Laptop, Plus, Search } from 'lucide-vue-next' import { Laptop, Plus, Search } from 'lucide-vue-next'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import { inject, computed, ref, onMounted } from 'vue' import { inject, computed, ref, onMounted, watch } from 'vue'
import JobCard from '@/components/JobCard.vue' import JobCard from '@/components/JobCard.vue'
import Link from '@/components/Controls/Link.vue'
const user = inject('$user') const user = inject('$user')
const jobType = ref(null) const jobType = ref(null)
const { brand } = sessionStore() const { brand } = sessionStore()
const searchQuery = ref('') const searchQuery = ref('')
const country = ref(null)
const filters = ref({}) const filters = ref({})
const orFilters = ref({}) const orFilters = ref({})
const jobCount = ref(0) const jobCount = ref(0)
@@ -159,6 +165,12 @@ const updateFilters = () => {
} else { } else {
orFilters.value = {} orFilters.value = {}
} }
if (country.value) {
filters.value.country = country.value
} else {
delete filters.value.country
}
} }
const getJobCount = () => { const getJobCount = () => {
@@ -172,6 +184,11 @@ const getJobCount = () => {
jobCount.value = data jobCount.value = data
}) })
} }
watch(country, (val) => {
updateJobs()
})
const jobTypes = computed(() => { const jobTypes = computed(() => {
return [ return [
'', '',

View File

@@ -9,10 +9,11 @@
"field_order": [ "field_order": [
"job_title", "job_title",
"location", "location",
"disabled", "country",
"column_break_5", "column_break_5",
"type", "type",
"status", "status",
"disabled",
"section_break_6", "section_break_6",
"company_name", "company_name",
"company_website", "company_website",
@@ -36,7 +37,7 @@
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Location", "label": "City",
"reqd": 1 "reqd": 1
}, },
{ {
@@ -111,6 +112,13 @@
{ {
"fieldname": "column_break_phkm", "fieldname": "column_break_phkm",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "country",
"fieldtype": "Link",
"label": "Country",
"options": "Country",
"reqd": 1
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
@@ -122,7 +130,7 @@
} }
], ],
"make_attachments_public": 1, "make_attachments_public": 1,
"modified": "2025-04-24 13:21:24.311596", "modified": "2025-04-24 14:34:35.920242",
"modified_by": "sayali@frappe.io", "modified_by": "sayali@frappe.io",
"module": "Job", "module": "Job",
"name": "Job Opportunity", "name": "Job Opportunity",

View File

@@ -306,6 +306,7 @@ def get_job_opportunities(filters=None, orFilters=None):
fields=[ fields=[
"job_title", "job_title",
"location", "location",
"country",
"type", "type",
"company_name", "company_name",
"company_logo", "company_logo",

View File

@@ -101,4 +101,5 @@ lms.patches.v2_0.allow_guest_access #05-02-2025
lms.patches.v2_0.migrate_batch_student_data #10-02-2025 lms.patches.v2_0.migrate_batch_student_data #10-02-2025
lms.patches.v2_0.delete_old_enrollment_doctypes lms.patches.v2_0.delete_old_enrollment_doctypes
lms.patches.v2_0.delete_unused_custom_fields lms.patches.v2_0.delete_unused_custom_fields
lms.patches.v2_0.update_certificate_request_status lms.patches.v2_0.update_certificate_request_status
lms.patches.v2_0.update_job_city_and_country

View File

@@ -0,0 +1,28 @@
import frappe
def execute():
jobs = frappe.get_all("Job Opportunity", fields=["name", "location"])
for job in jobs:
if "," in job.location:
city, country = job.location.split(",", 1)
city = city.strip()
country = country.strip()
save_country(country, job)
frappe.db.set_value("Job Opportunity", job.name, "location", city)
else:
save_country(job.location, job)
def save_country(country, job):
if frappe.db.exists("Country", country):
frappe.db.set_value("Job Opportunity", job.name, "country", country)
else:
country_mapping = {
"US": "United States",
"USA": "United States",
"UAE": "United Arab Emirates",
}
country = country_mapping.get(country, country)
frappe.db.set_value("Job Opportunity", job.name, "country", country)