Phase 1 of the Crewing module per wiki Crewing-Implementation-Spec §12, all dark behind NEXT_PUBLIC_CREWING_ENABLED (off by default — production unchanged). - schema: add SITE_STAFF to Role; add Rank (self-referential org hierarchy, like Account) + RankDocRequirement, RankCategory & SeafarerDocType enums. - permissions: full §6 crewing grant matrix (PO_ROLE_PERMISSIONS + CREWING_ROLE_PERMISSIONS merged); SITE_STAFF row; MPO has no attendance/leave, approvals are Manager-only, manage_ranks is Manager+Admin. - feature flag: CREWING_ENABLED (opt-in "true"). - nav: flag-gated Crewing section scaffold + "Ranks & documents" under Admin. - reference data: rank-data.ts + rank-doc-data.ts seeded via shared seed-ranks.ts in both dev and prod seeds (19 ranks, 118 doc requirements). - screen: /admin/ranks — rank hierarchy card + per-rank required-documents card. - role-label/prefix maps updated for the new role. Tests: unit (permission matrix + flag), integration (ranks admin CRUD, parent linking, cycle/children guards, doc-requirement upsert/remove, permission gating). Docs: CLAUDE.md "Crewing (feature-flagged)" section + env var. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
56 lines
1.9 KiB
TypeScript
56 lines
1.9 KiB
TypeScript
// Shared, idempotent seeding of the crewing reference data (ranks + their
|
|
// required-document rules). Used by both the dev seed (seed.ts) and the
|
|
// production seed (seed-prod.ts). Two passes mirror the accounting-code seed:
|
|
// upsert every rank by code, then link parents, then upsert doc requirements.
|
|
|
|
import type { PrismaClient } from "@prisma/client";
|
|
import { RANKS } from "./rank-data";
|
|
import { RANK_DOC_REQUIREMENTS } from "./rank-doc-data";
|
|
|
|
export async function seedRanks(db: PrismaClient) {
|
|
const rankIdMap = new Map<string, string>();
|
|
|
|
// Pass 1: upsert all ranks (no parent yet) to obtain ids.
|
|
for (const r of RANKS) {
|
|
const rec = await db.rank.upsert({
|
|
where: { code: r.code },
|
|
update: {
|
|
name: r.name,
|
|
category: r.category,
|
|
isSeafarer: r.isSeafarer,
|
|
grantsLogin: r.grantsLogin,
|
|
},
|
|
create: {
|
|
code: r.code,
|
|
name: r.name,
|
|
category: r.category,
|
|
isSeafarer: r.isSeafarer,
|
|
grantsLogin: r.grantsLogin,
|
|
},
|
|
});
|
|
rankIdMap.set(r.code, rec.id);
|
|
}
|
|
|
|
// Pass 2: link parent relationships.
|
|
for (const r of RANKS) {
|
|
if (r.parentCode) {
|
|
const parentId = rankIdMap.get(r.parentCode);
|
|
if (parentId) {
|
|
await db.rank.update({ where: { code: r.code }, data: { parentId } });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Document requirements (keyed by the rank + docType compound unique).
|
|
for (const req of RANK_DOC_REQUIREMENTS) {
|
|
const rankId = rankIdMap.get(req.rankCode);
|
|
if (!rankId) continue;
|
|
await db.rankDocRequirement.upsert({
|
|
where: { rankId_docType: { rankId, docType: req.docType } },
|
|
update: { isMandatory: req.isMandatory, note: req.note ?? null },
|
|
create: { rankId, docType: req.docType, isMandatory: req.isMandatory, note: req.note ?? null },
|
|
});
|
|
}
|
|
|
|
console.log(` ✓ ${RANKS.length} ranks, ${RANK_DOC_REQUIREMENTS.length} document requirements`);
|
|
}
|