import type { ApplicationStage, Role } from "@prisma/client"; // Recruitment pipeline state machine (Crewing-Implementation-Spec §5.1) — mirrors // po-state-machine / requisition-state-machine. The 7 board stages advance in // order; ONBOARDED is the terminal system state set at onboarding (Phase 3c); // REJECTED is an orthogonal branch reachable from any active stage. // // Stage advances are modelled here. The within-stage work — recording reference // checks, capturing bank/EPF, agreeing the salary, recording the interview // result, requesting a waiver — happens in server actions; this machine governs // when a candidate may move to the next column and who may move them. // // Manager-gated advances (spec §6): SALARY_AGREEMENT → PROPOSED (salary approval) // and INTERVIEW → SELECTED (final selection) are Manager-only. The interview // waiver is a separate Manager-approved action (R2), never automatic. export type ApplicationAction = | "start_competency" // SHORTLISTED → COMPETENCY_AND_REFERENCES | "verify_competency" // COMPETENCY_AND_REFERENCES → DOC_VERIFICATION | "verify_docs" // DOC_VERIFICATION → SALARY_AGREEMENT | "approve_salary" // SALARY_AGREEMENT → PROPOSED (Manager) | "propose_accepted" // PROPOSED → INTERVIEW | "select" // INTERVIEW → SELECTED (Manager) | "onboard"; // SELECTED → ONBOARDED (Phase 3c) interface Transition { to: ApplicationStage; allowedRoles: Role[]; } type TransitionMap = Partial>; const SOURCING_ROLES: Role[] = ["MANNING", "MANAGER", "SUPERUSER"]; const MANAGER_ROLES: Role[] = ["MANAGER", "SUPERUSER"]; const TRANSITIONS: Partial> = { SHORTLISTED: { start_competency: { to: "COMPETENCY_AND_REFERENCES", allowedRoles: SOURCING_ROLES }, }, COMPETENCY_AND_REFERENCES: { verify_competency: { to: "DOC_VERIFICATION", allowedRoles: SOURCING_ROLES }, }, DOC_VERIFICATION: { verify_docs: { to: "SALARY_AGREEMENT", allowedRoles: SOURCING_ROLES }, }, SALARY_AGREEMENT: { // Manager approves the agreed salary structure (spec §6). approve_salary: { to: "PROPOSED", allowedRoles: MANAGER_ROLES }, }, PROPOSED: { propose_accepted: { to: "INTERVIEW", allowedRoles: SOURCING_ROLES }, }, INTERVIEW: { // Final selection is a Manager approval (spec §6). The action enforces that // the interview was accepted or a Manager-approved waiver is in place (R2). select: { to: "SELECTED", allowedRoles: MANAGER_ROLES }, }, SELECTED: { // The onboarding side-effect (Phase 3c) moves SELECTED → ONBOARDED. onboard: { to: "ONBOARDED", allowedRoles: SOURCING_ROLES }, }, }; // The 7 visible board columns, in order (spec §8.4). ONBOARDED/REJECTED are not // board columns — they are terminal/branch states. export const BOARD_STAGES: ApplicationStage[] = [ "SHORTLISTED", "COMPETENCY_AND_REFERENCES", "DOC_VERIFICATION", "SALARY_AGREEMENT", "PROPOSED", "INTERVIEW", "SELECTED", ]; export function getTransition(from: ApplicationStage, action: ApplicationAction): Transition | null { return TRANSITIONS[from]?.[action] ?? null; } export function canPerformAction(from: ApplicationStage, action: ApplicationAction, role: Role): boolean { return getTransition(from, action)?.allowedRoles.includes(role) ?? false; } export function getAvailableActions(stage: ApplicationStage, role: Role): ApplicationAction[] { const map = TRANSITIONS[stage]; if (!map) return []; return (Object.keys(map) as ApplicationAction[]).filter((a) => canPerformAction(stage, a, role)); } // ── Rejection (orthogonal) ─────────────────────────────────────────────────── // A candidate may be rejected with remarks from any active stage (not once // SELECTED/ONBOARDED, and not again if already REJECTED), by MPO or Manager. export const REJECT_ROLES: Role[] = ["MANNING", "MANAGER", "SUPERUSER"]; const TERMINAL: ApplicationStage[] = ["SELECTED", "ONBOARDED", "REJECTED"]; export function canReject(from: ApplicationStage, role: Role): boolean { return !TERMINAL.includes(from) && REJECT_ROLES.includes(role); }