"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import type { SeafarerDocType } from "@prisma/client";
import { AdminDialog } from "@/components/ui/admin-dialog";
import { verifyDocument, verifyBankEpf, verifyPpe, verifyNextOfKin, recordEpfoCheck } from "./actions";
import { verifyAppraisal } from "../appraisals/actions";
import type { PpeItem } from "@prisma/client";
const label = (s: string) => s.replace(/_/g, " ").toLowerCase().replace(/\b\w/g, (m) => m.toUpperCase());
const fmt = (iso: string | null) => (iso ? new Date(iso).toLocaleDateString() : "—");
const isExpired = (iso: string | null) => Boolean(iso && new Date(iso) < new Date());
type Doc = { id: string; crewName: string; location: string; docType: SeafarerDocType; number: string | null; expiryDate: string | null; submitted: string };
type Bank = { crewMemberId: string; crewName: string; accountName: string | null; accountNumber: string | null; ifsc: string | null; bankName: string | null };
type Epf = { crewMemberId: string; crewName: string; uan: string | null; aadhaarLast4: string | null; pfNumber: string | null };
type Appr = { id: string; crewName: string; rank: string; period: string; comments: string | null };
type Ppe = { id: string; crewName: string; item: PpeItem; size: string | null };
type Nok = { id: string; crewName: string; name: string; relationship: string | null };
function Actions({ onVerify, onReject }: { onVerify: () => Promise<{ ok: true } | { error: string }>; onReject: (reason: string) => Promise<{ ok: true } | { error: string }> }) {
const router = useRouter();
const [pending, setPending] = useState(false);
const [error, setError] = useState("");
const [open, setOpen] = useState(false);
const [reason, setReason] = useState("");
async function verify() {
setPending(true); setError("");
const res = await onVerify();
setPending(false);
if ("error" in res) setError(res.error); else router.refresh();
}
async function reject(e: React.FormEvent) {
e.preventDefault();
setPending(true); setError("");
const res = await onReject(reason);
setPending(false);
if ("error" in res) setError(res.error); else { setOpen(false); router.refresh(); }
}
return (
{error &&
{error}
}
setOpen(false)}>
);
}
// EPFO assisted lookup (Accounts): OTP handshake against EpfoService via /api/epfo,
// then record the returned member name onto the EpfDetail (A3). Aadhaar is not
// checked here (UIDAI-restricted — stays manual).
function EpfoAssist({ crewMemberId, uan }: { crewMemberId: string; uan: string | null }) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [step, setStep] = useState<"start" | "otp" | "result">("start");
const [sessionId, setSessionId] = useState("");
const [mobileHint, setMobileHint] = useState("");
const [otp, setOtp] = useState("");
const [result, setResult] = useState<{ matched: boolean; name: string | null } | null>(null);
const [pending, setPending] = useState(false);
const [error, setError] = useState("");
if (!uan) return null;
function reset() { setStep("start"); setSessionId(""); setOtp(""); setResult(null); setError(""); setMobileHint(""); }
async function requestOtp() {
setPending(true); setError("");
try {
const r = await fetch("/api/epfo/otp", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ uan }) });
const d = await r.json();
if (!r.ok) throw new Error(d.error || "Failed to request OTP");
setSessionId(d.sessionId); setMobileHint(d.mobileHint || ""); setStep("otp");
} catch (e) { setError(String(e instanceof Error ? e.message : e)); }
setPending(false);
}
async function verify() {
setPending(true); setError("");
try {
const r = await fetch("/api/epfo", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId, uan, otp }) });
const d = await r.json();
if (!r.ok) throw new Error(d.error || "Lookup failed");
setResult({ matched: Boolean(d.matched), name: d.name ?? null }); setStep("result");
} catch (e) { setError(String(e instanceof Error ? e.message : e)); }
setPending(false);
}
async function record() {
setPending(true);
await recordEpfoCheck(crewMemberId, result?.name ?? null);
setPending(false); setOpen(false); reset(); router.refresh();
}
return (
<>
setOpen(false)}>
Assisted UAN lookup via the EPFO portal. An OTP is sent to the member's registered mobile. (Aadhaar is verified manually — not via this check.)
UAN: {uan}
{step === "start" && (
)}
{step === "otp" && (
)}
{step === "result" && (
{result?.matched ? (
Matched — EPFO member: {result.name}
) : (
No matching EPFO member for this UAN.
)}
)}
{error &&
{error}
}
>
);
}
function Card({ title, sub, empty, children }: { title: string; sub: string; empty: boolean; children: React.ReactNode }) {
return (
{title}
{sub}
{empty ?
Nothing awaiting verification.
: (
)}
);
}
export function VerificationManager({ docs, bank, epf, appraisals, ppe, nok, canDocs, canBankEpf, canAppraisals }: { docs: Doc[]; bank: Bank[]; epf: Epf[]; appraisals: Appr[]; ppe: Ppe[]; nok: Nok[]; canDocs: boolean; canBankEpf: boolean; canAppraisals: boolean }) {
return (
Verification
Site-entered records awaiting office verification.
{canDocs && (
| Crew | Vessel / site | Document | Expiry | Submitted | |
{docs.map((d) => (
| {d.crewName} |
{d.location} |
{label(d.docType)}{d.number ? ` · ${d.number}` : ""} |
{d.expiryDate ? {fmt(d.expiryDate)}{isExpired(d.expiryDate) ? " · expired" : ""} : "—"} |
{fmt(d.submitted)} |
verifyDocument(d.id, true)} onReject={(r) => verifyDocument(d.id, false, r)} /> |
))}
)}
{canDocs && (
| Crew | Item | Size | |
{ppe.map((r) => (
| {r.crewName} |
{label(r.item)} |
{r.size ?? "—"} |
verifyPpe(r.id, true)} onReject={(x) => verifyPpe(r.id, false, x)} /> |
))}
)}
{canDocs && (
| Crew | Contact | Relationship | |
{nok.map((r) => (
| {r.crewName} |
{r.name} |
{r.relationship ?? "—"} |
verifyNextOfKin(r.id, true)} onReject={(x) => verifyNextOfKin(r.id, false, x)} /> |
))}
)}
{canBankEpf && (
| Crew | Account | IFSC | Bank | |
{bank.map((b) => (
| {b.crewName} |
{b.accountNumber ?? "—"}{b.accountName ? ` (${b.accountName})` : ""} |
{b.ifsc ?? "—"} |
{b.bankName ?? "—"} |
verifyBankEpf(b.crewMemberId, "bank", true)} onReject={(r) => verifyBankEpf(b.crewMemberId, "bank", false, r)} /> |
))}
)}
{canBankEpf && (
| Crew | UAN | Aadhaar | PF no. | |
{epf.map((e) => (
| {e.crewName} |
{e.uan ?? "—"} |
{e.aadhaarLast4 ?? "—"} |
{e.pfNumber ?? "—"} |
verifyBankEpf(e.crewMemberId, "epf", true)} onReject={(r) => verifyBankEpf(e.crewMemberId, "epf", false, r)} />
|
))}
)}
{canAppraisals && (
| Crew | Rank | Period | Comments | |
{appraisals.map((a) => (
| {a.crewName} |
{a.rank} |
{a.period} |
{a.comments ?? "—"} |
verifyAppraisal(a.id, true)} onReject={(r) => verifyAppraisal(a.id, false, r)} /> |
))}
)}
);
}