import { auth } from "@/auth"; import { db } from "@/lib/db"; import { hasPermission } from "@/lib/permissions"; import { redirect } from "next/navigation"; import Link from "next/link"; import { formatCurrency, formatDate } from "@/lib/utils"; import { ApprovalsSearch } from "./approvals-search"; import { CREWING_ENABLED } from "@/lib/feature-flags"; import { CrewingApprovals, type CrewApprovalItem, type CrewApprovalKind } from "./crewing-approvals"; import { Suspense } from "react"; import type { Metadata } from "next"; export const metadata: Metadata = { title: "Approvals" }; interface Props { searchParams: Promise<{ q?: string; vesselId?: string; dateFrom?: string; }>; } export default async function ApprovalsPage({ searchParams }: Props) { const session = await auth(); if (!session?.user) redirect("/login"); if (!hasPermission(session.user.role, "approve_po")) redirect("/dashboard"); const { q, vesselId, dateFrom } = await searchParams; const where: NonNullable[0]>["where"] = { status: "MGR_REVIEW", }; if (q?.trim()) { where.OR = [ { poNumber: { contains: q.trim(), mode: "insensitive" } }, { submitter: { name: { contains: q.trim(), mode: "insensitive" } } }, { title: { contains: q.trim(), mode: "insensitive" } }, ]; } if (vesselId) where.vesselId = vesselId; if (dateFrom) where.submittedAt = { gte: new Date(dateFrom) }; const [pending, vessels] = await Promise.all([ db.purchaseOrder.findMany({ where, include: { submitter: true, vessel: true, account: true }, orderBy: { submittedAt: "asc" }, }), db.vessel.findMany({ orderBy: { name: "asc" }, select: { id: true, name: true } }), ]); // Crewing approvals (spec §8.13 R8) — the same unified Manager queue. Pending // SALARY / SELECTION / WAIVER gates surface here alongside POs. const role = session.user.role; const showCrewing = CREWING_ENABLED && (hasPermission(role, "approve_salary_structure") || hasPermission(role, "select_candidate") || hasPermission(role, "approve_interview_waiver") || hasPermission(role, "decide_leave") || hasPermission(role, "approve_appraisal")); const crewGates = showCrewing ? await db.applicationGate.findMany({ where: { result: "PENDING", gate: { in: ["SALARY", "SELECTION", "WAIVER"] } }, orderBy: { createdAt: "asc" }, include: { application: { include: { crewMember: { select: { name: true } }, requisition: { select: { code: true, rank: { select: { name: true } } } }, salaryStructures: { where: { approvedById: null }, orderBy: { createdAt: "desc" }, take: 1 }, }, }, }, }) : []; const crewItems: CrewApprovalItem[] = crewGates.map((g) => { const sal = g.application.salaryStructures[0]; const detail = g.gate === "SALARY" && sal ? `${sal.currency} ${Number(sal.basic).toLocaleString("en-IN")} / ${sal.rateBasis.toLowerCase()}` : g.gate === "WAIVER" ? "Returning crew — interview waiver" : "Interview cleared"; return { id: g.applicationId, kind: g.gate as CrewApprovalKind, candidateName: g.application.crewMember.name, rank: g.application.requisition.rank.name, requisitionCode: g.application.requisition.code, detail, link: `/crewing/applications/${g.applicationId}`, }; }); // Pending leave requests (Manager decides) — the §8.13 "Leave" queue kind. const leaveItems: CrewApprovalItem[] = (showCrewing && hasPermission(role, "decide_leave")) ? (await db.leaveRequest.findMany({ where: { status: "APPLIED" }, orderBy: { createdAt: "asc" }, include: { assignment: { include: { crewMember: { select: { name: true } }, rank: { select: { name: true } } } } }, })).map((l) => ({ id: l.id, kind: "LEAVE" as CrewApprovalKind, candidateName: l.assignment.crewMember.name, rank: l.assignment.rank.name, requisitionCode: `${l.fromDate.toLocaleDateString()}–${l.toDate.toLocaleDateString()}`, detail: l.type.toLowerCase(), link: "/crewing/leave", })) : []; // MPO-verified appraisals awaiting Manager approval (§8.13/§8.14). const appraisalItems: CrewApprovalItem[] = (showCrewing && hasPermission(role, "approve_appraisal")) ? (await db.appraisal.findMany({ where: { status: "MPO_VERIFIED" }, orderBy: { createdAt: "asc" }, include: { assignment: { include: { crewMember: { select: { name: true } }, rank: { select: { name: true } } } } }, })).map((a) => ({ id: a.id, kind: "APPRAISAL" as CrewApprovalKind, candidateName: a.assignment.crewMember.name, rank: a.assignment.rank.name, requisitionCode: a.period, detail: "MPO-verified appraisal", link: "/approvals", })) : []; const allCrewItems = [...crewItems, ...leaveItems, ...appraisalItems]; return (

Approval Queue

{pending.length} order{pending.length !== 1 ? "s" : ""} awaiting your decision

{pending.length === 0 ? (

No purchase orders awaiting approval.

) : ( <> {/* ── Desktop table ─────────────────────────────────────────── */}
{pending.map((po) => ( ))}
PO Number Title Submitter Cost Centre Amount Submitted
{po.poNumber} {po.title} {po.submitter.name} {po.vessel.name} {formatCurrency(Number(po.totalAmount), po.currency)} {po.submittedAt ? formatDate(po.submittedAt) : "—"} Review →
{/* ── Mobile cards ──────────────────────────────────────────── */}
{pending.map((po) => (
{po.poNumber} {po.submittedAt ? formatDate(po.submittedAt) : "—"}

{po.title}

{po.submitter.name} · {po.vessel.name} {formatCurrency(Number(po.totalAmount), po.currency)}
Review →
))}
)} {showCrewing && allCrewItems.length > 0 && }
); }