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
  REFERENCE_RECORDED
  SALARY_AGREED
  SALARY_APPROVED
  SALARY_RETURNED
  CANDIDATE_PROPOSED
  INTERVIEW_RECORDED
  WAIVER_REQUESTED
  WAIVER_APPROVED
  WAIVER_DECLINED
  CANDIDATE_SELECTED
  SELECTION_RETURNED
  APPLICATION_REJECTED
  CREW_ONBOARDED
  DOCUMENT_UPLOADED
  RECORD_UPDATED
  RECORD_DELETED
  PPE_ISSUED
  PPE_RETURNED
  EXPERIENCE_ADDED
  LEAVE_APPLIED
  LEAVE_DECIDED
  ATTENDANCE_RECORDED
  CREW_SIGNED_OFF
  RECORD_VERIFIED
  RECORD_REJECTED
  APPRAISAL_SUBMITTED
  APPRAISAL_VERIFIED
  APPRAISAL_APPROVED
  APPRAISAL_REJECTED
}

// ─── Crewing appraisal (Phase 5b, Epic H) ───────────────────────────────────
// Appraisal lifecycle (Crewing-Implementation-Spec §5.4/§8.14): a PM raises
// (→ SUBMITTED), the MPO verifies (→ MPO_VERIFIED), the Manager approves
// (→ MANAGER_APPROVED); → REJECTED with remarks from either review.
enum AppraisalStatus {
  DRAFT
  SUBMITTED
  MPO_VERIFIED
  MANAGER_APPROVED
  REJECTED
}

// ─── Crewing leave & attendance (Phase 4b, Epic G) ──────────────────────────
// Leave is applied by the Site In-charge on a crew member and decided by the
// Manager (the MPO has no leave role — R1). See Crewing-Data-Model §1/§4.
enum LeaveType {
  ANNUAL
  MEDICAL
  EMERGENCY
  UNPAID
  OTHER
}

enum LeaveStatus {
  APPLIED
  APPROVED
  REJECTED
  CANCELLED
}

// Daily attendance (§8.10). v1 is the daily model; hours/overtime is deferred (A7).
enum AttendanceStatus {
  PRESENT
  ABSENT
  HALF_DAY
  ON_LEAVE
  SIGN_OFF
}

// PPE kit items issued to crew (Phase 4a, Epic F). See Crewing-Data-Model §1.
enum PpeItem {
  BOILER_SUIT
  SAFETY_SHOES
  HELMET
  VEST
  GLOVES
  MASK
  GOGGLES
  TIFFIN
  TORCH
  WALKIE_TALKIE
}

// ─── 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[]

  // Site-staff home site (Crewing §8.7 own-site scoping). Null = unscoped.
  siteId String?
  site   Site?   @relation(fields: [siteId], references: [id])
}

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[]
  staff          User[]
}

model Vessel {
  id       String  @id @default(cuid())
  name     String
  code     String  @unique
  isActive Boolean @default(true)

  purchaseOrders   PurchaseOrder[]
  requisitions     Requisition[]
  reliefRequests   ReliefRequest[]
  assignments      CrewAssignment[]
  rankRequirements VesselRankRequirement[]
}

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[]
  deliveryLocations DeliveryLocation[]
}

// Admin-managed delivery destinations (issue #19). Each is a Company + a
// free-text address; the PO "Place of Delivery" field becomes a dropdown sourced
// from these. The PO stores the resolved text snapshot in
// PurchaseOrder.placeOfDelivery (point-in-time document), so deleting/editing a
// location never rewrites historical POs. Managed by manage_delivery_locations.
model DeliveryLocation {
  id        String   @id @default(cuid())
  companyId String
  company   Company  @relation(fields: [companyId], references: [id])
  address   String
  isActive  Boolean  @default(true)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([companyId])
}

// Admin-managed Terms & Conditions catalogue (issue #11). Categories are
// user-defined data (not a fixed set) — admins add new ones — and every PO T&C
// line is a catalogued clause, including the standard "fixed" lines (seeded under
// a "General" category) and the "Others" bucket. The PO form is a dynamic editor:
// add rows, pick a category, type/pick a clause. The chosen rows are stored as a
// JSON snapshot on PurchaseOrder.terms, so editing/removing a clause never
// rewrites historical POs. Managed by manage_terms.
model TermsCategory {
  id        String           @id @default(cuid())
  name      String           @unique
  sortOrder Int              @default(0)
  isActive  Boolean          @default(true)
  createdAt DateTime         @default(now())
  updatedAt DateTime         @updatedAt
  clauses   TermsCondition[]
}

model TermsCondition {
  id         String        @id @default(cuid())
  categoryId String
  category   TermsCategory @relation(fields: [categoryId], references: [id], onDelete: Cascade)
  text       String
  // Pre-added to a new PO's default T&C set (reproduces the old standard wording).
  isDefault  Boolean       @default(false)
  isActive   Boolean       @default(true)
  sortOrder  Int           @default(0)
  createdAt  DateTime      @default(now())
  updatedAt  DateTime      @updatedAt

  @@index([categoryId])
}

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)
  // Advance the approving Manager wants paid first (absolute amount, not %).
  // The approval slider (0–100% of totalAmount) is convenience only — the
  // resolved amount is stored here. Null on legacy/pre-feature POs ⇒ no explicit
  // advance, so Accounts defaults to the full remaining balance. Set once at
  // approval and not edited afterwards (issue #92).
  //
  // NOTE (issue #91): this IS the "exact sum due for payment" for an ADVANCE/PART
  // request. When the structured payment-request lane (payment-term enum +
  // separate approval) is built, reuse this column for the requested amount
  // rather than adding a parallel "exact sum" field.
  suggestedAdvancePayment 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?
  // Dynamic T&C snapshot (issue #11): [{ category, text }] chosen on the PO form.
  // When present it supersedes the legacy tc* columns for display/export; null on
  // pre-feature POs (which still render from tc* + the fixed boilerplate lines).
  terms              Json?
  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[]
  experienceRecords  ExperienceRecord[]
  vesselRequirements VesselRankRequirement[]
}

// 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[]
  documents         SeafarerDocument[]
  nextOfKin         NextOfKin[]
  experienceRecords ExperienceRecord[]
  ppeIssues         PpeIssue[]
}

// ─── 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?
  // EPFO assisted-lookup result (recorded from the EpfoService check, A3).
  epfoMemberName     String?
  epfoCheckedAt      DateTime?
  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?
  leaveRequests    LeaveRequest[]
  attendance       Attendance[]
  appraisals       Appraisal[]
}

// A periodic appraisal on a tour of duty (Phase 5b). Actor ids are denormalised
// strings — the audited actor lives on the CrewAction.
model Appraisal {
  id             String          @id @default(cuid())
  assignmentId   String
  assignment     CrewAssignment  @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
  period         String // e.g. "2026" or "2026-Q2"
  ratings        Json?
  comments       String?
  status         AppraisalStatus @default(SUBMITTED)
  rejectedReason String?
  addedById      String
  verifiedById   String?
  approvedById   String?
  createdAt      DateTime        @default(now())
  updatedAt      DateTime        @updatedAt
}

// Leave applied by the Site In-charge on a crew member's assignment, decided by
// the Manager (§8.9, R1). Actor ids are denormalised strings — the audited actor
// lives on the CrewAction.
model LeaveRequest {
  id           String         @id @default(cuid())
  assignmentId String
  assignment   CrewAssignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
  type         LeaveType      @default(ANNUAL)
  fromDate     DateTime
  toDate       DateTime
  reason       String?
  status       LeaveStatus    @default(APPLIED)
  appliedById  String
  decidedById  String?
  decidedAt    DateTime?
  createdAt    DateTime       @default(now())
  updatedAt    DateTime       @updatedAt
}

// One attendance mark per assignment per day (§8.10). Site staff + Manager only.
model Attendance {
  id           String           @id @default(cuid())
  assignmentId String
  assignment   CrewAssignment   @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
  date         DateTime         @db.Date
  status       AttendanceStatus
  note         String?
  recordedById String
  createdAt    DateTime         @default(now())
  updatedAt    DateTime         @updatedAt

  @@unique([assignmentId, date])
}

// Required crew strength per rank, per vessel (Phase 4b, Option A). Drives
// leave-clash detection (§5.3, R6): approving a leave is a clash when the active
// same-rank cover over the window would fall below this. Managed by the office
// (manage_crew). Absent a row, the clash check falls back to a strength of 1.
model VesselRankRequirement {
  id          String   @id @default(cuid())
  vesselId    String
  vessel      Vessel   @relation(fields: [vesselId], references: [id], onDelete: Cascade)
  rankId      String
  rank        Rank     @relation(fields: [rankId], references: [id], onDelete: Cascade)
  minStrength Int      @default(1)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  @@unique([vesselId, rankId])
}

// 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())
}

// ─── Crewing crew records (Phase 4a, Epics E + F) ───────────────────────────

// A held document on the crew profile (medical, passport, CDC, STCW, …). The
// verify queue (MPO/Accounts) lands in Phase 5; here we capture + display, with
// `verificationStatus` carried and "expired" derived from expiryDate in the UI.
model SeafarerDocument {
  id                 String          @id @default(cuid())
  crewMemberId       String
  crewMember         CrewMember      @relation(fields: [crewMemberId], references: [id], onDelete: Cascade)
  docType            SeafarerDocType
  number             String? // PII — masked in the UI for non-privileged roles
  fileKey            String?
  issueDate          DateTime?
  expiryDate         DateTime?
  verificationStatus GateResult      @default(PENDING)
  verifiedById       String?
  createdAt          DateTime        @default(now())
  updatedAt          DateTime        @updatedAt
}

// Next of kin / emergency contacts (§8.8). `isEmergency` marks the emergency row.
model NextOfKin {
  id                 String     @id @default(cuid())
  crewMemberId       String
  crewMember         CrewMember @relation(fields: [crewMemberId], references: [id], onDelete: Cascade)
  name               String
  relationship       String?
  phone              String?
  address            String?
  isEmergency        Boolean    @default(false)
  verificationStatus GateResult @default(PENDING) // MPO verifies (Phase 5 follow-up)
  verifiedById       String?
  createdAt          DateTime   @default(now())
}

// A tour-of-duty experience row — added manually or auto-appended at sign-off
// (Phase 4c). `source` is "internal" (a PPMS assignment) or "declared".
model ExperienceRecord {
  id             String     @id @default(cuid())
  crewMemberId   String
  crewMember     CrewMember @relation(fields: [crewMemberId], references: [id], onDelete: Cascade)
  vesselType     String?
  rankId         String?
  rank           Rank?      @relation(fields: [rankId], references: [id])
  fromDate       DateTime?
  toDate         DateTime?
  durationMonths Int?
  source         String     @default("declared")
  createdAt      DateTime   @default(now())
}

// PPE issued to a crew member (§8.8). A reissue is a new row; `returnedDate`
// marks a returned item. Optional ItemInventory draw-down is a later refinement.
model PpeIssue {
  id                 String     @id @default(cuid())
  crewMemberId       String
  crewMember         CrewMember @relation(fields: [crewMemberId], references: [id], onDelete: Cascade)
  item               PpeItem
  size               String?
  quantity           Int        @default(1)
  issuedDate         DateTime   @default(now())
  returnedDate       DateTime?
  issuedById         String?
  comment            String?
  verificationStatus GateResult @default(PENDING) // MPO verifies (Phase 5 follow-up)
  verifiedById       String?
  createdAt          DateTime   @default(now())
}
