Clears the self-contained deferrals tracked across phases. Stacks on 5b appraisal. Behind NEXT_PUBLIC_CREWING_ENABLED. - SITE_STAFF login on onboard/placement (Epic D follow-up): lib/crew-login.ts maybeCreateSiteStaffLogin creates a passwordless SITE_STAFF User (sharing the CRW- employee no., siteId = the assignment's site) when a grantsLogin rank is onboarded (onboardCandidate) or placed (placeCrew) and the crew member has an email. No-op otherwise. - Own-site scoping (Epic E follow-up, §8.7): User.siteId added (migration crewing_followups); the Crew directory filters a SITE_STAFF user with a home site to crew whose active assignment is at that site (graceful when unset). The link is set at login creation. - PPE / next-of-kin verify gates (Epic F/I follow-up): PpeIssue/NextOfKin gained verificationStatus + verifiedById; verifyPpe / verifyNextOfKin (verify_site_records, MPO) + queue sections in /crewing/verification. Tests & docs - Integration: crewing-followups.test.ts (6) — login created/skipped by rank+email (+ siteId set), PPE/NoK verify + reject-reason + already-decided guard + gating. type-check clean; full unit (245) + integration (211) green (RESEND_API_KEY unset). - CLAUDE.md updated. Part of Epic D (#78), Epic E (#79), Epic F (#80), Epic I (#83). Still deferred (not self-contained): public careers API (A2); Pay-status pay rows (Phase 6). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
82 lines
3.6 KiB
TypeScript
82 lines
3.6 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, ppe, nok] = 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 } } } } },
|
|
})
|
|
: [],
|
|
canDocs
|
|
? db.ppeIssue.findMany({ where: { verificationStatus: "PENDING" }, orderBy: { issuedDate: "asc" }, include: { crewMember: { select: { name: true } } } })
|
|
: [],
|
|
canDocs
|
|
? db.nextOfKin.findMany({ where: { verificationStatus: "PENDING" }, orderBy: { createdAt: "asc" }, include: { crewMember: { 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 }))}
|
|
ppe={ppe.map((p) => ({ id: p.id, crewName: p.crewMember.name, item: p.item, size: p.size }))}
|
|
nok={nok.map((n) => ({ id: n.id, crewName: n.crewMember.name, name: n.name, relationship: n.relationship }))}
|
|
canDocs={canDocs}
|
|
canBankEpf={canBankEpf}
|
|
canAppraisals={canAppraisals}
|
|
/>
|
|
);
|
|
}
|