pelagia-portal/App/prisma/seed-prod.ts
Hardik d0006a8fc7
All checks were successful
PR checks / checks (pull_request) Successful in 36s
PR checks / integration (pull_request) Successful in 28s
feat(crewing): foundations — SITE_STAFF role, ranks reference data + admin (flagged)
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>
2026-06-22 13:26:04 +05:30

251 lines
11 KiB
TypeScript

/**
* Production seed — Pelagia Marine Services
*
* Idempotent: safe to run multiple times (all operations are upsert).
* Run with: pnpm db:seed:prod
*
* Seeds:
* - Users (SSO, no password)
* - Sites
* - Vessels (assigned to sites)
* - Accounting codes (full hierarchy from Rev. 01/251227)
*/
import { PrismaClient, Role } from "@prisma/client";
import { ACCOUNTING_CODES } from "./accounting-codes-data";
import { seedRanks } from "./seed-ranks";
import bcrypt from "bcryptjs";
const hash = (p: string) => bcrypt.hash(p, 12);
const db = new PrismaClient();
// ─── Users ────────────────────────────────────────────────────────────────────
const USERS: { employeeId: string; name: string; email: string; role: Role }[] = [
{ employeeId: "ACC-001", name: "Akshata Teli", email: "akshata@pelagiamarine.com", role: Role.ACCOUNTS },
{ employeeId: "ACC-002", name: "Dipali K", email: "dipali.k@pelagiamarine.com", role: Role.ACCOUNTS },
{ employeeId: "ACC-003", name: "Nikita Accounts", email: "nikita.m@pelagiamarine.com", role: Role.ACCOUNTS },
{ employeeId: "ACC-004", name: "Shailesh B", email: "shailesh.b@pelagiamarine.com", role: Role.ACCOUNTS },
{ employeeId: "MGR-001", name: "Chhagan Sarang", email: "chhagan.sarang@pelagiamarine.com", role: Role.MANAGER },
{ employeeId: "MGR-002", name: "Kaushal Pal Singh", email: "kps@pelagiamarine.com", role: Role.MANAGER },
{ employeeId: "MGR-003", name: "Rakesh Kumar Pandey", email: "rkp@pelagiamarine.com", role: Role.MANAGER },
{ employeeId: "MGR-004", name: "Tajinder Kaur", email: "tajinder.kaur@pelagiamarine.com", role: Role.MANAGER },
{ employeeId: "MAN-001", name: "Mayur Deore", email: "mayur@pelagiamarine.com", role: Role.MANNING },
{ employeeId: "MAN-002", name: "Sunil Gupta", email: "sunil.gupta@pelagiamarine.com", role: Role.MANNING },
{ employeeId: "TCH-001", name: "Eeshan Singh", email: "eeshan.singh@pelagiamarine.com", role: Role.TECHNICAL },
{ employeeId: "TCH-002", name: "Manjuprasad B", email: "manjuprasad.b@pelagiamarine.com", role: Role.TECHNICAL },
{ employeeId: "TCH-003", name: "Shrikant T", email: "shrikant.t@pelagiamarine.com", role: Role.TECHNICAL },
{ employeeId: "TCH-004", name: "Supriya Sutar", email: "supriya.s@pelagiamarine.com", role: Role.TECHNICAL },
];
// ─── Sites ────────────────────────────────────────────────────────────────────
type SiteEntry = { code: string; name: string; address?: string; latitude?: number; longitude?: number };
const SITES: SiteEntry[] = [
{
code: "HOFC", name: "Head Office Mumbai",
address: "ZION, 409-410, Sector 10, Kharghar, Panvel, Maharashtra 410210",
latitude: 19.0449, longitude: 73.0758,
},
{
code: "HLDA", name: "Haldia Port",
address: "Haldia Dock Complex, Haldia, West Bengal 721604",
latitude: 22.0286, longitude: 88.0780,
},
{
code: "KCHI", name: "Kochi",
address: "LNG Terminal, Survey No. 347, Puthuvype, Kochi, Kerala 682508",
latitude: 10.0261, longitude: 76.2193,
},
{
code: "PTNA", name: "Patna",
address: "Gaighat, Ashok Rajpath, Gulzarbagh, Patna, Bihar 800007",
latitude: 25.6097, longitude: 85.1376,
},
];
// ─── Vessels (code, name, site code) ─────────────────────────────────────────
const VESSELS: { code: string; name: string }[] = [
{ name: "Head Office", code: "HOFC" },
{ name: "CSD PMS KOCHI", code: "PMSK" },
{ name: "CSD H&R 1", code: "HNR1" },
{ name: "CSD H&R 3", code: "HNR3" },
{ name: "CSD H&R 4", code: "HNR4" },
{ name: "CSD CHAMPION", code: "CHMP" },
{ name: "CSD HANUMAN", code: "HANU" },
{ name: "KAVARATTI", code: "KVRT" },
{ name: "LACCADIVES", code: "LACD" },
{ name: "THINNAKARA", code: "THNK" },
{ name: "THILLAAKAM", code: "THKM" },
{ name: "GD3000", code: "GD30" },
];
// ─── Companies ───────────────────────────────────────────────────────────────
type CompanyEntry = {
code: string; name: string; gstNumber: string;
address: string; telephone: string; mobile: string;
email: string; invoiceEmail: string; invoiceAddress: string;
};
const COMPANIES: CompanyEntry[] = [
{
code: "PMS",
name: "Pelagia Marine Services Pvt Ltd",
gstNumber: "27AAHCP5787B1Z6",
address: "409-410, ZION, Plot 273, Sector-10, Kharghar, Navi Mumbai- 410210",
telephone: "+91-22-6909 9028",
mobile: "+91 74000 60772",
email: "technical@pelagiamarine.com",
invoiceEmail: "accounts@pelagiamarine.com",
invoiceAddress: "Pelagia Marine Services Pvt Ltd, 409-410, ZION, Plot 273, Sector-10, Kharghar, Navi Mumbai- 410210 (MH)",
},
{
code: "HNR",
name: "H&R Offshore Pvt Ltd",
gstNumber: "27AAECH2810G1ZX",
address: "409-410, ZION, Plot 273, Sector-10, Kharghar, Navi Mumbai- 410210",
telephone: "+91-22-6909 9028",
mobile: "+91 74000 60772",
email: "technical@pelagiamarine.com",
invoiceEmail: "accounts@pelagiamarine.com",
invoiceAddress: "H&R Offshore Pvt Ltd, 409-410, ZION, Plot 273, Sector-10, Kharghar, Navi Mumbai- 410210 (MH)",
},
{
code: "DEI",
name: "Dredge Experts India Pvt Ltd",
gstNumber: "27AAGCD3114P1ZH",
address: "409-410, ZION, Plot 273, Sector-10, Kharghar, Navi Mumbai- 410210",
telephone: "+91-22-6909 9028",
mobile: "+91 74000 60772",
email: "technical@pelagiamarine.com",
invoiceEmail: "accounts@pelagiamarine.com",
invoiceAddress: "Dredge Experts India Pvt Ltd, 409-410, ZION, Plot 273, Sector-10, Kharghar, Navi Mumbai- 410210 (MH)",
},
];
// ─── Main ─────────────────────────────────────────────────────────────────────
async function main() {
console.log("🌱 Pelagia production seed starting…\n");
// ── Users ──────────────────────────────────────────────────────────────────
console.log("👤 Seeding users…");
for (const u of USERS) {
await db.user.upsert({
where: { email: u.email },
update: { name: u.name, role: u.role },
create: {
employeeId: u.employeeId,
email: u.email,
name: u.name,
role: u.role,
// No passwordHash — SSO-only login
},
});
console.log(`${u.name} <${u.email}> [${u.role}]`);
}
const admin = await db.user.upsert({
where: { email: "admin@pelagiamarine.com" },
update: {},
create: { employeeId: "ADM-001", email: "admin@pelagiamarine.com", name: "System Admin", passwordHash: await hash("admin1234"), role: Role.ADMIN },
});
// ── Sites ──────────────────────────────────────────────────────────────────
console.log("\n📍 Seeding sites…");
for (const s of SITES) {
const data = {
name: s.name,
address: s.address ?? null,
latitude: s.latitude ?? null,
longitude: s.longitude ?? null,
};
await db.site.upsert({
where: { code: s.code },
update: data,
create: { code: s.code, ...data },
});
const geo = s.latitude ? ` (${s.latitude.toFixed(4)}, ${s.longitude!.toFixed(4)})` : "";
console.log(`${s.name} (${s.code})${geo}`);
}
// ── Vessels ────────────────────────────────────────────────────────────────
console.log("\n🚢 Seeding vessels…");
for (const v of VESSELS) {
await db.vessel.upsert({
where: { code: v.code },
update: { name: v.name },
create: { code: v.code, name: v.name },
});
console.log(`${v.name} (${v.code})`);
}
// ── Companies ──────────────────────────────────────────────────────────────
console.log("\n🏢 Seeding companies…");
for (const c of COMPANIES) {
await db.company.upsert({
where: { code: c.code },
update: {
name: c.name, gstNumber: c.gstNumber, address: c.address,
telephone: c.telephone, mobile: c.mobile, email: c.email,
invoiceEmail: c.invoiceEmail, invoiceAddress: c.invoiceAddress,
},
create: {
code: c.code, name: c.name, gstNumber: c.gstNumber, address: c.address,
telephone: c.telephone, mobile: c.mobile, email: c.email,
invoiceEmail: c.invoiceEmail, invoiceAddress: c.invoiceAddress,
},
});
console.log(`${c.name} (${c.code})`);
}
// ── Accounting Codes ───────────────────────────────────────────────────────
console.log("\n📊 Seeding accounting codes…");
const codeIdMap = new Map<string, string>();
// Pass 1 — upsert every entry without parent links
for (const entry of ACCOUNTING_CODES) {
const rec = await db.account.upsert({
where: { code: entry.code },
update: { name: entry.name },
create: { code: entry.code, name: entry.name },
});
codeIdMap.set(entry.code, rec.id);
}
// Pass 2 — wire up parent relationships
for (const entry of ACCOUNTING_CODES) {
if (entry.parentCode) {
const parentId = codeIdMap.get(entry.parentCode);
if (parentId) {
await db.account.update({
where: { code: entry.code },
data: { parentId },
});
}
}
}
const leafCount = ACCOUNTING_CODES.filter((e) => {
return !ACCOUNTING_CODES.some((other) => other.parentCode === e.code);
}).length;
console.log(`${ACCOUNTING_CODES.length} codes (${leafCount} selectable leaf items)`);
// ── Crewing reference data (ranks + document rules) ──────────────────────────
console.log("\n⚓ Seeding crew ranks…");
await seedRanks(db);
console.log("\n✅ Production seed complete.");
}
main()
.catch((e) => {
console.error("❌ Seed failed:", e);
process.exit(1);
})
.finally(() => db.$disconnect());