Services & API Layer
All service files use the shared Axios instance from src/services/auth.service.ts.
stageType.service.ts
// src/services/stageType.service.ts
export const stageTypeApi = {
getAll: () => axiosInstance.get<IStageTypeConfig[]>('/stage-types'),
};Cached in a React Query key or StageTypeContext for the session.
job.service.ts
export const jobApi = {
list: (params?) => axiosInstance.get('/jobs', { params }),
getOne: (id: string) => axiosInstance.get(`/jobs/${id}`),
create: (data) => axiosInstance.post('/jobs', data),
update: (id: string, data) => axiosInstance.patch(`/jobs/${id}`, data),
remove: (id: string) => axiosInstance.delete(`/jobs/${id}`),
};Note on ScreeningQuestion in stage data: The stages[].screeningConfig.questions array now uses { questionId: string; order: number } shape (not embedded text). Screening question text is created/resolved server-side via findOrCreateQuestion().
interview.service.ts
// Interfaces
interface IFeedback {
interviewerEmail: string;
overallRating: number;
traits: string[];
recommendation: 'strong_yes' | 'yes' | 'no' | 'strong_no';
comments: string;
submittedAt: string;
}
interface ICandidateFeedback {
overallRating: number;
traits: string[];
recommendation: 'strong_yes' | 'yes' | 'no' | 'strong_no';
comments?: string;
submittedAt: string;
}
interface Interview {
_id: string;
title: string;
round: string;
stageTypeKey?: string;
status: 'scheduled' | 'in_progress' | 'completed' | 'cancelled' | 'expired' | 'no_show' | 'declined';
meetingLink?: string;
candidatePipelineId?: string;
stageId?: string;
feedbacks: IFeedback[];
candidateFeedback?: ICandidateFeedback;
// ...
}
export const interviewApi = {
create: (data: CreateInterviewData) =>
api.post('/interviews', data),
getAll: (userId, email, jobId?, params?) =>
api.get('/interviews', { params: { userId, email, jobId, ...params } }),
getOne: (id: string): Promise<{ success: boolean; data: Interview }> =>
api.get(`/interviews/${id}`),
rsvp: (id: string, email: string, status: 'accepted' | 'rejected') =>
api.post(`/interviews/${id}/rsvp`, { email, status }),
// Phase 1B — recruiter feedback (triggers pipeline stage completion)
submitFeedback: (id: string, data: {
interviewerEmail: string;
overallRating: number;
traits: string[];
recommendation: 'strong_yes' | 'yes' | 'no' | 'strong_no';
comments: string;
}): Promise<{ success: boolean; data: Interview }> =>
api.post(`/interviews/${id}/feedback`, data),
// Phase 1B — candidate feedback (informational only, no pipeline side-effects)
submitCandidateFeedback: (id: string, data: {
overallRating: number;
traits: string[];
recommendation: 'strong_yes' | 'yes' | 'no' | 'strong_no';
comments?: string;
}): Promise<{ success: boolean; data: Interview }> =>
api.post(`/interviews/${id}/candidate-feedback`, data),
update: (id: string, data: Partial<Interview>) =>
api.put(`/interviews/${id}`, data),
};The old
addFeedback()method (which usedratinginstead ofoverallRating) has been replaced bysubmitFeedback().getPrivateQuestions()is deferred to Phase 1C.
pipeline.service.ts
export const pipelineApi = {
listByJob: (jobId: string) => axiosInstance.get('/pipeline', { params: { jobId } }),
getOne: (id: string) => axiosInstance.get(`/pipeline/${id}`),
setStatus: (id: string, status: string) => axiosInstance.patch(`/pipeline/${id}/status`, { status }),
unlockStage: (id: string, stageIndex: number, force?: boolean) =>
axiosInstance.post(`/pipeline/${id}/unlock-stage`, { stageIndex, force }),
addNote: (id: string, content: string) =>
axiosInstance.post(`/pipeline/${id}/notes`, { content }),
// Candidate Pool
getPool: (params?: { search?, status?, sort?, page?, limit? }) =>
axiosInstance.get('/pipeline/pool', { params }),
getPoolProfile: (participantId: string) =>
axiosInstance.get(`/pipeline/pool/${participantId}`),
};unlockStage with force: true cascade-completes all preceding incomplete stages before unlocking the target. The frontend shows a confirmation dialog listing incomplete stages before sending force: true.
Pool types exported from pipeline.service.ts:
interface CandidatePoolEntry {
participantId: string;
name?: string;
email: string;
stats?: { totalPipelines, totalInterviews, noShowCount };
jobCount: number;
latestStatus: PipelineStatus;
lastActivityAt: string;
}
interface CandidatePoolListResponse {
candidates: CandidatePoolEntry[];
pagination: { total, page, limit, totalPages };
summary: { total, active, hired, shortlisted };
}
interface CandidatePoolDetailResponse {
participant: { _id, name?, email, stats? };
pipelines: CandidatePipeline[];
}screening.service.ts
Uses a separate no-auth Axios instance (no Firebase token attached). Called from the public screening session page.
// src/services/screening.service.ts
const publicAxios = axios.create({ baseURL: import.meta.env.VITE_SERVER_URL + '/v1' });
export const screeningSessionApi = {
getSession: (token: string) =>
publicAxios.get(`/screening/${token}`),
startSession: (token: string) =>
publicAxios.post(`/screening/${token}/start`),
submitSession: (token: string, responses: { questionId: string; response: string }[]) =>
publicAxios.post(`/screening/${token}/submit`, { responses }),
};question.service.ts
export const questionApi = {
getSystemQuestions: () =>
axiosInstance.get<IScreeningQuestion[]>('/screening-questions?source=system'),
createOrFind: (text: string) =>
axiosInstance.post<IScreeningQuestion>('/screening-questions', { text }),
};candidateDashboard.service.ts (Phase 2)
export const candidateDashboardApi = {
getDashboard: () => axiosInstance.get('/candidate/dashboard'),
getPipeline: (id: string) => axiosInstance.get(`/candidate/pipeline/${id}`),
getInterview: (id: string) => axiosInstance.get(`/candidate/interviews/${id}`),
rsvp: (id: string, status: 'accepted' | 'declined') =>
axiosInstance.patch(`/candidate/interviews/${id}/rsvp`, { status }),
updatePreferences: (data) => axiosInstance.patch('/candidate/preferences', data),
};TypeScript Types
Frontend TS types mirror the backend interfaces. Key types to maintain in sync:
IStageTypeConfig— mirrors backend model exactlyIJobOpening,IStageConfig— mirrors backendICandidatePipeline— note: frontend only receivescandidateFacingStatusandstageProgression[].candidateStatusfrom candidate endpointsIInterview— frontend receives projected version from candidate endpoints
Types should live in src/types/ and be shared across service files.