"use server"; import { auth } from "@/auth"; import { db } from "@/lib/db"; import { hasPermission } from "@/lib/permissions"; import { CREWING_ENABLED } from "@/lib/feature-flags"; import { generateEmployeeId } from "@/lib/employee-number"; import { maybeCreateSiteStaffLogin } from "@/lib/crew-login"; import { CrewStatus, CandidateType, CandidateSource } from "@prisma/client"; import { z } from "zod"; import { revalidatePath } from "next/cache"; type ActionResult = { ok: true; id?: string } | { error: string }; const PATH = "/admin/crew"; async function guard(): Promise<{ error: string } | { userId: string }> { if (!CREWING_ENABLED) return { error: "Crewing is not enabled" }; const session = await auth(); if (!session?.user) return { error: "Unauthorized" }; if (!hasPermission(session.user.role, "manage_crew")) return { error: "Unauthorized" }; return { userId: session.user.id }; } const crewSchema = z.object({ name: z.string().trim().min(1, "Name is required"), status: z.nativeEnum(CrewStatus).default("CANDIDATE"), type: z.nativeEnum(CandidateType).default("NEW"), source: z.nativeEnum(CandidateSource).default("CAREERS"), email: z.string().trim().email("Enter a valid email").optional().or(z.literal("")), phone: z.string().optional(), appliedRankId: z.string().optional(), currentRankId: z.string().optional(), experienceMonths: z.coerce.number().int().min(0).max(720).default(0), }); function parse(formData: FormData) { return crewSchema.safeParse({ name: formData.get("name"), status: (formData.get("status") as string) || undefined, type: (formData.get("type") as string) || undefined, source: (formData.get("source") as string) || undefined, email: (formData.get("email") as string) || undefined, phone: (formData.get("phone") as string) || undefined, appliedRankId: (formData.get("appliedRankId") as string) || undefined, currentRankId: (formData.get("currentRankId") as string) || undefined, experienceMonths: (formData.get("experienceMonths") as string) || undefined, }); } export async function createCrewMember(formData: FormData): Promise { const g = await guard(); if ("error" in g) return g; const parsed = parse(formData); if (!parsed.success) return { error: parsed.error.errors[0]?.message ?? "Validation failed" }; const d = parsed.data; const crew = await db.crewMember.create({ data: { name: d.name, status: d.status, type: d.type, source: d.source, email: d.email || null, phone: d.phone || null, appliedRankId: d.appliedRankId || null, currentRankId: d.currentRankId || null, experienceMonths: d.experienceMonths, actions: { create: { actionType: "CANDIDATE_ADDED", actorId: g.userId } }, }, }); revalidatePath(PATH); return { ok: true, id: crew.id }; } export async function updateCrewMember(formData: FormData): Promise { const g = await guard(); if ("error" in g) return g; const id = formData.get("id") as string; if (!id) return { error: "Crew ID is required" }; const parsed = parse(formData); if (!parsed.success) return { error: parsed.error.errors[0]?.message ?? "Validation failed" }; const d = parsed.data; if (!(await db.crewMember.findUnique({ where: { id }, select: { id: true } }))) return { error: "Crew member not found" }; await db.crewMember.update({ where: { id }, data: { name: d.name, status: d.status, type: d.type, source: d.source, email: d.email || null, phone: d.phone || null, appliedRankId: d.appliedRankId || null, currentRankId: d.currentRankId || null, experienceMonths: d.experienceMonths, actions: { create: { actionType: "CANDIDATE_UPDATED", actorId: g.userId } }, }, }); revalidatePath(PATH); return { ok: true }; } export async function deleteCrewMember(id: string): Promise { const g = await guard(); if ("error" in g) return g; const crew = await db.crewMember.findUnique({ where: { id }, select: { _count: { select: { assignments: true, applications: true } } }, }); if (!crew) return { error: "Crew member not found" }; if (crew._count.assignments > 0 || crew._count.applications > 0) { return { error: "Cannot delete: this crew member has assignments or applications. Remove those first." }; } await db.crewAction.deleteMany({ where: { crewMemberId: id } }); await db.crewMember.delete({ where: { id } }); revalidatePath(PATH); return { ok: true }; } // ── Direct placement (Manager) — assign crew to a vessel/site, no requisition ── const placeSchema = z .object({ crewMemberId: z.string().min(1, "Crew member is required"), rankId: z.string().min(1, "Rank is required"), vesselId: z.string().optional(), siteId: z.string().optional(), signOnDate: z.string().min(1, "Joining date is required"), }) .refine((d) => Boolean(d.vesselId) || Boolean(d.siteId), { message: "A vessel or site is required" }); export async function placeCrew(formData: FormData): Promise { const g = await guard(); if ("error" in g) return g; const parsed = placeSchema.safeParse({ crewMemberId: formData.get("crewMemberId"), rankId: formData.get("rankId"), vesselId: (formData.get("vesselId") as string) || undefined, siteId: (formData.get("siteId") as string) || undefined, signOnDate: formData.get("signOnDate"), }); if (!parsed.success) return { error: parsed.error.errors[0]?.message ?? "Validation failed" }; const d = parsed.data; const crew = await db.crewMember.findUnique({ where: { id: d.crewMemberId }, include: { assignments: { where: { status: { not: "SIGNED_OFF" } }, select: { id: true } } }, }); if (!crew) return { error: "Crew member not found" }; if (crew.assignments.length > 0) return { error: "This crew member already has an active assignment" }; await db.$transaction(async (tx) => { await tx.crewAssignment.create({ data: { status: "ACTIVE", signOnDate: new Date(d.signOnDate), crewMemberId: crew.id, rankId: d.rankId, vesselId: d.vesselId || null, siteId: d.siteId || null, }, }); // Promote a candidate/ex-hand to active crew (employee no.) on first placement. const data: { status: "EMPLOYEE"; currentRankId: string; employeeId?: string } = { status: "EMPLOYEE", currentRankId: d.rankId }; if (!crew.employeeId) data.employeeId = await generateEmployeeId(tx); await tx.crewMember.update({ where: { id: crew.id }, data }); await tx.crewAction.create({ data: { actionType: "CREW_ONBOARDED", actorId: g.userId, crewMemberId: crew.id, metadata: { direct: true } } }); // Management ranks (grantsLogin) become a SITE_STAFF login on placement. await maybeCreateSiteStaffLogin(tx, { name: crew.name, email: crew.email, employeeId: data.employeeId ?? crew.employeeId }, d.rankId, d.siteId || null); }); revalidatePath(PATH); revalidatePath("/crewing/crew"); return { ok: true }; }