pelagia-portal/App/app/(portal)/admin/crew/page.tsx
Hardik bb5f4126b0
All checks were successful
PR checks / checks (pull_request) Successful in 39s
PR checks / integration (pull_request) Successful in 28s
feat(crewing): admin crew management — direct placement, CRUD, strength config
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>
2026-06-22 21:23:31 +05:30

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}
/>
);
}