pelagia-portal/App/prisma/schema.prisma
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

444 lines
12 KiB
Text

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum Role {
TECHNICAL
MANNING
ACCOUNTS
MANAGER
SUPERUSER
AUDITOR
ADMIN
SITE_STAFF
}
enum POStatus {
DRAFT
SUBMITTED
MGR_REVIEW
VENDOR_ID_PENDING
EDITS_REQUESTED
REJECTED
MGR_APPROVED
SENT_FOR_PAYMENT
PARTIALLY_PAID
PAID_DELIVERED
PARTIALLY_CLOSED
CLOSED
CANCELLED
}
enum ActionType {
CREATED
SUBMITTED
APPROVED
APPROVED_WITH_NOTE
REJECTED
EDITS_REQUESTED
VENDOR_ID_REQUESTED
VENDOR_ID_PROVIDED
PAYMENT_SENT
PARTIAL_PAYMENT_CONFIRMED
RECEIPT_CONFIRMED
PARTIAL_RECEIPT_CONFIRMED
CLOSED
REASSIGNED
PRODUCT_PRICE_UPDATED
MANAGER_LINE_EDIT
CANCELLED
SUPERSEDED
}
enum RequestStatus {
PENDING
APPROVED
DENIED
}
// ─── Crewing (feature-flagged: NEXT_PUBLIC_CREWING_ENABLED) ──────────────────
// Phase 1 (Foundations) lands only the reference-data layer. The lifecycle
// models/enums (Requisition, Application, Assignment, …) arrive in later phases.
// See wiki Crewing-Implementation-Spec §12.
// Org-chart grouping for a Rank. Drives reporting/segmentation, not login.
enum RankCategory {
OPERATIONAL
SUPPORT
}
// The seafarer/crew document set a rank may be required to hold. Drives
// candidate vetting and crew uploads via RankDocRequirement.
enum SeafarerDocType {
STCW
AADHAAR
PAN
PASSPORT
CDC
COC
PHOTOGRAPH
DRIVING_LICENSE
MEDICAL_FITNESS
CONTRACT_LETTER
}
model User {
id String @id @default(cuid())
employeeId String @unique
email String @unique
name String
passwordHash String?
role Role
isActive Boolean @default(true)
signatureKey String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
submittedPOs PurchaseOrder[] @relation("Submitter")
actions POAction[]
notifications Notification[]
consumption ItemConsumption[]
superUserRequests SuperUserRequest[] @relation("Requester")
resolvedRequests SuperUserRequest[] @relation("RequestResolver")
}
model SuperUserRequest {
id String @id @default(cuid())
userId String
user User @relation("Requester", fields: [userId], references: [id])
reason String?
status RequestStatus @default(PENDING)
createdAt DateTime @default(now())
resolvedAt DateTime?
resolvedById String?
resolvedBy User? @relation("RequestResolver", fields: [resolvedById], references: [id])
}
model Site {
id String @id @default(cuid())
name String
code String @unique
address String?
latitude Float?
longitude Float?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
purchaseOrders PurchaseOrder[]
inventory ItemInventory[]
consumption ItemConsumption[]
}
model Vessel {
id String @id @default(cuid())
name String
code String @unique
isActive Boolean @default(true)
purchaseOrders PurchaseOrder[]
}
model Company {
id String @id @default(cuid())
name String
code String? @unique
gstNumber String?
address String?
telephone String?
mobile String?
email String?
invoiceEmail String?
invoiceAddress String?
logoKey String? // storage key for uploaded logo image (top of exported POs)
stampKey String? // storage key for uploaded company stamp/seal (signatory block of exported POs)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
purchaseOrders PurchaseOrder[]
}
model Account {
id String @id @default(cuid())
code String @unique
name String
description String?
isActive Boolean @default(true)
parentId String?
parent Account? @relation("AccountHierarchy", fields: [parentId], references: [id])
children Account[] @relation("AccountHierarchy")
purchaseOrders PurchaseOrder[]
lineItems POLineItem[]
}
model VendorContact {
id String @id @default(cuid())
name String
role String?
mobile String?
email String?
isPrimary Boolean @default(false)
createdAt DateTime @default(now())
vendorId String
vendor Vendor @relation(fields: [vendorId], references: [id], onDelete: Cascade)
}
model Vendor {
id String @id @default(cuid())
name String
vendorId String? @unique
address String?
pincode String?
gstin String?
latitude Float?
longitude Float?
isVerified Boolean @default(false)
isActive Boolean @default(true)
createdAt DateTime @default(now())
contacts VendorContact[]
purchaseOrders PurchaseOrder[]
products Product[] @relation("ProductLastVendor")
vendorPrices ProductVendorPrice[]
}
model Product {
id String @id @default(cuid())
code String @unique
name String
description String?
lastPrice Decimal? @db.Decimal(12, 2)
lastVendorId String?
lastVendor Vendor? @relation("ProductLastVendor", fields: [lastVendorId], references: [id])
isActive Boolean @default(true)
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
lineItems POLineItem[]
vendorPrices ProductVendorPrice[]
inventory ItemInventory[]
consumption ItemConsumption[]
}
model ProductVendorPrice {
id String @id @default(cuid())
price Decimal @db.Decimal(12, 2)
updatedAt DateTime @updatedAt
productId String
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
vendorId String
vendor Vendor @relation(fields: [vendorId], references: [id])
@@unique([productId, vendorId])
}
model ItemInventory {
id String @id @default(cuid())
quantity Decimal @db.Decimal(10, 3)
updatedAt DateTime @updatedAt
productId String
product Product @relation(fields: [productId], references: [id])
siteId String
site Site @relation(fields: [siteId], references: [id])
@@unique([productId, siteId])
}
model ItemConsumption {
id String @id @default(cuid())
date DateTime @db.Date
quantity Decimal @db.Decimal(10, 3)
note String?
productId String
product Product @relation(fields: [productId], references: [id])
siteId String
site Site @relation(fields: [siteId], references: [id])
recordedById String
recordedBy User @relation(fields: [recordedById], references: [id])
@@unique([productId, siteId, date])
}
model PurchaseOrder {
id String @id @default(cuid())
poNumber String @unique
title String
status POStatus @default(DRAFT)
totalAmount Decimal @db.Decimal(12, 2)
currency String @default("INR")
dateRequired DateTime?
projectCode String?
managerNote String?
paymentRef String?
paymentDate DateTime?
paidAmount Decimal? @db.Decimal(12, 2)
piQuotationNo String?
piQuotationDate DateTime?
requisitionNo String?
requisitionDate DateTime?
placeOfDelivery String?
tcDelivery String?
tcDispatch String?
tcInspection String?
tcTransitInsurance String?
tcPaymentTerms String?
tcOthers String?
poDate DateTime?
submittedAt DateTime?
approvedAt DateTime?
paidAt DateTime?
closedAt DateTime?
cancelledAt DateTime?
cancellationReason String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
submitterId String
submitter User @relation("Submitter", fields: [submitterId], references: [id])
vesselId String
vessel Vessel @relation(fields: [vesselId], references: [id])
accountId String
account Account @relation(fields: [accountId], references: [id])
companyId String?
company Company? @relation(fields: [companyId], references: [id])
vendorId String?
vendor Vendor? @relation(fields: [vendorId], references: [id])
siteId String?
site Site? @relation(fields: [siteId], references: [id])
// Supersede: a cancelled PO may be linked to the existing PO that replaces it.
// `supersededBy` is that replacement; `supersedes` is the reciprocal list.
supersededById String?
supersededBy PurchaseOrder? @relation("Supersede", fields: [supersededById], references: [id])
supersedes PurchaseOrder[] @relation("Supersede")
lineItems POLineItem[]
documents PODocument[]
actions POAction[]
receipt Receipt?
notifications Notification[]
}
model POLineItem {
id String @id @default(cuid())
name String
description String?
quantity Decimal @db.Decimal(10, 3)
unit String
unitPrice Decimal @db.Decimal(12, 2)
totalPrice Decimal @db.Decimal(12, 2)
gstRate Decimal @default(0.18) @db.Decimal(5, 4)
sortOrder Int @default(0)
size String?
deliveredQuantity Decimal? @db.Decimal(10, 3)
productId String?
product Product? @relation(fields: [productId], references: [id])
accountId String?
account Account? @relation(fields: [accountId], references: [id])
poId String
po PurchaseOrder @relation(fields: [poId], references: [id], onDelete: Cascade)
}
model PODocument {
id String @id @default(cuid())
fileName String
fileSize Int
mimeType String
storageKey String
uploadedAt DateTime @default(now())
poId String
po PurchaseOrder @relation(fields: [poId], references: [id], onDelete: Cascade)
}
model POAction {
id String @id @default(cuid())
actionType ActionType
note String?
metadata Json?
createdAt DateTime @default(now())
poId String
po PurchaseOrder @relation(fields: [poId], references: [id])
actorId String
actor User @relation(fields: [actorId], references: [id])
}
model Receipt {
id String @id @default(cuid())
storageKey String
fileName String
notes String?
confirmedAt DateTime @default(now())
poId String @unique
po PurchaseOrder @relation(fields: [poId], references: [id])
}
model Notification {
id String @id @default(cuid())
subject String
body String
link String?
isRead Boolean @default(false)
sentAt DateTime @default(now())
status String @default("sent")
poId String?
po PurchaseOrder? @relation(fields: [poId], references: [id])
userId String
user User @relation(fields: [userId], references: [id])
}
// ─── Crewing reference data ──────────────────────────────────────────────────
// The crew org hierarchy. A self-referential tree (parent/children), exactly
// like the Account accounting-code hierarchy. Reference data managed at
// /admin/ranks. `grantsLogin` is true only for the three management ranks
// (PM, Assistant PM, Site In-charge) — every other rank is a crew member /
// data subject with no portal account. See Crewing-Data-Model §2.
model Rank {
id String @id @default(cuid())
code String @unique
name String
description String?
category RankCategory @default(OPERATIONAL)
isSeafarer Boolean @default(false)
grantsLogin Boolean @default(false)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parentId String?
parent Rank? @relation("RankHierarchy", fields: [parentId], references: [id])
children Rank[] @relation("RankHierarchy")
docRequirements RankDocRequirement[]
}
// Which documents a rank is required (or conditionally required) to hold.
// `isMandatory = false` is the "conditional" tag in the UI.
model RankDocRequirement {
id String @id @default(cuid())
rankId String
rank Rank @relation(fields: [rankId], references: [id], onDelete: Cascade)
docType SeafarerDocType
isMandatory Boolean @default(true)
note String?
createdAt DateTime @default(now())
@@unique([rankId, docType])
}