First slice of Phase 4 (stacked on 3c onboarding). The Crew directory and tabbed crew profile with documents, bank/EPF (role-masked), next of kin, PPE and experience, per Crewing-Implementation-Spec §8.7–8.8. Behind NEXT_PUBLIC_CREWING_ENABLED; production unchanged. What's in - Schema (crewing_crew_records migration): SeafarerDocument, NextOfKin (isEmergency), ExperienceRecord, PpeIssue (PpeItem enum) — all on CrewMember; CrewActionType += DOCUMENT_UPLOADED/RECORD_UPDATED/PPE_ISSUED/PPE_RETURNED/ EXPERIENCE_ADDED. - PII masking (lib/crew-pii.ts, §6/§8.8): bank account + Aadhaar full only for Accounts/SuperUser, masked otherwise; salary hidden from site staff. Applied server-side before crossing to the client. - Actions (crewing/crew/actions.ts): uploadDocument/deleteDocument, saveBankEpf, addNextOfKin/deleteNextOfKin, issuePpe/returnPpe, addExperience — guarded by upload_crew_records / issue_ppe, each writes a CrewAction. - Screens: /crewing/crew (directory, search + vessel filter, ex-hands excluded) and /crewing/crew/[id] (tabbed profile: Documents · Bank & EPF · Next of kin · PPE · Experience · Pay status). Crew added to the flag-gated nav (MGR/MPO/Site/ Accounts). Tests & docs - Unit: crew-pii.test.ts (6). Integration: crew-records.test.ts (7) — documents, bank/EPF upsert, NoK, PPE issue/return, experience + permission gating. type-check clean; full unit (240) + integration (175) green. - CLAUDE.md updated with the Phase 4a surface. Deferred: site-staff own-site scoping (needs a User↔Site link); the records verify queue (§8.11, Phase 5); Pay-status shows the salary structure only until payroll (Phase 6). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
47 lines
1.5 KiB
TypeScript
47 lines
1.5 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");
|
|
|
|
// NOTE: site-staff "own site only" scoping (§8.7) needs a User↔Site link that
|
|
// isn't modelled yet — deferred to a follow-up; for now all active crew show.
|
|
const crew = await db.crewMember.findMany({
|
|
where: { status: "EMPLOYEE" },
|
|
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} />;
|
|
}
|