pelagia-portal/App/app/(portal)/crewing/verification/page.tsx
Hardik c14a22588e
All checks were successful
PR checks / checks (pull_request) Successful in 40s
PR checks / integration (pull_request) Successful in 30s
feat(crewing): Phase 5b — appraisal (flagged)
Final slice of Phase 5. The appraisal lifecycle raise → verify → approve across
three role-gated surfaces, per Crewing-Implementation-Spec §5.4/§8.14. Stacks on
5a verification. Behind NEXT_PUBLIC_CREWING_ENABLED. Completes Phase 5.

What's in
- Schema: Appraisal (on CrewAssignment) + AppraisalStatus
  (DRAFT/SUBMITTED/MPO_VERIFIED/MANAGER_APPROVED/REJECTED); CrewActionType +=
  APPRAISAL_SUBMITTED/VERIFIED/APPROVED/REJECTED. Migration crewing_appraisal.
- State machine lib/appraisal-state-machine.ts: verify (SUBMITTED→MPO_VERIFIED,
  MPO/Manager), approve (MPO_VERIFIED→MANAGER_APPROVED, Manager); orthogonal reject.
- Actions (crewing/appraisals/actions.ts): raiseAppraisal (raise_appraisal — PM/
  site staff), verifyAppraisal (verify_appraisal — MPO), approveAppraisal
  (approve_appraisal — Manager); reject paths require remarks; notifications
  APPRAISAL_FOR_VERIFICATION / APPRAISAL_FOR_APPROVAL.
- Three surfaces (§8.14): PM raises + tracks status on the crew-profile Appraisals
  tab; MPO verifies in the Verification queue (Appraisals section); Manager approves
  in the central /approvals queue (Appraisal kind).

Tests & docs
- Unit: appraisal-state-machine.test.ts (4). Integration: appraisal.test.ts (4) —
  raise→verify→approve happy path, MPO reject, permission gating (MPO can't raise,
  site staff can't verify, MPO can't approve). type-check clean; full unit (245) +
  integration (205) green (verified with RESEND_API_KEY unset).
- CLAUDE.md updated — completes Phase 5 (I + H).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:09:32 +05:30

74 lines
3 KiB
TypeScript

import { auth } from "@/auth";
import { db } from "@/lib/db";
import { hasPermission } from "@/lib/permissions";
import { CREWING_ENABLED } from "@/lib/feature-flags";
import { redirect, notFound } from "next/navigation";
import { VerificationManager } from "./verification-manager";
import type { Metadata } from "next";
export const metadata: Metadata = { title: "Verification" };
export default async function VerificationPage() {
if (!CREWING_ENABLED) notFound();
const session = await auth();
if (!session?.user) redirect("/login");
const role = session.user.role;
const canDocs = hasPermission(role, "verify_site_records");
const canBankEpf = hasPermission(role, "verify_bank_epf");
const canAppraisals = hasPermission(role, "verify_appraisal");
if (!canDocs && !canBankEpf && !canAppraisals) redirect("/dashboard");
const [docs, bank, epf, appraisals] = await Promise.all([
canDocs
? db.seafarerDocument.findMany({
where: { verificationStatus: "PENDING" },
orderBy: { createdAt: "asc" },
include: {
crewMember: {
select: {
name: true,
assignments: { where: { status: { not: "SIGNED_OFF" } }, take: 1, include: { vessel: { select: { name: true } }, site: { select: { name: true } } } },
},
},
},
})
: [],
canBankEpf
? db.bankDetail.findMany({ where: { verificationStatus: "PENDING" }, orderBy: { createdAt: "asc" }, include: { crewMember: { select: { name: true } } } })
: [],
canBankEpf
? db.epfDetail.findMany({ where: { verificationStatus: "PENDING" }, orderBy: { createdAt: "asc" }, include: { crewMember: { select: { name: true } } } })
: [],
canAppraisals
? db.appraisal.findMany({
where: { status: "SUBMITTED" },
orderBy: { createdAt: "asc" },
include: { assignment: { include: { crewMember: { select: { name: true } }, rank: { select: { name: true } } } } },
})
: [],
]);
return (
<VerificationManager
docs={docs.map((d) => {
const a = d.crewMember.assignments[0];
return {
id: d.id,
crewName: d.crewMember.name,
location: a?.vessel?.name ?? a?.site?.name ?? "—",
docType: d.docType,
number: d.number,
expiryDate: d.expiryDate?.toISOString() ?? null,
submitted: d.createdAt.toISOString(),
};
})}
bank={bank.map((b) => ({ crewMemberId: b.crewMemberId, crewName: b.crewMember.name, accountName: b.accountName, accountNumber: b.accountNumber, ifsc: b.ifsc, bankName: b.bankName }))}
epf={epf.map((e) => ({ crewMemberId: e.crewMemberId, crewName: e.crewMember.name, uan: e.uan, aadhaarLast4: e.aadhaarLast4, pfNumber: e.pfNumber }))}
appraisals={appraisals.map((a) => ({ id: a.id, crewName: a.assignment.crewMember.name, rank: a.assignment.rank.name, period: a.period, comments: a.comments }))}
canDocs={canDocs}
canBankEpf={canBankEpf}
canAppraisals={canAppraisals}
/>
);
}