From 3ec3a2b4ef5ece7a1253a4b892ce84629a7c235a Mon Sep 17 00:00:00 2001 From: Hardik Date: Mon, 22 Jun 2026 18:49:12 +0530 Subject: [PATCH] =?UTF-8?q?feat(crewing):=20Phase=203b=20=E2=80=94=20recru?= =?UTF-8?q?itment=20pipeline=20(flagged)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second slice of Phase 3 (stacked on 3a candidates). The gated 7-stage recruitment pipeline per Crewing-Implementation-Spec §5.1/§8.4–8.5/§8.13. Behind NEXT_PUBLIC_CREWING_ENABLED; production unchanged. What's in - Schema (crewing_pipeline migration): Application (one per requisition+candidate) + 7-stage ApplicationStage; ApplicationGate (SALARY/SELECTION/WAIVER pending = Manager queue items); ReferenceCheck; effective-dated SalaryStructure (attached to the Application now, bound to the assignment in 3c); minimal BankDetail/EpfDetail captured at DOC_VERIFICATION (PII encryption deferred to Phase 4). CrewAction += applicationId; pipeline CrewActionTypes. - State machine: lib/application-pipeline.ts — sourcing advances MPO/Manager; approve_salary + select are Manager-only; orthogonal canReject; BOARD_STAGES. - Actions: addApplication (first candidate → requisition SHORTLISTING), advanceStage, recordReferenceCheck, verifyDocuments (bank/EPF), agreeSalary→approveSalary/returnSalary, recordInterviewResult, requestInterviewWaiver→approve/decline, selectCandidate (→ requisition SELECTED)/returnSelection, rejectApplication. Waiver never automatic (R2). Notifications SALARY/SELECTION/WAIVER + CANDIDATE_PROPOSED. - Screens: pipeline board per requisition (7 columns + Add candidate); application workhorse (7-step stepper + adaptive per-stage action card); "Open pipeline" on the requisition detail. Central /approvals gains a crewing section (inline Approve/Return) for one unified Manager queue (§8.13 R8). Tests & docs - Unit: application-pipeline.test.ts (9). Integration: applications.test.ts (10) — full happy path, salary/selection/waiver approvals + Manager-only gating, failed interview, reject, site-staff lockout. type-check clean; full unit (234) + integration (163) green. - CLAUDE.md "Crewing" updated with the Phase 3b surface. Deferred: onboarding (Epic D, Phase 3c) — SELECTED → ONBOARDED, CrewAssignment, employeeId, requisition → FILLED, salary bound to the assignment. Co-Authored-By: Claude Opus 4.8 (1M context) --- App/CLAUDE.md | 8 + .../(portal)/approvals/crewing-approvals.tsx | 113 ++++ App/app/(portal)/approvals/page.tsx | 47 ++ .../crewing/applications/[id]/page.tsx | 142 +++++ .../(portal)/crewing/applications/actions.ts | 537 ++++++++++++++++++ .../applications/application-action-card.tsx | 347 +++++++++++ .../crewing/applications/application-ui.ts | 47 ++ .../crewing/requisitions/[id]/page.tsx | 26 +- .../requisitions/[id]/pipeline/page.tsx | 63 ++ .../[id]/pipeline/pipeline-board.tsx | 125 ++++ App/lib/application-pipeline.ts | 99 ++++ App/lib/notifier.ts | 10 +- App/lib/requisition-service.ts | 7 + .../migration.sql | 168 ++++++ App/prisma/schema.prisma | 176 +++++- App/tests/integration/applications.test.ts | 209 +++++++ App/tests/unit/application-pipeline.test.ts | 74 +++ 17 files changed, 2189 insertions(+), 9 deletions(-) create mode 100644 App/app/(portal)/approvals/crewing-approvals.tsx create mode 100644 App/app/(portal)/crewing/applications/[id]/page.tsx create mode 100644 App/app/(portal)/crewing/applications/actions.ts create mode 100644 App/app/(portal)/crewing/applications/application-action-card.tsx create mode 100644 App/app/(portal)/crewing/applications/application-ui.ts create mode 100644 App/app/(portal)/crewing/requisitions/[id]/pipeline/page.tsx create mode 100644 App/app/(portal)/crewing/requisitions/[id]/pipeline/pipeline-board.tsx create mode 100644 App/lib/application-pipeline.ts create mode 100644 App/prisma/migrations/20260622130627_crewing_pipeline/migration.sql create mode 100644 App/tests/integration/applications.test.ts create mode 100644 App/tests/unit/application-pipeline.test.ts diff --git a/App/CLAUDE.md b/App/CLAUDE.md index 0820d46..1238bf5 100644 --- a/App/CLAUDE.md +++ b/App/CLAUDE.md @@ -143,6 +143,14 @@ A crew-management module built incrementally per the **wiki `Crewing-Implementat - **Screens:** `/crewing/candidates` (master list with search / source / rank-applied / min-experience filters rendered as removable chips + match count + Clear all; Add-candidate modal) and `/crewing/candidates/[id]` (profile; the 7-stage pipeline/stepper is 3b). **Candidates** added to the flag-gated Crewing nav (Manager + MPO). - **Deferred:** the public careers intake API (A2, §13 open question) — 3a uses the internal Add-candidate modal only; CVs are stored but not parsed. +**Phase 3b — Recruitment pipeline (Epic C; spec §5.1/§8.4–8.5/§8.13):** + +- **Models:** `Application` (one per requisition+candidate) drives the 7-stage `ApplicationStage` (`SHORTLISTED → COMPETENCY_AND_REFERENCES → DOC_VERIFICATION → SALARY_AGREEMENT → PROPOSED → INTERVIEW → SELECTED`; `→ REJECTED`; `ONBOARDED` is 3c). `ApplicationGate` records each vetting gate — `SALARY` / `SELECTION` / `WAIVER` gates with `result=PENDING` are the Manager's queue items. `ReferenceCheck`, effective-dated `SalaryStructure` (attached to the Application in 3b; bound to the assignment in 3c), and minimal `BankDetail` / `EpfDetail` captured at DOC_VERIFICATION (PII encryption deferred to Phase 4). `CrewAction` gained `applicationId`. +- **State machine:** `lib/application-pipeline.ts` (mirrors po/requisition machines) — sourcing advances are MPO/Manager; `approve_salary` and `select` are Manager-only; `canReject` is orthogonal. `BOARD_STAGES` is the 7 columns. +- **Actions** (`app/(portal)/crewing/applications/actions.ts`): `addApplication` (first candidate moves the requisition OPEN→SHORTLISTING), `advanceStage`, `recordReferenceCheck`, `verifyDocuments` (captures bank/EPF), `agreeSalary`→`approveSalary`/`returnSalary`, `recordInterviewResult`, `requestInterviewWaiver`→`approveInterviewWaiver`/`declineInterviewWaiver`, `selectCandidate`/`returnSelection` (sets requisition→SELECTED), `rejectApplication`. Waiver is **never automatic** (R2). Notifications: `SALARY_FOR_APPROVAL` / `SELECTION_FOR_APPROVAL` / `WAIVER_REQUESTED` (+ `CANDIDATE_PROPOSED`). +- **Screens:** pipeline board per requisition (`/crewing/requisitions/[id]/pipeline`, 7 columns + Add-candidate), the application workhorse (`/crewing/applications/[id]` — 7-step stepper + adaptive per-stage action card), and an **"Open pipeline"** action on the requisition detail. +- **Central approvals (§8.13 R8):** `/approvals` now also lists pending crewing gates (Salary / Selection / Waiver) with inline Approve/Return, alongside POs — one unified Manager queue. + ### GST Calculation `totalAmount = sum(quantity × unitPrice × (1 + gstRate))` for each line item. The `gstRate` is stored as a decimal on `POLineItem` (e.g., `0.18` = 18%). This applies in Server Actions when computing `totalPrice` per line and the PO `totalAmount`. diff --git a/App/app/(portal)/approvals/crewing-approvals.tsx b/App/app/(portal)/approvals/crewing-approvals.tsx new file mode 100644 index 0000000..22ff719 --- /dev/null +++ b/App/app/(portal)/approvals/crewing-approvals.tsx @@ -0,0 +1,113 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { Badge } from "@/components/ui/badge"; +import { AdminDialog } from "@/components/ui/admin-dialog"; +import { + approveSalary, + returnSalary, + selectCandidate, + returnSelection, + approveInterviewWaiver, + declineInterviewWaiver, +} from "../crewing/applications/actions"; + +export type CrewApprovalKind = "SALARY" | "SELECTION" | "WAIVER"; + +export type CrewApprovalItem = { + applicationId: string; + kind: CrewApprovalKind; + candidateName: string; + rank: string; + requisitionCode: string; + detail: string; // amount for salary, etc. +}; + +const KIND_LABEL: Record = { SALARY: "Salary", SELECTION: "Selection", WAIVER: "Waiver" }; +const KIND_VARIANT = { SALARY: "warning", SELECTION: "default", WAIVER: "secondary" } as const; + +const approveFn: Record Promise<{ ok: true } | { error: string }>> = { + SALARY: approveSalary, + SELECTION: selectCandidate, + WAIVER: approveInterviewWaiver, +}; +const returnFn: Record Promise<{ ok: true } | { error: string }>> = { + SALARY: returnSalary, + SELECTION: returnSelection, + WAIVER: declineInterviewWaiver, +}; + +function Row({ item }: { item: CrewApprovalItem }) { + const router = useRouter(); + const [pending, setPending] = useState(false); + const [error, setError] = useState(""); + const [returnOpen, setReturnOpen] = useState(false); + const [reason, setReason] = useState(""); + + async function approve() { + setPending(true); setError(""); + const res = await approveFn[item.kind](item.applicationId); + setPending(false); + if ("error" in res) setError(res.error); else router.refresh(); + } + async function doReturn(e: React.FormEvent) { + e.preventDefault(); + setPending(true); setError(""); + const res = await returnFn[item.kind](item.applicationId, reason); + setPending(false); + if ("error" in res) setError(res.error); else { setReturnOpen(false); router.refresh(); } + } + + return ( + + {KIND_LABEL[item.kind]} + + {item.candidateName} + {item.rank} · {item.requisitionCode} + + {item.detail} + +
+ + +
+ {error &&

{error}

} + setReturnOpen(false)}> +
+