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>
55 lines
1.8 KiB
TypeScript
55 lines
1.8 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 { CrewDirectory } from "./crew-directory";
|
|
import type { Metadata } from "next";
|
|
|
|
export const metadata: Metadata = { title: "Crew" };
|
|
|
|
export default async function CrewPage() {
|
|
if (!CREWING_ENABLED) notFound();
|
|
|
|
const session = await auth();
|
|
if (!session?.user) redirect("/login");
|
|
if (!hasPermission(session.user.role, "view_crew_records")) redirect("/dashboard");
|
|
|
|
// Own-site scoping (§8.7): a site-staff user with a home site sees only crew whose
|
|
// active assignment is at that site. Without a home site they remain unscoped.
|
|
let siteScopeId: string | null = null;
|
|
if (session.user.role === "SITE_STAFF") {
|
|
siteScopeId = (await db.user.findUnique({ where: { id: session.user.id }, select: { siteId: true } }))?.siteId ?? null;
|
|
}
|
|
|
|
const crew = await db.crewMember.findMany({
|
|
where: {
|
|
status: "EMPLOYEE",
|
|
...(siteScopeId ? { assignments: { some: { status: { not: "SIGNED_OFF" }, siteId: siteScopeId } } } : {}),
|
|
},
|
|
orderBy: { name: "asc" },
|
|
include: {
|
|
currentRank: { select: { name: true } },
|
|
assignments: {
|
|
where: { status: { not: "SIGNED_OFF" } },
|
|
orderBy: { signOnDate: "desc" },
|
|
take: 1,
|
|
include: { vessel: { select: { name: true } }, site: { select: { name: true } } },
|
|
},
|
|
},
|
|
});
|
|
|
|
const rows = crew.map((c) => {
|
|
const a = c.assignments[0];
|
|
return {
|
|
id: c.id,
|
|
name: c.name,
|
|
employeeId: c.employeeId ?? "—",
|
|
rank: c.currentRank?.name ?? "—",
|
|
location: a?.vessel?.name ?? a?.site?.name ?? "—",
|
|
status: a?.status ?? null,
|
|
};
|
|
});
|
|
|
|
return <CrewDirectory crew={rows} />;
|
|
}
|