"use client"; import { useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import type { CandidateSource, CandidateType, CrewStatus } from "@prisma/client"; import { Badge } from "@/components/ui/badge"; import { AdminDialog } from "@/components/ui/admin-dialog"; import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu"; import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog"; import { createCrewMember, updateCrewMember, deleteCrewMember, placeCrew } from "./actions"; const INPUT = "w-full 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"; const BTN = "rounded-lg bg-primary-600 px-4 py-2 text-sm font-semibold text-white hover:bg-primary-700 disabled:opacity-60"; const SECONDARY = "rounded-lg border border-neutral-300 px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50"; const STATUSES: CrewStatus[] = ["PROSPECT", "CANDIDATE", "EMPLOYEE", "EX_HAND", "BLACKLISTED"]; const SOURCES: CandidateSource[] = ["CAREERS", "EX_HAND", "WALK_IN", "REFERRAL", "OTHER"]; const label = (s: string) => s.replace(/_/g, " ").toLowerCase().replace(/\b\w/g, (m) => m.toUpperCase()); type Opt = { id: string; name: string }; type RankOpt = { id: string; code: string; name: string }; type Crew = { id: string; name: string; status: CrewStatus; type: CandidateType; source: CandidateSource; email: string | null; phone: string | null; employeeId: string | null; appliedRankId: string | null; currentRankId: string | null; currentRank: string | null; experienceMonths: number; hasActiveAssignment: boolean; removable: boolean; }; const STATUS_VARIANT: Record = { PROSPECT: "outline", CANDIDATE: "default", EMPLOYEE: "success", EX_HAND: "secondary", BLACKLISTED: "danger", }; export function AdminCrewManager({ crew, ranks, vessels, sites }: { crew: Crew[]; ranks: RankOpt[]; vessels: Opt[]; sites: Opt[] }) { const [search, setSearch] = useState(""); const filtered = useMemo(() => { const q = search.trim().toLowerCase(); return crew.filter((c) => !q || `${c.name} ${c.employeeId ?? ""}`.toLowerCase().includes(q)); }, [crew, search]); return (

Crew management

{crew.length} crew records · create, edit, place onto a vessel/site, or remove

setSearch(e.target.value)} />
{filtered.length === 0 ? ( ) : filtered.map((c) => )}
Name Employee Status Rank
No crew records.
); } function Row({ c, ranks, vessels, sites }: { c: Crew; ranks: RankOpt[]; vessels: Opt[]; sites: Opt[] }) { const [editOpen, setEditOpen] = useState(false); const [placeOpen, setPlaceOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); return ( {c.name} {c.employeeId ?? "—"} {label(c.status)} {c.currentRank ?? "—"} setEditOpen(true)}>Edit {!c.hasActiveAssignment && setPlaceOpen(true)}>Place onto vessel/site} setDeleteOpen(true)}>Delete deleteCrewMember(c.id)} /> ); } function CrewFormButton({ ranks, editing, open, onOpenChange }: { ranks: RankOpt[]; editing?: Crew; open?: boolean; onOpenChange?: (v: boolean) => void }) { const router = useRouter(); const [internalOpen, setInternalOpen] = useState(false); const isControlled = open !== undefined; const isOpen = isControlled ? open : internalOpen; const setOpen = isControlled ? (onOpenChange ?? (() => {})) : setInternalOpen; const [pending, setPending] = useState(false); const [error, setError] = useState(""); const [f, setF] = useState({ name: editing?.name ?? "", status: editing?.status ?? "CANDIDATE", type: editing?.type ?? "NEW", source: editing?.source ?? "CAREERS", email: editing?.email ?? "", phone: editing?.phone ?? "", appliedRankId: editing?.appliedRankId ?? "", currentRankId: editing?.currentRankId ?? "", experienceMonths: String(editing?.experienceMonths ?? 0), }); async function submit(e: React.FormEvent) { e.preventDefault(); setPending(true); setError(""); const fd = new FormData(); if (editing) fd.set("id", editing.id); Object.entries(f).forEach(([k, v]) => v !== "" && fd.set(k, String(v))); const res = await (editing ? updateCrewMember(fd) : createCrewMember(fd)); setPending(false); if ("error" in res) setError(res.error); else { setOpen(false); router.refresh(); } } return ( <> {!isControlled && } setOpen(false)}>
setF({ ...f, name: e.target.value })} required /> setF({ ...f, email: e.target.value })} /> setF({ ...f, phone: e.target.value })} /> setF({ ...f, experienceMonths: e.target.value })} />
{error &&

{error}

}
); } function PlaceDialog({ crew, ranks, vessels, sites, open, onOpenChange }: { crew: Crew; ranks: RankOpt[]; vessels: Opt[]; sites: Opt[]; open: boolean; onOpenChange: (v: boolean) => void }) { const router = useRouter(); const [pending, setPending] = useState(false); const [error, setError] = useState(""); const [f, setF] = useState({ rankId: crew.currentRankId ?? crew.appliedRankId ?? "", location: "", signOnDate: "" }); async function submit(e: React.FormEvent) { e.preventDefault(); setPending(true); setError(""); const fd = new FormData(); fd.set("crewMemberId", crew.id); fd.set("rankId", f.rankId); if (f.location.startsWith("v:")) fd.set("vesselId", f.location.slice(2)); else if (f.location.startsWith("s:")) fd.set("siteId", f.location.slice(2)); fd.set("signOnDate", f.signOnDate); const res = await placeCrew(fd); setPending(false); if ("error" in res) setError(res.error); else { onOpenChange(false); router.refresh(); } } return ( onOpenChange(false)}>

Assign this crew member directly to a vessel/site — no requisition needed. A candidate is promoted to active crew with an employee number.

setF({ ...f, signOnDate: e.target.value })} required />
{error &&

{error}

}
); }