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>
38 lines
1.1 KiB
TypeScript
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";
|
|
}
|