"use client"; import { useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { ArrowLeft } from "lucide-react"; import type { AssignmentStatus, GateResult, PpeItem, SeafarerDocType, SalaryRateBasis, AppraisalStatus } from "@prisma/client"; import { Badge } from "@/components/ui/badge"; import { AdminDialog } from "@/components/ui/admin-dialog"; import { cn } from "@/lib/utils"; import { uploadDocument, deleteDocument, saveBankEpf, addNextOfKin, deleteNextOfKin, issuePpe, returnPpe, addExperience, signOffCrew, } from "../actions"; import { raiseAppraisal } from "../../appraisals/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-3 py-2 text-sm font-semibold text-white hover:bg-primary-700 disabled:opacity-60"; const LINKBTN = "text-xs font-medium text-danger-600 hover:underline"; const DOC_TYPES: SeafarerDocType[] = ["STCW","AADHAAR","PAN","PASSPORT","CDC","COC","PHOTOGRAPH","DRIVING_LICENSE","MEDICAL_FITNESS","CONTRACT_LETTER"]; const PPE_ITEMS: PpeItem[] = ["BOILER_SUIT","SAFETY_SHOES","HELMET","VEST","GLOVES","MASK","GOGGLES","TIFFIN","TORCH","WALKIE_TALKIE"]; const label = (s: string) => s.replace(/_/g, " ").toLowerCase().replace(/\b\w/g, (m) => m.toUpperCase()); const fmtDate = (iso: string | null) => (iso ? new Date(iso).toLocaleDateString() : "—"); type Doc = { id: string; docType: SeafarerDocType; number: string | null; issueDate: string | null; expiryDate: string | null; verificationStatus: GateResult; hasFile: boolean }; type Nok = { id: string; name: string; relationship: string | null; phone: string | null; address: string | null; isEmergency: boolean }; type Ppe = { id: string; item: PpeItem; size: string | null; quantity: number; issuedDate: string; returnedDate: string | null }; type Exp = { id: string; vesselType: string | null; rank: string | null; fromDate: string | null; toDate: string | null; durationMonths: number | null; source: string }; type Props = { crew: { id: string; name: string; employeeId: string; rank: string; location: string; status: AssignmentStatus | null }; documents: Doc[]; bank: { accountName: string | null; accountNumber: string; ifsc: string | null; bankName: string | null }; epf: { uan: string | null; aadhaar: string; pfNumber: string | null }; nextOfKin: Nok[]; ppe: Ppe[]; experience: Exp[]; paystatus: { showSalary: boolean; salary: { basic: number; rateBasis: SalaryRateBasis; victualingPerDay: number; currency: string } | null }; ranks: { id: string; name: string }[]; perms: { editRecords: boolean; issuePpe: boolean }; signOff: { assignmentId: string | null; canSignOff: boolean }; appraisals: Appr[]; appraisalCtx: { assignmentId: string | null; canRaise: boolean }; }; type Appr = { id: string; period: string; status: AppraisalStatus; comments: string | null; ratings: { competence: number | null; conduct: number | null; safety: number | null } | null }; const TABS = ["Documents", "Bank & EPF", "Next of kin", "PPE", "Experience", "Pay status", "Appraisals"] as const; type Tab = (typeof TABS)[number]; const APPRAISAL_VARIANT: Record = { DRAFT: "outline", SUBMITTED: "warning", MPO_VERIFIED: "default", MANAGER_APPROVED: "success", REJECTED: "danger", }; export function CrewProfile(p: Props) { const [tab, setTab] = useState("Documents"); const router = useRouter(); const refresh = () => router.refresh(); return (
Crew

{p.crew.name}

{p.crew.status === "ACTIVE" && Active} {p.crew.status === "ON_LEAVE" && On leave}
{p.signOff.canSignOff && p.signOff.assignmentId && }

{p.crew.employeeId} · {p.crew.rank} · {p.crew.location}

{TABS.map((t) => ( ))}
{tab === "Documents" && } {tab === "Bank & EPF" && } {tab === "Next of kin" && } {tab === "PPE" && } {tab === "Experience" && } {tab === "Pay status" && } {tab === "Appraisals" && }
); } function Appraisals({ rows, ctx, onDone }: { rows: Appr[]; ctx: { assignmentId: string | null; canRaise: boolean }; onDone: () => void }) { const { pending, error, run } = useRun(onDone); const [f, setF] = useState({ period: "", competence: "3", conduct: "3", safety: "3", comments: "" }); function submit(e: React.FormEvent) { e.preventDefault(); if (!ctx.assignmentId) return; const fd = new FormData(); fd.set("assignmentId", ctx.assignmentId); Object.entries(f).forEach(([k, v]) => v && fd.set(k, v)); run(() => raiseAppraisal(fd), () => setF({ period: "", competence: "3", conduct: "3", safety: "3", comments: "" })); } return (
{rows.length === 0 ?

No appraisals.

: rows.map((a) => (

{a.period} {a.status.replace(/_/g, " ").toLowerCase()}

{a.ratings ? `Competence ${a.ratings.competence ?? "—"} · Conduct ${a.ratings.conduct ?? "—"} · Safety ${a.ratings.safety ?? "—"}` : "—"} {a.comments ? ` · ${a.comments}` : ""}

))} {ctx.canRaise && ctx.assignmentId && (
setF({ ...f, period: e.target.value })} required /> setF({ ...f, comments: e.target.value })} /> {(["competence", "conduct", "safety"] as const).map((k) => ( ))}
)} {!ctx.canRaise &&

Appraisals are raised by the PM and verified by the MPO, then approved by the Manager.

}
); } function Section({ children }: { children: React.ReactNode }) { return
{children}
; } function Err({ msg }: { msg: string }) { return msg ?

{msg}

: null; } function useRun(onDone: () => void) { const [pending, setPending] = useState(false); const [error, setError] = useState(""); async function run(fn: () => Promise<{ ok: true } | { error: string }>, after?: () => void) { setPending(true); setError(""); const res = await fn(); setPending(false); if ("error" in res) setError(res.error); else { after?.(); onDone(); } } return { pending, error, run }; } function docStatus(d: Doc): { label: string; variant: "success" | "warning" | "danger" | "secondary" } { if (d.expiryDate && new Date(d.expiryDate) < new Date()) return { label: "Expired", variant: "danger" }; if (d.verificationStatus === "VERIFIED") return { label: "Verified", variant: "success" }; if (d.verificationStatus === "REJECTED") return { label: "Rejected", variant: "danger" }; return { label: "Pending", variant: "warning" }; } function Documents({ crewId, docs, canEdit, onDone }: { crewId: string; docs: Doc[]; canEdit: boolean; onDone: () => void }) { const { pending, error, run } = useRun(onDone); const [f, setF] = useState({ docType: "PASSPORT", number: "", issueDate: "", expiryDate: "" }); const [file, setFile] = useState(null); function submit(e: React.FormEvent) { e.preventDefault(); const fd = new FormData(); fd.set("crewMemberId", crewId); Object.entries(f).forEach(([k, v]) => v && fd.set(k, v)); if (file) fd.set("file", file); run(() => uploadDocument(fd), () => { setF({ docType: "PASSPORT", number: "", issueDate: "", expiryDate: "" }); setFile(null); }); } return (
{docs.length === 0 ?

No documents.

: ( {docs.map((d) => { const s = docStatus(d); return ( ); })}
DocumentNumberIssuedExpiresStatus
{label(d.docType)}{d.hasFile && file} {d.number ?? "—"} {fmtDate(d.issueDate)} {fmtDate(d.expiryDate)} {s.label} {canEdit && }
)} {canEdit && (
setF({ ...f, number: e.target.value })} /> setFile(e.target.files?.[0] ?? null)} />
)}
); } function Row({ k, v }: { k: string; v: string | null }) { return
{k}{v ?? "—"}
; } function BankEpf({ crewId, bank, epf, canEdit, onDone }: { crewId: string; bank: Props["bank"]; epf: Props["epf"]; canEdit: boolean; onDone: () => void }) { const { pending, error, run } = useRun(onDone); const [edit, setEdit] = useState(false); const [f, setF] = useState({ accountName: bank.accountName ?? "", accountNumber: "", ifsc: bank.ifsc ?? "", bankName: bank.bankName ?? "", uan: epf.uan ?? "", aadhaarLast4: "", pfNumber: epf.pfNumber ?? "" }); function submit(e: React.FormEvent) { e.preventDefault(); const fd = new FormData(); fd.set("crewMemberId", crewId); Object.entries(f).forEach(([k, v]) => v && fd.set(k, v)); run(() => saveBankEpf(fd), () => setEdit(false)); } return (
Sensitive — account and Aadhaar numbers are masked unless you are Accounts.
{canEdit && !edit && } {canEdit && edit && (
setF({ ...f, accountName: e.target.value })} /> setF({ ...f, accountNumber: e.target.value })} /> setF({ ...f, ifsc: e.target.value })} /> setF({ ...f, bankName: e.target.value })} /> setF({ ...f, uan: e.target.value })} /> setF({ ...f, aadhaarLast4: e.target.value })} /> setF({ ...f, pfNumber: e.target.value })} />
)}
); } function NextOfKinTab({ crewId, rows, canEdit, onDone }: { crewId: string; rows: Nok[]; canEdit: boolean; onDone: () => void }) { const { pending, error, run } = useRun(onDone); const [f, setF] = useState({ name: "", relationship: "", phone: "", address: "", isEmergency: false }); function submit(e: React.FormEvent) { e.preventDefault(); const fd = new FormData(); fd.set("crewMemberId", crewId); fd.set("name", f.name); if (f.relationship) fd.set("relationship", f.relationship); if (f.phone) fd.set("phone", f.phone); if (f.address) fd.set("address", f.address); if (f.isEmergency) fd.set("isEmergency", "true"); run(() => addNextOfKin(fd), () => setF({ name: "", relationship: "", phone: "", address: "", isEmergency: false })); } return (
{rows.length === 0 ?

No next of kin recorded.

: rows.map((n) => (

{n.name} {n.isEmergency && Emergency}

{[n.relationship, n.phone, n.address].filter(Boolean).join(" · ") || "—"}

{canEdit && }
))} {canEdit && (
setF({ ...f, name: e.target.value })} required /> setF({ ...f, relationship: e.target.value })} /> setF({ ...f, phone: e.target.value })} /> setF({ ...f, address: e.target.value })} />
)}
); } function PpeTab({ crewId, rows, canIssue, onDone }: { crewId: string; rows: Ppe[]; canIssue: boolean; onDone: () => void }) { const { pending, error, run } = useRun(onDone); const [f, setF] = useState({ item: "BOILER_SUIT", size: "", quantity: "1", comment: "" }); function submit(e: React.FormEvent) { e.preventDefault(); const fd = new FormData(); fd.set("crewMemberId", crewId); Object.entries(f).forEach(([k, v]) => v && fd.set(k, v)); run(() => issuePpe(fd), () => setF({ item: "BOILER_SUIT", size: "", quantity: "1", comment: "" })); } return (
{rows.length === 0 ?

No PPE issued.

: ( {rows.map((r) => ( ))}
ItemSizeQtyIssuedStatus
{label(r.item)} {r.size ?? "—"} {r.quantity} {fmtDate(r.issuedDate)} {r.returnedDate ? Returned : Issued} {canIssue && !r.returnedDate && }
)} {canIssue && (
setF({ ...f, size: e.target.value })} /> setF({ ...f, quantity: e.target.value })} /> setF({ ...f, comment: e.target.value })} />
)}
); } function ExperienceTab({ crewId, rows, ranks, canEdit, onDone }: { crewId: string; rows: Exp[]; ranks: { id: string; name: string }[]; canEdit: boolean; onDone: () => void }) { const { pending, error, run } = useRun(onDone); const [f, setF] = useState({ vesselType: "", rankId: "", fromDate: "", toDate: "", durationMonths: "" }); function submit(e: React.FormEvent) { e.preventDefault(); const fd = new FormData(); fd.set("crewMemberId", crewId); Object.entries(f).forEach(([k, v]) => v && fd.set(k, v)); run(() => addExperience(fd), () => setF({ vesselType: "", rankId: "", fromDate: "", toDate: "", durationMonths: "" })); } return (
{rows.length === 0 ?

No experience records.

: rows.map((r) => (

{r.rank ?? "—"}{r.vesselType ? ` · ${r.vesselType}` : ""}

{fmtDate(r.fromDate)} – {fmtDate(r.toDate)}{r.durationMonths ? ` · ${r.durationMonths} mo` : ""} · {r.source}

))} {canEdit && (
setF({ ...f, vesselType: e.target.value })} /> setF({ ...f, durationMonths: e.target.value })} />
)}
); } function PayStatus({ paystatus }: { paystatus: Props["paystatus"] }) { return (
{!paystatus.showSalary ? (

Net pay is visible to office roles only. Site staff see pay status once monthly wage reports are generated.

) : paystatus.salary ? ( <> ) : (

No salary structure on file.

)}

Monthly pay rows (paid / processing) arrive with payroll wage reports in a later phase.

); } function SignOffButton({ assignmentId, crewName }: { assignmentId: string; crewName: string }) { const router = useRouter(); const [open, setOpen] = useState(false); const [date, setDate] = useState(""); const [remarks, setRemarks] = useState(""); const [pending, setPending] = useState(false); const [error, setError] = useState(""); async function submit(e: React.FormEvent) { e.preventDefault(); setPending(true); setError(""); const res = await signOffCrew(assignmentId, date, remarks); setPending(false); if ("error" in res) setError(res.error); else { setOpen(false); router.push("/crewing/crew"); } } return ( <> setOpen(false)}>

Ends this tour: the assignment closes, a tour record is added to Experience, and the crew member returns to the Candidates pool as an ex-hand. A backfill requisition is auto-raised.

setDate(e.target.value)} required />
setRemarks(e.target.value)} placeholder="Optional" />
{error &&

{error}

}
); }