Final slice of Phase 3 (stacked on 3b pipeline). The onboarding transaction that turns a SELECTED candidate into active crew, per Crewing-Implementation-Spec §8.5/§9/§11. Behind NEXT_PUBLIC_CREWING_ENABLED; production unchanged. What's in - Schema (crewing_onboarding migration): CrewAssignment + AssignmentStatus (ACTIVE/ON_LEAVE/SIGNED_OFF — leave/sign-off are Phase 4); ContractLetter (salaryRestricted); SalaryStructure += assignmentId; CrewActionType += CREW_ONBOARDED. Employee numbers CRW-xxxx via lib/employee-number.ts. - Action (onboardCandidate, onboard_crew): one transaction off a SELECTED application — assign employeeId, create CrewAssignment(ACTIVE, signOnDate), bind the approved SalaryStructure (assignmentId + effectiveFrom), Application → ONBOARDED, Requisition → FILLED, CrewMember → EMPLOYEE (+ currentRank); contract letter stored after. Guards flag + permission + SELECTED state. - Screen: the SELECTED action card's "Onboard to crew" modal (joining date, contract upload, starts-automatically chips); the CRW- number shows on the ONBOARDED card. Tests & docs - Integration: onboarding.test.ts (5) — full transaction, requisition FILLED + salary binding, joining-date + SELECTED-only guards, permission gating, sequential CRW- ids. type-check clean; full unit (234) + integration (168) green. - CLAUDE.md updated with the Phase 3c surface. Deferred: SITE_STAFF login creation for management ranks (grantsLogin) — a follow-up; attendance/experience/PPE records begin in Phase 4. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
872 lines
28 KiB
Text
872 lines
28 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,
|
||
// Phase 3a adds candidate intake.
|
||
enum CrewActionType {
|
||
REQUISITION_RAISED
|
||
REQUISITION_ADVANCED
|
||
REQUISITION_FILLED
|
||
REQUISITION_CANCELLED
|
||
RELIEF_REQUESTED
|
||
RELIEF_CONVERTED
|
||
RELIEF_CANCELLED
|
||
CANDIDATE_ADDED
|
||
CANDIDATE_UPDATED
|
||
APPLICATION_CREATED
|
||
GATE_PASSED
|
||
GATE_FAILED
|
||
REFERENCE_RECORDED
|
||
SALARY_AGREED
|
||
SALARY_APPROVED
|
||
CANDIDATE_PROPOSED
|
||
INTERVIEW_RECORDED
|
||
WAIVER_REQUESTED
|
||
WAIVER_APPROVED
|
||
CANDIDATE_SELECTED
|
||
APPLICATION_REJECTED
|
||
CREW_ONBOARDED
|
||
}
|
||
|
||
// ─── Crewing recruitment pipeline (Phase 3b: Epic C) ────────────────────────
|
||
// The gated 7-stage application pipeline (Crewing-Implementation-Spec §5.1).
|
||
// ONBOARDED is the terminal system state set at onboarding (Phase 3c);
|
||
// REJECTED is the branch reachable from any active stage.
|
||
enum ApplicationStage {
|
||
SHORTLISTED
|
||
COMPETENCY_AND_REFERENCES
|
||
DOC_VERIFICATION
|
||
SALARY_AGREEMENT
|
||
PROPOSED
|
||
INTERVIEW
|
||
SELECTED
|
||
REJECTED
|
||
ONBOARDED
|
||
}
|
||
|
||
// A vetting gate on an application. SALARY / SELECTION / WAIVER are the
|
||
// Manager-decided gates that surface in the central Approvals queue (§8.13).
|
||
enum ApplicationGateType {
|
||
COMPETENCY_REFERENCE
|
||
DOCUMENT
|
||
SALARY
|
||
INTERVIEW
|
||
WAIVER
|
||
SELECTION
|
||
}
|
||
|
||
enum GateResult {
|
||
PENDING
|
||
VERIFIED
|
||
REJECTED
|
||
}
|
||
|
||
// MPO's recorded interview outcome (Manager then approves selection).
|
||
enum InterviewOutcome {
|
||
PENDING
|
||
ACCEPTED
|
||
REJECTED
|
||
}
|
||
|
||
// Salary capture basis — the other is derived (R10/A4). Effective-dated.
|
||
enum SalaryRateBasis {
|
||
MONTHLY
|
||
DAILY
|
||
}
|
||
|
||
// A crew member's tour of duty (Phase 3c, Epic D). Created at onboarding; the
|
||
// leave/sign-off transitions land in Phase 4. See Crewing-Data-Model §4.
|
||
enum AssignmentStatus {
|
||
ACTIVE
|
||
ON_LEAVE
|
||
SIGNED_OFF
|
||
}
|
||
|
||
// ─── Crewing candidates (Phase 3a: Epic B) ──────────────────────────────────
|
||
// A CrewMember is the talent-pool spine: a row exists from first contact and
|
||
// persists through CANDIDATE → EMPLOYEE → EX_HAND. `employeeId` is assigned only
|
||
// at onboarding (Phase 3c). See Crewing-Data-Model §4 + Implementation-Spec §8.6.
|
||
enum CrewStatus {
|
||
PROSPECT
|
||
CANDIDATE
|
||
EMPLOYEE
|
||
EX_HAND
|
||
BLACKLISTED
|
||
}
|
||
|
||
// NEW applicants vs returning EX_HAND crew (drives the ex-hand affordances).
|
||
enum CandidateType {
|
||
NEW
|
||
EX_HAND
|
||
}
|
||
|
||
// Where the candidate came from (the §8.6 "Source" column; ex-hand renders purple).
|
||
enum CandidateSource {
|
||
CAREERS
|
||
EX_HAND
|
||
WALK_IN
|
||
REFERRAL
|
||
OTHER
|
||
}
|
||
|
||
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[]
|
||
assignments CrewAssignment[]
|
||
}
|
||
|
||
model Vessel {
|
||
id String @id @default(cuid())
|
||
name String
|
||
code String @unique
|
||
isActive Boolean @default(true)
|
||
|
||
purchaseOrders PurchaseOrder[]
|
||
requisitions Requisition[]
|
||
reliefRequests ReliefRequest[]
|
||
assignments CrewAssignment[]
|
||
}
|
||
|
||
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[]
|
||
crewCurrentRank CrewMember[] @relation("CrewCurrentRank")
|
||
crewAppliedRank CrewMember[] @relation("CrewAppliedRank")
|
||
assignments CrewAssignment[]
|
||
}
|
||
|
||
// 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[]
|
||
applications Application[]
|
||
assignment CrewAssignment?
|
||
}
|
||
|
||
// 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,
|
||
// Phase 3a adds candidates. A row references at most one entity (the rest null).
|
||
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])
|
||
crewMemberId String?
|
||
crewMember CrewMember? @relation(fields: [crewMemberId], references: [id])
|
||
applicationId String?
|
||
application Application? @relation(fields: [applicationId], references: [id])
|
||
}
|
||
|
||
// The talent-pool spine (Phase 3a, Epic B). One row per person, created the
|
||
// moment they enter the pool and kept through CANDIDATE → EMPLOYEE → EX_HAND, so
|
||
// an ex-hand's history/documents are already on file. `employeeId` is assigned
|
||
// at onboarding (Phase 3c). The recruitment pipeline (Applications, Phase 3b)
|
||
// and crew records (Phase 4) hang off this model. See Crewing-Data-Model §4.
|
||
model CrewMember {
|
||
id String @id @default(cuid())
|
||
employeeId String? @unique // assigned at onboarding (Phase 3c)
|
||
name String
|
||
status CrewStatus @default(CANDIDATE)
|
||
type CandidateType @default(NEW)
|
||
source CandidateSource @default(CAREERS)
|
||
email String?
|
||
phone String?
|
||
dob DateTime?
|
||
experienceMonths Int @default(0)
|
||
vesselTypeExperience String? // free-text "vessel type" from the Add-candidate modal
|
||
cvKey String? // storage key for an uploaded CV (no parsing yet — A2 deferred)
|
||
notes String?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
// Rank held / last held (ex-hands) and the rank being applied for.
|
||
currentRankId String?
|
||
currentRank Rank? @relation("CrewCurrentRank", fields: [currentRankId], references: [id])
|
||
appliedRankId String?
|
||
appliedRank Rank? @relation("CrewAppliedRank", fields: [appliedRankId], references: [id])
|
||
|
||
actions CrewAction[]
|
||
applications Application[]
|
||
bankDetail BankDetail?
|
||
epfDetail EpfDetail?
|
||
assignments CrewAssignment[]
|
||
}
|
||
|
||
// ─── Crewing recruitment pipeline models (Phase 3b) ─────────────────────────
|
||
|
||
// A candidate's application against one requisition — the gated pipeline spine
|
||
// (spec §5.1/§8.4–8.5). One application per (requisition, candidate).
|
||
model Application {
|
||
id String @id @default(cuid())
|
||
stage ApplicationStage @default(SHORTLISTED)
|
||
type CandidateType @default(NEW)
|
||
interviewResult InterviewOutcome @default(PENDING)
|
||
interviewWaived Boolean @default(false) // set true only on Manager-approved waiver (R2)
|
||
rejectedReason String?
|
||
rejectedAt DateTime?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
requisitionId String
|
||
requisition Requisition @relation(fields: [requisitionId], references: [id])
|
||
crewMemberId String
|
||
crewMember CrewMember @relation(fields: [crewMemberId], references: [id])
|
||
|
||
gates ApplicationGate[]
|
||
referenceChecks ReferenceCheck[]
|
||
salaryStructures SalaryStructure[]
|
||
actions CrewAction[]
|
||
|
||
@@unique([requisitionId, crewMemberId])
|
||
}
|
||
|
||
// One row per vetting gate. SALARY / SELECTION / WAIVER gates with result PENDING
|
||
// are the Manager's central Approvals-queue items (§8.13). `decidedById` is a
|
||
// denormalised actor id — the audited actor lives on the CrewAction.
|
||
model ApplicationGate {
|
||
id String @id @default(cuid())
|
||
applicationId String
|
||
application Application @relation(fields: [applicationId], references: [id], onDelete: Cascade)
|
||
gate ApplicationGateType
|
||
result GateResult @default(PENDING)
|
||
note String?
|
||
decidedById String?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([applicationId, gate])
|
||
}
|
||
|
||
// Competency & reference checks recorded by the MPO at the COMPETENCY_AND_REFERENCES gate.
|
||
model ReferenceCheck {
|
||
id String @id @default(cuid())
|
||
applicationId String
|
||
application Application @relation(fields: [applicationId], references: [id], onDelete: Cascade)
|
||
refereeName String
|
||
refereeContact String?
|
||
outcome String? // free-text / "positive" | "negative"
|
||
note String?
|
||
recordedById String?
|
||
createdAt DateTime @default(now())
|
||
}
|
||
|
||
// The salary agreed at SALARY_AGREEMENT, sent for Manager approval. Effective-dated
|
||
// (R10/A4) and attached to the Application in 3b; onboarding (3c) binds it to the
|
||
// CrewAssignment. `approvedById` is set when the Manager approves the SALARY gate.
|
||
model SalaryStructure {
|
||
id String @id @default(cuid())
|
||
applicationId String
|
||
application Application @relation(fields: [applicationId], references: [id], onDelete: Cascade)
|
||
rateBasis SalaryRateBasis @default(MONTHLY)
|
||
basic Decimal @db.Decimal(12, 2)
|
||
victualingPerDay Decimal @default(0) @db.Decimal(12, 2)
|
||
allowances Json?
|
||
currency String @default("INR")
|
||
effectiveFrom DateTime?
|
||
effectiveTo DateTime?
|
||
approvedById String?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
// Bound to the assignment at onboarding (Phase 3c); null while still a proposal.
|
||
assignmentId String?
|
||
assignment CrewAssignment? @relation(fields: [assignmentId], references: [id])
|
||
}
|
||
|
||
// Bank details captured at DOC_VERIFICATION (needed downstream for payroll).
|
||
// NOTE: PII — field-level encryption/masking is a Phase-4 task (§11); stored
|
||
// plainly for now behind the crewing flag.
|
||
model BankDetail {
|
||
id String @id @default(cuid())
|
||
crewMemberId String @unique
|
||
crewMember CrewMember @relation(fields: [crewMemberId], references: [id], onDelete: Cascade)
|
||
accountName String?
|
||
accountNumber String?
|
||
ifsc String?
|
||
bankName String?
|
||
verificationStatus GateResult @default(PENDING) // verified by Accounts in a later phase
|
||
verifiedById String?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
// EPF / identity details captured at DOC_VERIFICATION. PII note as BankDetail.
|
||
model EpfDetail {
|
||
id String @id @default(cuid())
|
||
crewMemberId String @unique
|
||
crewMember CrewMember @relation(fields: [crewMemberId], references: [id], onDelete: Cascade)
|
||
uan String?
|
||
aadhaarLast4 String?
|
||
pfNumber String?
|
||
verificationStatus GateResult @default(PENDING)
|
||
verifiedById String?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
}
|
||
|
||
// ─── Crewing onboarding (Phase 3c: Epic D) ──────────────────────────────────
|
||
|
||
// A single tour of duty, created at onboarding. Flips the requisition to FILLED
|
||
// and the crew member to EMPLOYEE. Leave/sign-off transitions arrive in Phase 4.
|
||
model CrewAssignment {
|
||
id String @id @default(cuid())
|
||
status AssignmentStatus @default(ACTIVE)
|
||
signOnDate DateTime
|
||
signOffDate DateTime?
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
crewMemberId String
|
||
crewMember CrewMember @relation(fields: [crewMemberId], references: [id])
|
||
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])
|
||
// The requisition this assignment fills (one assignment per requisition).
|
||
requisitionId String? @unique
|
||
requisition Requisition? @relation(fields: [requisitionId], references: [id])
|
||
|
||
salaryStructures SalaryStructure[]
|
||
contractLetter ContractLetter?
|
||
}
|
||
|
||
// The signed contract for an assignment. `salaryRestricted` hides salary from
|
||
// site staff on the crew profile (Phase 4 display gating).
|
||
model ContractLetter {
|
||
id String @id @default(cuid())
|
||
assignmentId String @unique
|
||
assignment CrewAssignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
|
||
fileKey String
|
||
salaryRestricted Boolean @default(true)
|
||
createdAt DateTime @default(now())
|
||
}
|