Second slice of the Crewing module per wiki Crewing-Implementation-Spec §12 (build order item 2). Everything stays behind NEXT_PUBLIC_CREWING_ENABLED; production is unchanged. Schema is added incrementally — this lands the requisition lifecycle layer. What's in - Schema: Requisition (OPEN→SHORTLISTING→PROPOSING→INTERVIEWING→SELECTED→FILLED, →CANCELLED), ReliefRequest, CrewAction (the POAction mirror) + their enums. Migration crewing_requisitions. - State machine: lib/requisition-state-machine.ts mirrors po-state-machine (selection Manager-only; orthogonal cancel from OPEN/SHORTLISTING by cancel_requisition holders, §6). Codes REQ-9000… via lib/requisition-number.ts. - Actions: raise/cancel/transition + requestReliefCover/convertReliefToRequisition, each guarding flag+permission+state, writing a CrewAction and notifying. Shared autoRaiseRequisition() (lib/requisition-service.ts) is the backfill entry point for sign-off / leave-clash (later phases). - Notifier: notifyCrew() PO-independent path + CrewNotificationEvent. - Screens: /crewing/requisitions (list + Raise modal + relief convert) and /crewing/requisitions/[id] (detail). Requisitions added to the flag-gated Crewing sidebar (Manager + MPO, §7). Tests & docs - Unit: requisition-state-machine.test.ts (11). - Integration: requisitions.test.ts (15) — raise/cancel/transition, relief request + convert, auto-raise, permission gating. - CLAUDE.md "Crewing" section updated with the Phase 2 surface. Deferred: sign-off/experience (Epic K, §12 item 2) depends on the crew/assignment models from Phase 3/4; autoRaiseRequisition() is ready for it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
577 lines
17 KiB
Text
577 lines
17 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
|
|
}
|
|
|
|
// ─── Crewing lifecycle (Phase 2: Requisitions + relief) ─────────────────────
|
|
// Requisition lifecycle — Crewing-Implementation-Spec §5.2. The intermediate
|
|
// stages (SHORTLISTING…SELECTED) are advanced by the recruitment pipeline that
|
|
// lands in Phase 3; Phase 2 wires OPEN, CANCELLED and the FILLED terminal.
|
|
enum RequisitionStatus {
|
|
OPEN
|
|
SHORTLISTING
|
|
PROPOSING
|
|
INTERVIEWING
|
|
SELECTED
|
|
FILLED
|
|
CANCELLED
|
|
}
|
|
|
|
// Why a vacancy exists. LEAVE / SIGN_OFF / END_OF_CONTRACT are the system
|
|
// auto-raise reasons (§5.2/§5.3); the rest are raised manually by MPO/Manager.
|
|
enum RequisitionReason {
|
|
NEW_VACANCY
|
|
REPLACEMENT
|
|
LEAVE
|
|
SIGN_OFF
|
|
END_OF_CONTRACT
|
|
OTHER
|
|
}
|
|
|
|
// A foreseen-gap flag raised by site staff (§8.2 "Relief requests from sites").
|
|
// The office converts an OPEN relief request into a real requisition.
|
|
enum ReliefRequestStatus {
|
|
OPEN
|
|
CONVERTED
|
|
CANCELLED
|
|
}
|
|
|
|
// Crewing audit-trail action types — the CrewAction mirror of ActionType for
|
|
// POAction (§4.5/§11). Extended per phase; Phase 2 covers requisition + relief.
|
|
enum CrewActionType {
|
|
REQUISITION_RAISED
|
|
REQUISITION_ADVANCED
|
|
REQUISITION_FILLED
|
|
REQUISITION_CANCELLED
|
|
RELIEF_REQUESTED
|
|
RELIEF_CONVERTED
|
|
RELIEF_CANCELLED
|
|
}
|
|
|
|
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")
|
|
requisitionsRaised Requisition[] @relation("RequisitionRaiser")
|
|
reliefRequested ReliefRequest[] @relation("ReliefRequester")
|
|
crewActions CrewAction[]
|
|
}
|
|
|
|
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[]
|
|
requisitions Requisition[]
|
|
reliefRequests ReliefRequest[]
|
|
}
|
|
|
|
model Vessel {
|
|
id String @id @default(cuid())
|
|
name String
|
|
code String @unique
|
|
isActive Boolean @default(true)
|
|
|
|
purchaseOrders PurchaseOrder[]
|
|
requisitions Requisition[]
|
|
reliefRequests ReliefRequest[]
|
|
}
|
|
|
|
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[]
|
|
requisitions Requisition[]
|
|
reliefRequests ReliefRequest[]
|
|
}
|
|
|
|
// 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])
|
|
}
|
|
|
|
// ─── Crewing lifecycle models (Phase 2) ──────────────────────────────────────
|
|
|
|
// A vacancy to be filled for a rank on a vessel/site. Raised manually by
|
|
// MPO/Manager, or auto-raised by the system on a leave clash / sign-off / EOC
|
|
// (autoRaised = true). The recruitment pipeline (Phase 3) attaches candidates
|
|
// and drives the intermediate stages. See Crewing-Implementation-Spec §5.2/§8.
|
|
model Requisition {
|
|
id String @id @default(cuid())
|
|
code String @unique // mono id, e.g. REQ-9000
|
|
status RequisitionStatus @default(OPEN)
|
|
reason RequisitionReason @default(NEW_VACANCY)
|
|
autoRaised Boolean @default(false)
|
|
neededBy DateTime?
|
|
notes String?
|
|
cancelledAt DateTime?
|
|
cancellationReason String?
|
|
filledAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
rankId String
|
|
rank Rank @relation(fields: [rankId], references: [id])
|
|
vesselId String?
|
|
vessel Vessel? @relation(fields: [vesselId], references: [id])
|
|
siteId String?
|
|
site Site? @relation(fields: [siteId], references: [id])
|
|
|
|
// Null when the system auto-raised it.
|
|
raisedById String?
|
|
raisedBy User? @relation("RequisitionRaiser", fields: [raisedById], references: [id])
|
|
|
|
// The site relief request this requisition was converted from, if any.
|
|
sourceReliefRequest ReliefRequest? @relation("ReliefConversion")
|
|
|
|
actions CrewAction[]
|
|
}
|
|
|
|
// A foreseen-gap flag from a site (site staff), pending office conversion into a
|
|
// Requisition. Complementary, proactive channel to the auto-raised LEAVE
|
|
// requisition. See Crewing-Implementation-Spec §8.2 (R3/R6).
|
|
model ReliefRequest {
|
|
id String @id @default(cuid())
|
|
status ReliefRequestStatus @default(OPEN)
|
|
note String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
rankId String
|
|
rank Rank @relation(fields: [rankId], references: [id])
|
|
vesselId String?
|
|
vessel Vessel? @relation(fields: [vesselId], references: [id])
|
|
siteId String?
|
|
site Site? @relation(fields: [siteId], references: [id])
|
|
|
|
requestedById String
|
|
requestedBy User @relation("ReliefRequester", fields: [requestedById], references: [id])
|
|
|
|
// Set when an MPO/Manager converts it; one relief request → one requisition.
|
|
convertedRequisitionId String? @unique
|
|
convertedRequisition Requisition? @relation("ReliefConversion", fields: [convertedRequisitionId], references: [id])
|
|
}
|
|
|
|
// Crewing audit trail — one row per transition / verification (mirror of
|
|
// POAction). Entity relations are added per phase; Phase 2 links requisitions.
|
|
model CrewAction {
|
|
id String @id @default(cuid())
|
|
actionType CrewActionType
|
|
note String?
|
|
metadata Json?
|
|
createdAt DateTime @default(now())
|
|
|
|
// Null for system-performed actions (auto-raise).
|
|
actorId String?
|
|
actor User? @relation(fields: [actorId], references: [id])
|
|
|
|
requisitionId String?
|
|
requisition Requisition? @relation(fields: [requisitionId], references: [id])
|
|
}
|