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