Rank held applies to every candidate, not just ex-hands; it auto-updates for returning crew on sign-off. Ex-hand designation is decoupled from the Source dropdown and owned by the office: - Candidate form: drop the EX_HAND source option, relabel "Rank held (ex-hands)" to "Rank held". addCandidate always intakes NEW/CANDIDATE (ex-hand recognition still reuses an existing EX_HAND row); updateCandidate no longer rewrites type/status, so an admin-set EX_HAND or onboarded EMPLOYEE is never clobbered by a candidate edit. - Admin crew form: the type NEW/EX_HAND select becomes an "Ex-hand (returning crew)" checkbox -- the only place ex-hand is tagged. - List/detail ex-hand indicators key on type === EX_HAND (not source). - Sign-off preserves the original recruitment source when flipping to EX_HAND. - Tests seed EX_HAND rows directly; assert candidate intake stays NEW. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
55 lines
1.9 KiB
TypeScript
55 lines
1.9 KiB
TypeScript
import { auth } from "@/auth";
|
|
import { db } from "@/lib/db";
|
|
import { hasPermission } from "@/lib/permissions";
|
|
import { CREWING_ENABLED } from "@/lib/feature-flags";
|
|
import { redirect, notFound } from "next/navigation";
|
|
import { CandidatesManager } from "./candidates-manager";
|
|
import type { Metadata } from "next";
|
|
|
|
export const metadata: Metadata = { title: "Candidates" };
|
|
|
|
export default async function CandidatesPage() {
|
|
if (!CREWING_ENABLED) notFound();
|
|
|
|
const session = await auth();
|
|
if (!session?.user) redirect("/login");
|
|
if (!hasPermission(session.user.role, "manage_candidates")) redirect("/dashboard");
|
|
|
|
const [candidates, ranks] = await Promise.all([
|
|
db.crewMember.findMany({
|
|
// Active employees live in the Crew directory (Phase 4); the pool is
|
|
// everyone still a candidate / ex-hand (spec §8.6 R9).
|
|
where: { status: { not: "EMPLOYEE" } },
|
|
orderBy: { createdAt: "desc" },
|
|
include: {
|
|
appliedRank: { select: { name: true } },
|
|
currentRank: { select: { name: true } },
|
|
},
|
|
}),
|
|
db.rank.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, code: true, name: true } }),
|
|
]);
|
|
|
|
const rows = candidates.map((c) => ({
|
|
id: c.id,
|
|
name: c.name,
|
|
source: c.source,
|
|
type: c.type,
|
|
status: c.status,
|
|
appliedRankId: c.appliedRankId,
|
|
appliedRank: c.appliedRank?.name ?? null,
|
|
currentRankId: c.currentRankId,
|
|
currentRank: c.currentRank?.name ?? null,
|
|
experienceMonths: c.experienceMonths,
|
|
vesselTypeExperience: c.vesselTypeExperience,
|
|
email: c.email,
|
|
phone: c.phone,
|
|
notes: c.notes,
|
|
hasCv: Boolean(c.cvKey),
|
|
}));
|
|
|
|
// B3 AC2 — ex-hands (proven crew) surface above new candidates by default.
|
|
// Stable sort preserves the createdAt-desc order within each group.
|
|
rows.sort((a, b) => Number(b.status === "EX_HAND") - Number(a.status === "EX_HAND"));
|
|
|
|
return <CandidatesManager candidates={rows} ranks={ranks} />;
|
|
}
|