pelagia-portal/App/app/(portal)/crewing/candidates/candidate-ui.ts
Hardik be6db075dc
All checks were successful
PR checks / checks (pull_request) Successful in 36s
PR checks / integration (pull_request) Successful in 27s
feat(crewing): Phase 3a — candidates / talent pool (flagged)
First slice of Phase 3 (Epics B/C/D shipped as stacked sub-PRs). Adds the
CrewMember talent-pool spine and the Candidates screens. Behind
NEXT_PUBLIC_CREWING_ENABLED; production unchanged. Stacks on the requisitions
branch (Phase 2).

What's in
- Schema (crewing_candidates migration): CrewMember (spine) + CrewStatus,
  CandidateType, CandidateSource enums; CrewAction gains a nullable crewMemberId;
  CrewActionType += CANDIDATE_ADDED/UPDATED. employeeId is assigned at onboarding
  (3c), so it's nullable here.
- Actions (crewing/candidates/actions.ts): addCandidate / updateCandidate —
  guard flag + manage_candidates, write a CrewAction, optional CV upload via
  buildStorageKey("cv", …) + uploadBuffer (no parsing — A2 deferred). EX_HAND
  source ⇒ type/status EX_HAND; edits never downgrade an EMPLOYEE.
- Screens: /crewing/candidates (master list with search/source/rank-applied/
  min-experience filters as removable chips + match count + Clear all; Add-candidate
  modal) and /crewing/candidates/[id] (profile; pipeline stepper is 3b). Candidates
  added to the flag-gated Crewing nav (Manager + MPO).

Tests & docs
- Integration: candidates.test.ts (7) — add/update, ex-hand derivation, employee
  no-downgrade, permission gating. type-check clean; full unit (225) + integration
  (153) suites green.
- CLAUDE.md "Crewing" section updated with the Phase 3a surface.

Deferred: public careers intake API (A2, §13 open question); CV parsing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 18:23:01 +05:30

38 lines
1.1 KiB
TypeScript

import type { CandidateSource, CrewStatus } from "@prisma/client";
import type { BadgeProps } from "@/components/ui/badge";
type Variant = NonNullable<BadgeProps["variant"]>;
export const SOURCE_LABEL: Record<CandidateSource, string> = {
CAREERS: "Careers",
EX_HAND: "Ex-hand",
WALK_IN: "Walk-in",
REFERRAL: "Referral",
OTHER: "Other",
};
export const SOURCE_OPTIONS: CandidateSource[] = ["CAREERS", "EX_HAND", "WALK_IN", "REFERRAL", "OTHER"];
export const STATUS_LABEL: Record<CrewStatus, string> = {
PROSPECT: "Prospect",
CANDIDATE: "Candidate",
EMPLOYEE: "Employee",
EX_HAND: "Ex-hand",
BLACKLISTED: "Blacklisted",
};
export const STATUS_VARIANT: Record<CrewStatus, Variant> = {
PROSPECT: "outline",
CANDIDATE: "default",
EMPLOYEE: "success",
EX_HAND: "secondary",
BLACKLISTED: "danger",
};
// Compact experience label, e.g. "3y 6m", "8m", "—".
export function experienceLabel(months: number): string {
if (!months) return "—";
const y = Math.floor(months / 12);
const m = months % 12;
return [y ? `${y}y` : "", m ? `${m}m` : ""].filter(Boolean).join(" ") || "0m";
}