"use client"; import { useMemo, useState } from "react"; import Link from "next/link"; import type { CandidateSource, CandidateType, CrewStatus } from "@prisma/client"; import { Badge } from "@/components/ui/badge"; import { RowActionsMenu, RowActionsItem } from "@/components/ui/row-actions-menu"; import { AddCandidateButton, EditCandidateButton, type EditableCandidate } from "./candidate-form"; import { SOURCE_LABEL, SOURCE_OPTIONS, STATUS_LABEL, STATUS_VARIANT, experienceLabel } from "./candidate-ui"; type CandidateRow = { id: string; name: string; source: CandidateSource; type: CandidateType; status: CrewStatus; appliedRankId: string | null; appliedRank: string | null; currentRankId: string | null; currentRank: string | null; experienceMonths: number; vesselTypeExperience: string | null; email: string | null; phone: string | null; notes: string | null; hasCv: boolean; }; type RankOpt = { id: string; code: string; name: string }; const INPUT = "rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20"; function Chip({ label, onClear }: { label: string; onClear: () => void }) { return ( {label} ); } function toEditable(c: CandidateRow): EditableCandidate { return { id: c.id, name: c.name, source: c.source, appliedRankId: c.appliedRankId, currentRankId: c.currentRankId, experienceMonths: c.experienceMonths, vesselTypeExperience: c.vesselTypeExperience, email: c.email, phone: c.phone, notes: c.notes, }; } function CandidateRowView({ c, ranks }: { c: CandidateRow; ranks: RankOpt[] }) { const [editOpen, setEditOpen] = useState(false); return ( {c.name} {c.type === "EX_HAND" && ( Ex-hand )} {c.hasCv && CV} {SOURCE_LABEL[c.source]} {c.currentRank ?? "—"} {c.appliedRank ?? "—"} {experienceLabel(c.experienceMonths)} {STATUS_LABEL[c.status]}
e.stopPropagation()}> setEditOpen(true)}>Edit
); } export function CandidatesManager({ candidates, ranks }: { candidates: CandidateRow[]; ranks: RankOpt[] }) { const [search, setSearch] = useState(""); const [source, setSource] = useState<"ALL" | CandidateSource>("ALL"); const [appliedRankId, setAppliedRankId] = useState("ALL"); const [minExp, setMinExp] = useState(""); const minExpMonths = minExp ? Math.max(0, parseInt(minExp, 10) || 0) : 0; const filtered = useMemo(() => { const q = search.trim().toLowerCase(); return candidates.filter((c) => { if (source !== "ALL" && c.source !== source) return false; if (appliedRankId !== "ALL" && c.appliedRankId !== appliedRankId) return false; if (minExpMonths && c.experienceMonths < minExpMonths) return false; if (q && !`${c.name} ${c.appliedRank ?? ""} ${c.currentRank ?? ""}`.toLowerCase().includes(q)) return false; return true; }); }, [candidates, search, source, appliedRankId, minExpMonths]); const rankName = (id: string) => ranks.find((r) => r.id === id)?.name ?? id; const hasFilters = Boolean(search) || source !== "ALL" || appliedRankId !== "ALL" || Boolean(minExp); const clearAll = () => { setSearch(""); setSource("ALL"); setAppliedRankId("ALL"); setMinExp(""); }; return (

Candidates

{candidates.length} in the talent pool · careers applicants, ex-hands, walk-ins and referrals

{/* Filters */}
setSearch(e.target.value)} /> setMinExp(e.target.value)} />
{/* Active filter chips + match count */} {hasFilters && (
{search && setSearch("")} />} {source !== "ALL" && setSource("ALL")} />} {appliedRankId !== "ALL" && setAppliedRankId("ALL")} />} {minExp && setMinExp("")} />} {filtered.length} match{filtered.length === 1 ? "" : "es"}
)}
{filtered.length === 0 ? ( ) : ( filtered.map((c) => ) )}
Name Source Rank held Rank applied Experience Status
{candidates.length === 0 ? "No candidates yet. Add the first to the pool." : "No candidates match these filters."}
); }