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, } }