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

116 lines
4.0 KiB
JavaScript

import { request } from '../lib/apiClient'
/* ============================================================
interviews.js — backend/job/app.py `/interview/*`.
THREE READ MODES on one endpoint, selected by which params are present
(backend/job/app.py::fetch_interview):
interview_id -> one row, bare object
inbox_id -> every interview on one application, list
range -> from_date / to_date / status / top
Range mode only engages when at least ONE of from_date, to_date, status or
top is set. With none of them the route raises 400 "interview_id or inbox_id
is required" — so `list()` always sends `top`, and the screens never call it
bare.
Permissions are candidates.*, NOT interviews.* — the eight interviews.* tags
exist in the catalogue but no route reads them. A user holding only
interviews.view gets a 403 here.
============================================================ */
/** Status vocabulary. `interview_status` is a free-text column, so this file is
the only place the spelling is decided; writes and filters share it. */
export const INTERVIEW_STATUSES = ['Scheduled', 'Completed', 'Cancelled', 'No Show']
/** Round vocabulary — same story: free text, pinned here. */
export const INTERVIEW_TYPES = [
'Phone Screen',
'Technical',
'System Design',
'Onsite Loop',
'Hiring Manager',
'Culture Fit',
'Final Round',
]
/** Range read. `top` is always sent so the route takes the range branch. */
export function listRange({ fromDate, toDate, status, top = 200, skip } = {}) {
return request('/interview/fetch', {
params: {
from_date: fromDate,
to_date: toDate,
status,
top,
skip,
},
})
}
export function listByInbox(inboxId) {
return request('/interview/fetch', { params: { inbox_id: inboxId } })
}
/**
* Schedule one interview against an APPLICATION (inbox.id), not a candidate:
* `inbox_id` is the only link the table has, so a candidate with no inbox row
* (a manual upload) cannot be scheduled through this endpoint at all.
*
* interview_date and interview_time are both `datetime` columns, so the same
* instant goes to each rather than inventing a second one — the same rule the
* candidate profile's Interview tab already follows.
*/
export function create({ inboxId, instant, type, status }) {
return request('/interview/create', {
method: 'POST',
body: {
inbox_id: inboxId,
interview_date: instant,
interview_time: instant,
interview_type: type,
interview_status: status,
},
})
}
/** Partial update. Only the keys present are written (exclude_unset server-side). */
export function update(interviewId, { instant, type, status } = {}) {
const body = {}
if (instant != null) {
body.interview_date = instant
body.interview_time = instant
}
if (type != null) body.interview_type = type
if (status != null) body.interview_status = status
return request('/interview/update', {
method: 'PATCH',
params: { interview_id: interviewId },
body,
})
}
/**
* API row -> what the Interviews table, the Calendar grid and the Up Next rail
* render.
*
* serialize_interview returns seven fields and the `interviews` table has no
* more columns than that, so five things the prototype showed have no source:
* meeting mode (video / on-site / phone), duration, interviewer list, the
* feedback verdict and a numeric score. They are absent here rather than
* defaulted, and the screens drop those columns — the same rule Jobs and
* Candidates already follow. `job_title` is likewise absent; the caller
* hydrates it from the application row when it has one.
*/
export function toInterviewView(row) {
const whenRaw = row.interview_date || row.interview_time
const when = whenRaw ? new Date(whenRaw) : null
return {
id: row.id,
inboxId: row.inbox_id,
candidate: row.candidate_name || 'Unknown candidate',
type: row.interview_type || 'Interview',
status: row.interview_status || 'Scheduled',
when: when && !Number.isNaN(when.getTime()) ? when : null,
}
}