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])
}

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