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. enum CrewActionType { REQUISITION_RAISED REQUISITION_ADVANCED REQUISITION_FILLED REQUISITION_CANCELLED RELIEF_REQUESTED RELIEF_CONVERTED RELIEF_CANCELLED } model User { id String @id @default(cuid()) employeeId String @unique email String @unique name String passwordHash String? role Role isActive Boolean @default(true) signatureKey String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt submittedPOs PurchaseOrder[] @relation("Submitter") actions POAction[] notifications Notification[] consumption ItemConsumption[] superUserRequests SuperUserRequest[] @relation("Requester") resolvedRequests SuperUserRequest[] @relation("RequestResolver") requisitionsRaised Requisition[] @relation("RequisitionRaiser") reliefRequested ReliefRequest[] @relation("ReliefRequester") crewActions CrewAction[] } model SuperUserRequest { id String @id @default(cuid()) userId String user User @relation("Requester", fields: [userId], references: [id]) reason String? status RequestStatus @default(PENDING) createdAt DateTime @default(now()) resolvedAt DateTime? resolvedById String? resolvedBy User? @relation("RequestResolver", fields: [resolvedById], references: [id]) } model Site { id String @id @default(cuid()) name String code String @unique address String? latitude Float? longitude Float? isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt purchaseOrders PurchaseOrder[] inventory ItemInventory[] consumption ItemConsumption[] requisitions Requisition[] reliefRequests ReliefRequest[] } model Vessel { id String @id @default(cuid()) name String code String @unique isActive Boolean @default(true) purchaseOrders PurchaseOrder[] requisitions Requisition[] reliefRequests ReliefRequest[] } model Company { id String @id @default(cuid()) name String code String? @unique gstNumber String? address String? telephone String? mobile String? email String? invoiceEmail String? invoiceAddress String? logoKey String? // storage key for uploaded logo image (top of exported POs) stampKey String? // storage key for uploaded company stamp/seal (signatory block of exported POs) isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt purchaseOrders PurchaseOrder[] } model Account { id String @id @default(cuid()) code String @unique name String description String? isActive Boolean @default(true) parentId String? parent Account? @relation("AccountHierarchy", fields: [parentId], references: [id]) children Account[] @relation("AccountHierarchy") purchaseOrders PurchaseOrder[] lineItems POLineItem[] } model VendorContact { id String @id @default(cuid()) name String role String? mobile String? email String? isPrimary Boolean @default(false) createdAt DateTime @default(now()) vendorId String vendor Vendor @relation(fields: [vendorId], references: [id], onDelete: Cascade) } model Vendor { id String @id @default(cuid()) name String vendorId String? @unique address String? pincode String? gstin String? latitude Float? longitude Float? isVerified Boolean @default(false) isActive Boolean @default(true) createdAt DateTime @default(now()) contacts VendorContact[] purchaseOrders PurchaseOrder[] products Product[] @relation("ProductLastVendor") vendorPrices ProductVendorPrice[] } model Product { id String @id @default(cuid()) code String @unique name String description String? lastPrice Decimal? @db.Decimal(12, 2) lastVendorId String? lastVendor Vendor? @relation("ProductLastVendor", fields: [lastVendorId], references: [id]) isActive Boolean @default(true) updatedAt DateTime @updatedAt createdAt DateTime @default(now()) lineItems POLineItem[] vendorPrices ProductVendorPrice[] inventory ItemInventory[] consumption ItemConsumption[] } model ProductVendorPrice { id String @id @default(cuid()) price Decimal @db.Decimal(12, 2) updatedAt DateTime @updatedAt productId String product Product @relation(fields: [productId], references: [id], onDelete: Cascade) vendorId String vendor Vendor @relation(fields: [vendorId], references: [id]) @@unique([productId, vendorId]) } model ItemInventory { id String @id @default(cuid()) quantity Decimal @db.Decimal(10, 3) updatedAt DateTime @updatedAt productId String product Product @relation(fields: [productId], references: [id]) siteId String site Site @relation(fields: [siteId], references: [id]) @@unique([productId, siteId]) } model ItemConsumption { id String @id @default(cuid()) date DateTime @db.Date quantity Decimal @db.Decimal(10, 3) note String? productId String product Product @relation(fields: [productId], references: [id]) siteId String site Site @relation(fields: [siteId], references: [id]) recordedById String recordedBy User @relation(fields: [recordedById], references: [id]) @@unique([productId, siteId, date]) } model PurchaseOrder { id String @id @default(cuid()) poNumber String @unique title String status POStatus @default(DRAFT) totalAmount Decimal @db.Decimal(12, 2) currency String @default("INR") dateRequired DateTime? projectCode String? managerNote String? paymentRef String? paymentDate DateTime? paidAmount Decimal? @db.Decimal(12, 2) piQuotationNo String? piQuotationDate DateTime? requisitionNo String? requisitionDate DateTime? placeOfDelivery String? tcDelivery String? tcDispatch String? tcInspection String? tcTransitInsurance String? tcPaymentTerms String? tcOthers String? poDate DateTime? submittedAt DateTime? approvedAt DateTime? paidAt DateTime? closedAt DateTime? cancelledAt DateTime? cancellationReason String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt submitterId String submitter User @relation("Submitter", fields: [submitterId], references: [id]) vesselId String vessel Vessel @relation(fields: [vesselId], references: [id]) accountId String account Account @relation(fields: [accountId], references: [id]) companyId String? company Company? @relation(fields: [companyId], references: [id]) vendorId String? vendor Vendor? @relation(fields: [vendorId], references: [id]) siteId String? site Site? @relation(fields: [siteId], references: [id]) // Supersede: a cancelled PO may be linked to the existing PO that replaces it. // `supersededBy` is that replacement; `supersedes` is the reciprocal list. supersededById String? supersededBy PurchaseOrder? @relation("Supersede", fields: [supersededById], references: [id]) supersedes PurchaseOrder[] @relation("Supersede") lineItems POLineItem[] documents PODocument[] actions POAction[] receipt Receipt? notifications Notification[] } model POLineItem { id String @id @default(cuid()) name String description String? quantity Decimal @db.Decimal(10, 3) unit String unitPrice Decimal @db.Decimal(12, 2) totalPrice Decimal @db.Decimal(12, 2) gstRate Decimal @default(0.18) @db.Decimal(5, 4) sortOrder Int @default(0) size String? deliveredQuantity Decimal? @db.Decimal(10, 3) productId String? product Product? @relation(fields: [productId], references: [id]) accountId String? account Account? @relation(fields: [accountId], references: [id]) poId String po PurchaseOrder @relation(fields: [poId], references: [id], onDelete: Cascade) } model PODocument { id String @id @default(cuid()) fileName String fileSize Int mimeType String storageKey String uploadedAt DateTime @default(now()) poId String po PurchaseOrder @relation(fields: [poId], references: [id], onDelete: Cascade) } model POAction { id String @id @default(cuid()) actionType ActionType note String? metadata Json? createdAt DateTime @default(now()) poId String po PurchaseOrder @relation(fields: [poId], references: [id]) actorId String actor User @relation(fields: [actorId], references: [id]) } model Receipt { id String @id @default(cuid()) storageKey String fileName String notes String? confirmedAt DateTime @default(now()) poId String @unique po PurchaseOrder @relation(fields: [poId], references: [id]) } model Notification { id String @id @default(cuid()) subject String body String link String? isRead Boolean @default(false) sentAt DateTime @default(now()) status String @default("sent") poId String? po PurchaseOrder? @relation(fields: [poId], references: [id]) userId String user User @relation(fields: [userId], references: [id]) } // ─── Crewing reference data ────────────────────────────────────────────────── // The crew org hierarchy. A self-referential tree (parent/children), exactly // like the Account accounting-code hierarchy. Reference data managed at // /admin/ranks. `grantsLogin` is true only for the three management ranks // (PM, Assistant PM, Site In-charge) — every other rank is a crew member / // data subject with no portal account. See Crewing-Data-Model §2. model Rank { id String @id @default(cuid()) code String @unique name String description String? category RankCategory @default(OPERATIONAL) isSeafarer Boolean @default(false) grantsLogin Boolean @default(false) isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt parentId String? parent Rank? @relation("RankHierarchy", fields: [parentId], references: [id]) children Rank[] @relation("RankHierarchy") docRequirements RankDocRequirement[] requisitions Requisition[] reliefRequests ReliefRequest[] } // 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[] } // 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. 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]) }