Office/admin crewing-management surface behind a new manage_crew permission (Manager + SuperUser + Admin). Stacks on 4b. Behind NEXT_PUBLIC_CREWING_ENABLED. What's in - Permission: manage_crew added to the §6 matrix (MGR/SU/ADMIN). - Direct placement (placeCrew): a Manager assigns a crew member to a vessel/site WITHOUT a requisition — creates an ACTIVE CrewAssignment, promotes a candidate to EMPLOYEE with a CRW- number (generateEmployeeId), blocked if already actively assigned. - Admin crew CRUD: createCrewMember / updateCrewMember / deleteCrewMember (delete blocked when assignments/applications exist). - Crew strength config: upsert/delete VesselRankRequirement (the minStrength that drives R6 leave-clash detection). - Screens under Administration (flag-gated, MGR/SU/ADMIN): /admin/crew (list + add/ edit/delete + Place modal) and /admin/crew-strength (requirement table + form). Tests & docs - Unit: permissions-crewing.test.ts gains a manage_crew check. Integration: crewing-admin.test.ts (9) — CRUD, delete guard, direct placement (+promotion, +active-assignment guard), strength upsert/delete, manage_crew gating. type-check clean; full unit (241) + integration (192) green. - CLAUDE.md updated with the crewing-admin surface. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
56 lines
2 KiB
TypeScript
56 lines
2 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 { AdminCrewManager } from "./admin-crew-manager";
|
|
import type { Metadata } from "next";
|
|
|
|
export const metadata: Metadata = { title: "Crew management" };
|
|
|
|
export default async function AdminCrewPage() {
|
|
if (!CREWING_ENABLED) notFound();
|
|
|
|
const session = await auth();
|
|
if (!session?.user) redirect("/login");
|
|
if (!hasPermission(session.user.role, "manage_crew")) redirect("/dashboard");
|
|
|
|
const [crew, ranks, vessels, sites] = await Promise.all([
|
|
db.crewMember.findMany({
|
|
orderBy: { name: "asc" },
|
|
include: {
|
|
currentRank: { select: { name: true } },
|
|
appliedRank: { select: { name: true } },
|
|
assignments: { where: { status: { not: "SIGNED_OFF" } }, select: { id: true }, take: 1 },
|
|
_count: { select: { assignments: true, applications: true } },
|
|
},
|
|
}),
|
|
db.rank.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, code: true, name: true } }),
|
|
db.vessel.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, name: true } }),
|
|
db.site.findMany({ where: { isActive: true }, orderBy: { name: "asc" }, select: { id: true, name: true } }),
|
|
]);
|
|
|
|
return (
|
|
<AdminCrewManager
|
|
crew={crew.map((c) => ({
|
|
id: c.id,
|
|
name: c.name,
|
|
status: c.status,
|
|
type: c.type,
|
|
source: c.source,
|
|
email: c.email,
|
|
phone: c.phone,
|
|
employeeId: c.employeeId,
|
|
appliedRankId: c.appliedRankId,
|
|
currentRankId: c.currentRankId,
|
|
currentRank: c.currentRank?.name ?? null,
|
|
experienceMonths: c.experienceMonths,
|
|
hasActiveAssignment: c.assignments.length > 0,
|
|
removable: c._count.assignments === 0 && c._count.applications === 0,
|
|
}))}
|
|
ranks={ranks}
|
|
vessels={vessels}
|
|
sites={sites}
|
|
/>
|
|
);
|
|
}
|