"use server"; import { auth } from "@/auth"; import { db } from "@/lib/db"; import { hasPermission, type Permission } from "@/lib/permissions"; import { CREWING_ENABLED } from "@/lib/feature-flags"; import type { Role } from "@prisma/client"; import { revalidatePath } from "next/cache"; type ActionResult = { ok: true } | { error: string }; const PATH = "/crewing/verification"; async function guard(permission: Permission): Promise<{ error: string } | { userId: string; role: Role }> { if (!CREWING_ENABLED) return { error: "Crewing is not enabled" }; const session = await auth(); if (!session?.user) return { error: "Unauthorized" }; if (!hasPermission(session.user.role, permission)) return { error: "Unauthorized" }; return { userId: session.user.id, role: session.user.role }; } // ── Document verification (MPO / Manager) ────────────────────────────────────── export async function verifyDocument(id: string, approve: boolean, remarks?: string): Promise { const g = await guard("verify_site_records"); if ("error" in g) return g; if (!approve && !remarks?.trim()) return { error: "A reason is required to reject" }; const doc = await db.seafarerDocument.findUnique({ where: { id }, select: { crewMemberId: true, verificationStatus: true } }); if (!doc) return { error: "Document not found" }; if (doc.verificationStatus !== "PENDING") return { error: `This document is already ${doc.verificationStatus.toLowerCase()}` }; await db.seafarerDocument.update({ where: { id }, data: { verificationStatus: approve ? "VERIFIED" : "REJECTED", verifiedById: g.userId }, }); await db.crewAction.create({ data: { actionType: approve ? "RECORD_VERIFIED" : "RECORD_REJECTED", actorId: g.userId, crewMemberId: doc.crewMemberId, note: remarks?.trim() || null, metadata: { record: "document" }, }, }); revalidatePath(PATH); revalidatePath(`/crewing/crew/${doc.crewMemberId}`); return { ok: true }; } // ── Bank / EPF verification (Accounts) ───────────────────────────────────────── export async function verifyBankEpf(crewMemberId: string, kind: "bank" | "epf", approve: boolean, remarks?: string): Promise { const g = await guard("verify_bank_epf"); if ("error" in g) return g; if (!approve && !remarks?.trim()) return { error: "A reason is required to reject" }; const status = approve ? "VERIFIED" : "REJECTED"; if (kind === "bank") { const rec = await db.bankDetail.findUnique({ where: { crewMemberId }, select: { id: true, verificationStatus: true } }); if (!rec) return { error: "Bank details not found" }; if (rec.verificationStatus !== "PENDING") return { error: `Bank details already ${rec.verificationStatus.toLowerCase()}` }; await db.bankDetail.update({ where: { crewMemberId }, data: { verificationStatus: status, verifiedById: g.userId } }); } else { const rec = await db.epfDetail.findUnique({ where: { crewMemberId }, select: { id: true, verificationStatus: true } }); if (!rec) return { error: "EPF details not found" }; if (rec.verificationStatus !== "PENDING") return { error: `EPF details already ${rec.verificationStatus.toLowerCase()}` }; await db.epfDetail.update({ where: { crewMemberId }, data: { verificationStatus: status, verifiedById: g.userId } }); } await db.crewAction.create({ data: { actionType: approve ? "RECORD_VERIFIED" : "RECORD_REJECTED", actorId: g.userId, crewMemberId, note: remarks?.trim() || null, metadata: { record: kind }, }, }); revalidatePath(PATH); revalidatePath(`/crewing/crew/${crewMemberId}`); return { ok: true }; } // ── PPE / next-of-kin verification (MPO) ─────────────────────────────────────── async function verifyRecord( load: () => Promise<{ crewMemberId: string; verificationStatus: "PENDING" | "VERIFIED" | "REJECTED" } | null>, set: (status: "VERIFIED" | "REJECTED", userId: string) => Promise, recordLabel: string, approve: boolean, remarks: string | undefined, userId: string ): Promise { if (!approve && !remarks?.trim()) return { error: "A reason is required to reject" }; const rec = await load(); if (!rec) return { error: "Record not found" }; if (rec.verificationStatus !== "PENDING") return { error: `This record is already ${rec.verificationStatus.toLowerCase()}` }; await set(approve ? "VERIFIED" : "REJECTED", userId); await db.crewAction.create({ data: { actionType: approve ? "RECORD_VERIFIED" : "RECORD_REJECTED", actorId: userId, crewMemberId: rec.crewMemberId, note: remarks?.trim() || null, metadata: { record: recordLabel } }, }); revalidatePath(PATH); revalidatePath(`/crewing/crew/${rec.crewMemberId}`); return { ok: true }; } export async function verifyPpe(id: string, approve: boolean, remarks?: string): Promise { const g = await guard("verify_site_records"); if ("error" in g) return g; return verifyRecord( () => db.ppeIssue.findUnique({ where: { id }, select: { crewMemberId: true, verificationStatus: true } }), (status, userId) => db.ppeIssue.update({ where: { id }, data: { verificationStatus: status, verifiedById: userId } }), "ppe", approve, remarks, g.userId ); } export async function verifyNextOfKin(id: string, approve: boolean, remarks?: string): Promise { const g = await guard("verify_site_records"); if ("error" in g) return g; return verifyRecord( () => db.nextOfKin.findUnique({ where: { id }, select: { crewMemberId: true, verificationStatus: true } }), (status, userId) => db.nextOfKin.update({ where: { id }, data: { verificationStatus: status, verifiedById: userId } }), "next_of_kin", approve, remarks, g.userId ); } // ── EPFO assisted lookup (Accounts) ──────────────────────────────────────────── // Records the result of an EpfoService UAN check on the crew member's EpfDetail // (A3 "record the result"). The actual lookup runs in the browser via /api/epfo; // this just persists the returned member name + a timestamp for the audit trail. export async function recordEpfoCheck(crewMemberId: string, memberName: string | null): Promise { const g = await guard("verify_bank_epf"); if ("error" in g) return g; const rec = await db.epfDetail.findUnique({ where: { crewMemberId }, select: { id: true } }); if (!rec) return { error: "EPF details not found" }; await db.epfDetail.update({ where: { crewMemberId }, data: { epfoMemberName: memberName, epfoCheckedAt: new Date() }, }); await db.crewAction.create({ data: { actionType: "RECORD_UPDATED", actorId: g.userId, crewMemberId, note: memberName ? `EPFO check matched: ${memberName}` : "EPFO check: no match", metadata: { record: "epfo_check" }, }, }); revalidatePath(PATH); revalidatePath(`/crewing/crew/${crewMemberId}`); return { ok: true }; }