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 clauses (issue #11). Each clause belongs to a // category matching one of the PO's named T&C slots; the PO form turns those slots // into dropdowns sourced from the active clauses of that category. The PO keeps // the chosen clause as a text snapshot in its tc* columns (point-in-time // document), so editing/removing a clause never rewrites historical POs. Managed // by manage_terms. ("Others" stays free text; the fixed boilerplate lines — // TC_FIXED_LINE / TC_FIXED_LINE_2 — are not catalogued.) enum TermsCategory { DELIVERY DISPATCH INSPECTION TRANSIT_INSURANCE PAYMENT_TERMS } model TermsCondition { id String @id @default(cuid()) category TermsCategory text String isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([category]) } 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()) }