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` } function recruiterIdsFrom(row) { const ids = Array.isArray(row?.current_recruiter_ids) ? row.current_recruiter_ids.filter(Boolean).map(String) : [] if (ids.length) return ids return row?.current_recruiter_id ? [String(row.current_recruiter_id)] : [] } function recruiterNamesFrom(row) { if (Array.isArray(row?.recruiter_names) && row.recruiter_names.length) { return row.recruiter_names.filter(Boolean) } if (Array.isArray(row?.recruiters) && row.recruiters.length) { return row.recruiters.map((r) => r?.name).filter(Boolean) } return row?.recruiter_name ? [row.recruiter_name] : [] } /** API row -> what the Jobs table and detail modal render. */ export function toJobView(row) { const recruiterIds = recruiterIdsFrom(row) const recruiterNames = recruiterNamesFrom(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: recruiterNames.join(', ') || null, recruiterId: recruiterIds[0] || null, recruiterIds, recruiterNames, 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 } }) }