HR-ATS-Portal/frontend/src/api/jobs.js

162 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { downloadFile, fetchBlobUrl, request } from '../lib/apiClient'
import { toDate } from '../lib/format'
/**
* Job requisitions — backend/job/app.py `GET /jobs/fetch`.
*
* Distinct from api/jobPosts.js on purpose: that serves the Matching and CV-import
* PICKERS off /job/fetch (job_board.view). This is the requisition list behind
* jobs.view, and carries department / vacancies / requisition_status, which the
* picker payload does not.
*/
export function list({ search, department, requisitionStatus, employmentType,
hiringManagerId, top, skip, activeOnly } = {}) {
return request('/jobs/fetch', {
params: {
search,
department,
requisition_status: requisitionStatus,
employment_type: employmentType,
hiring_manager_id: hiringManagerId,
top,
skip,
active_only: activeOnly,
},
})
}
/* requisition_status is the HIRING lifecycle. The row's separate `status` field is
the Buffer publishing lifecycle — never map the two onto one badge. Fallback
matches GET /jobs/requisition-statuses/fetch so the dropdown still works if
that call 403s. */
export const REQUISITION_STATUSES = [
{ value: 'open', label: 'Open' },
{ value: 'on_hold', label: 'On Hold' },
{ value: 'closed', label: 'Closed' },
{ value: 'completed', label: 'Completed' },
]
export const REQ_STATUS_LABEL = Object.fromEntries(REQUISITION_STATUSES.map((s) => [s.value, s.label]))
const LABEL_TO_STATUS = Object.fromEntries(REQUISITION_STATUSES.map((s) => [s.label, s.value]))
export const JOB_STATUSES = REQUISITION_STATUSES.map((s) => s.label)
export function listRequisitionStatuses() {
return request('/jobs/requisition-statuses/fetch')
}
/**
* Distinct departments across all requisitions (closed included — a past
* hiring department is a legitimate lens on a past window). The shared option
* list for the Dashboard and Analytics department filters: both screens cache
* it under qk.jobs.list({ scope: 'departments' }), and React Query caches on
* the key alone, so every consumer of that key must use THIS fetcher — two
* queryFns returning different shapes on one key overwrite each other (the
* React error #31 failure documented in api/inbox.js).
*/
export async function fetchDepartmentOptions() {
const res = await list({ top: 500, activeOnly: false })
const rows = Array.isArray(res?.data) ? res.data : []
return [...new Set(rows.map((r) => r.department).filter(Boolean))].sort()
}
function experienceLabel(min, max) {
if (min == null && max == null) return null
if (min != null && max != null) return `${min}${max} years`
return `${min ?? max}+ years`
}
/** API row -> what the Jobs table and detail modal render. */
export function toJobView(row) {
return {
id: row.id,
title: row.title,
department: row.department,
location: row.location,
type: row.employment_type,
vacancies: row.vacancies,
platform: row.platform || null,
status: REQ_STATUS_LABEL[row.requisition_status] ?? row.requisition_status,
publishStatus: row.status,
recruiter: row.recruiter_name,
recruiterId: row.current_recruiter_id,
hiringManager: row.hiring_manager_name,
hiringManagerId: row.hiring_manager_id,
createdByName: row.created_by_name,
applicantCount: row.applicant_count ?? 0,
// A real Date, so the existing `sortValue: j => j.created.getTime()` keeps working.
created: toDate(row.created_at),
closedAt: toDate(row.closed_at),
requisitionStatus: row.requisition_status,
experienceMin: row.experience_min,
experienceMax: row.experience_max,
experience: experienceLabel(row.experience_min, row.experience_max),
skills: row.requirements ?? [],
optionalSkills: row.optional_skills ?? [],
description: row.description,
requisitionId: row.requisition_id || null,
requisitionTitle: row.requisition_title || '',
requisitionDepartment: row.requisition_department || '',
requisitionLabel: row.requisition_id
? `${(row.requisition_title || 'Untitled').trim() || 'Untitled'} - ${(row.requisition_department || '—').trim() || '—'}`
: null,
}
}
/**
* Styled .xlsx download of the requisition list — GET /jobs/export
* (jobs.export). Same filters as list(); `status` takes the UI label.
* downloadFile triggers the browser save from the Content-Disposition name.
*/
export function exportXlsx({ search, department, status, employmentType } = {}) {
return downloadFile('/jobs/export', {
params: {
search: search || undefined,
department: department || undefined,
requisition_status: status ? (LABEL_TO_STATUS[status] ?? status) : undefined,
employment_type: employmentType || undefined,
},
})
}
export function update(jobPostId, body) {
return request('/jobs/update', {
method: 'PATCH',
params: { job_post_id: jobPostId },
body,
})
}
export function remove(jobPostId) {
return request('/jobs/delete', {
method: 'DELETE',
params: { job_post_id: jobPostId },
})
}
/** Attach or replace a job post's cover image — POST /job/image/upload (multipart). */
export function uploadImage(jobPostId, file) {
const fd = new FormData()
fd.append('job_post_id', jobPostId)
fd.append('file', file)
return request('/job/image/upload', { method: 'POST', body: fd })
}
/** Object URL of the cover image, or null when the post has none. Caller revokes. */
export function fetchImageUrl(jobPostId) {
return fetchBlobUrl('/job/image/fetch', { params: { job_post_id: jobPostId } })
}
/** `status` is the Jobs UI label (Open / Closed / On Hold) or a raw requisition_status. */
export function setStatus(jobPostId, status) {
const requisition_status = LABEL_TO_STATUS[status] ?? status
return request('/jobs/status', {
method: 'PATCH',
params: { job_post_id: jobPostId },
body: { requisition_status },
})
}
/** Status-change audit for one requisition — GET /jobs/status-history/fetch. */
export function listStatusHistory(jobPostId) {
return request('/jobs/status-history/fetch', { params: { job_post_id: jobPostId } })
}