"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 { canPerformAction, canReject } from "@/lib/appraisal-state-machine"; import { getManagerRecipients, getMpoRecipients } from "@/lib/requisition-service"; import { notifyCrew } from "@/lib/notifier"; import type { Role } from "@prisma/client"; import { z } from "zod"; import { revalidatePath } from "next/cache"; type ActionResult = { ok: true; id?: string } | { error: string }; 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 }; } function loadAppraisal(id: string) { return db.appraisal.findUnique({ where: { id }, include: { assignment: { include: { crewMember: { select: { id: true, name: true } }, rank: { select: { name: true } } } } }, }); } function revalidate(crewMemberId: string) { revalidatePath(`/crewing/crew/${crewMemberId}`); revalidatePath("/crewing/verification"); revalidatePath("/approvals"); } // ── Raise an appraisal (PM / site staff) ─────────────────────────────────────── const raiseSchema = z.object({ assignmentId: z.string().min(1, "Crew assignment is required"), period: z.string().trim().min(1, "Period is required"), comments: z.string().optional(), competence: z.coerce.number().int().min(1).max(5).optional(), conduct: z.coerce.number().int().min(1).max(5).optional(), safety: z.coerce.number().int().min(1).max(5).optional(), }); export async function raiseAppraisal(formData: FormData): Promise { const g = await guard("raise_appraisal"); if ("error" in g) return g; const parsed = raiseSchema.safeParse(Object.fromEntries(formData)); if (!parsed.success) return { error: parsed.error.errors[0]?.message ?? "Validation failed" }; const d = parsed.data; const assignment = await db.crewAssignment.findUnique({ where: { id: d.assignmentId }, include: { crewMember: { select: { id: true, name: true } }, rank: { select: { name: true } } }, }); if (!assignment) return { error: "Crew assignment not found" }; const appraisal = await db.appraisal.create({ data: { assignmentId: d.assignmentId, period: d.period, comments: d.comments ?? null, ratings: { competence: d.competence ?? null, conduct: d.conduct ?? null, safety: d.safety ?? null }, status: "SUBMITTED", addedById: g.userId, }, }); await db.crewAction.create({ data: { actionType: "APPRAISAL_SUBMITTED", actorId: g.userId, crewMemberId: assignment.crewMember.id } }); const mpos = await getMpoRecipients(); await notifyCrew({ event: "APPRAISAL_FOR_VERIFICATION", recipients: mpos, subject: `Appraisal to verify — ${assignment.crewMember.name}`, body: `An appraisal for ${assignment.crewMember.name} (${assignment.rank.name}, ${d.period}) awaits MPO verification.`, link: "/crewing/verification", }); revalidate(assignment.crewMember.id); return { ok: true, id: appraisal.id }; } // ── Verify (MPO) ─────────────────────────────────────────────────────────────── export async function verifyAppraisal(id: string, approve: boolean, remarks?: string): Promise { const g = await guard("verify_appraisal"); if ("error" in g) return g; const a = await loadAppraisal(id); if (!a) return { error: "Appraisal not found" }; if (!approve) { if (!canReject(a.status)) return { error: `Cannot reject from ${a.status}` }; if (!remarks?.trim()) return { error: "A reason is required to reject" }; await db.appraisal.update({ where: { id }, data: { status: "REJECTED", rejectedReason: remarks.trim() } }); await db.crewAction.create({ data: { actionType: "APPRAISAL_REJECTED", actorId: g.userId, crewMemberId: a.assignment.crewMember.id, note: remarks.trim() } }); revalidate(a.assignment.crewMember.id); return { ok: true }; } if (!canPerformAction(a.status, "verify", g.role)) return { error: `Cannot verify from ${a.status}` }; await db.appraisal.update({ where: { id }, data: { status: "MPO_VERIFIED", verifiedById: g.userId } }); await db.crewAction.create({ data: { actionType: "APPRAISAL_VERIFIED", actorId: g.userId, crewMemberId: a.assignment.crewMember.id } }); const managers = await getManagerRecipients(); await notifyCrew({ event: "APPRAISAL_FOR_APPROVAL", recipients: managers, subject: `Appraisal for approval — ${a.assignment.crewMember.name}`, body: `${a.assignment.crewMember.name}'s appraisal (${a.assignment.rank.name}, ${a.period}) has been MPO-verified and awaits your approval.`, link: "/approvals", }); revalidate(a.assignment.crewMember.id); return { ok: true }; } // ── Approve (Manager) ────────────────────────────────────────────────────────── export async function approveAppraisal(id: string, approve: boolean, remarks?: string): Promise { const g = await guard("approve_appraisal"); if ("error" in g) return g; const a = await loadAppraisal(id); if (!a) return { error: "Appraisal not found" }; if (!approve) { if (!canReject(a.status)) return { error: `Cannot return from ${a.status}` }; if (!remarks?.trim()) return { error: "A reason is required to return" }; await db.appraisal.update({ where: { id }, data: { status: "REJECTED", rejectedReason: remarks.trim() } }); await db.crewAction.create({ data: { actionType: "APPRAISAL_REJECTED", actorId: g.userId, crewMemberId: a.assignment.crewMember.id, note: remarks.trim() } }); revalidate(a.assignment.crewMember.id); return { ok: true }; } if (!canPerformAction(a.status, "approve", g.role)) return { error: `Cannot approve from ${a.status}` }; await db.appraisal.update({ where: { id }, data: { status: "MANAGER_APPROVED", approvedById: g.userId } }); await db.crewAction.create({ data: { actionType: "APPRAISAL_APPROVED", actorId: g.userId, crewMemberId: a.assignment.crewMember.id } }); revalidate(a.assignment.crewMember.id); return { ok: true }; }