Skip to content

Services & API Layer

All service files use the shared Axios instance from src/services/auth.service.ts.


stageType.service.ts

typescript
// 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

typescript
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

typescript
// 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 used rating instead of overallRating) has been replaced by submitFeedback(). getPrivateQuestions() is deferred to Phase 1C.


pipeline.service.ts

typescript
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:

typescript
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.

typescript
// 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

typescript
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)

typescript
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 exactly
  • IJobOpening, IStageConfig — mirrors backend
  • ICandidatePipeline — note: frontend only receives candidateFacingStatus and stageProgression[].candidateStatus from candidate endpoints
  • IInterview — frontend receives projected version from candidate endpoints

Types should live in src/types/ and be shared across service files.