// V11 Enterprise QMS — SQLite schema // NOTE: SQLite (Prisma connector) supports neither native enums nor the Json type. // All former enums are stored as String (validated in app code; union types live in @/types). // All former Json columns are stored as String (JSON-encoded; (de)serialised in app code). generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } // ─── USERS & AUTH ───────────────────────────────────────────────────────────── // Role: ADMIN | QC | PRODUCTION | PRODUCTION_LEAD | LOGISTICS_LEAD | MANAGEMENT model User { id String @id @default(cuid()) email String @unique name String password String role String @default("PRODUCTION") department String? active Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt sessions Session[] capasOwned CAPA[] @relation("CAPAOwner") capasRaised CAPA[] @relation("CAPARaisedBy") auditsLed Audit[] @relation("AuditLead") ncrsRaised NCR[] @relation("NCRRaisedBy") submissions FormSubmission[] auditLogs AuditLog[] notifications Notification[] } model Session { id String @id @default(cuid()) userId String token String @unique expiresAt DateTime user User @relation(fields: [userId], references: [id], onDelete: Cascade) } // ─── AUDIT TRAIL ────────────────────────────────────────────────────────────── model AuditLog { id String @id @default(cuid()) userId String action String entity String entityId String before String? // JSON-encoded after String? // JSON-encoded createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) @@index([entity, entityId]) @@index([userId]) @@index([createdAt]) } // ─── NOTIFICATIONS ──────────────────────────────────────────────────────────── // type: CAPA_OVERDUE | CAPA_ASSIGNED | AUDIT_DUE | NCR_ESCALATED | // FORM_REVIEW_READY | DOC_EXPIRING | STANDARD_APPROVED | SOLUTION_CONFIRMED model Notification { id String @id @default(cuid()) userId String type String title String body String read Boolean @default(false) link String? createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, read]) } // ─── CAPA ───────────────────────────────────────────────────────────────────── // priority: CRITICAL | HIGH | MEDIUM | LOW // status: OPEN | IN_PROGRESS | OVERDUE | CLOSED model CAPA { id String @id @default(cuid()) ref String @unique title String description String? priority String @default("MEDIUM") status String @default("OPEN") progress Int @default(0) ownerId String raisedById String dueDate DateTime closedAt DateTime? rootCause String? corrAction String? prevAction String? department String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt owner User @relation("CAPAOwner", fields: [ownerId], references: [id]) raisedBy User @relation("CAPARaisedBy", fields: [raisedById], references: [id]) timeline CAPAEvent[] ncrs NCR[] escapes QualityEscape[] @@index([status]) @@index([ownerId]) @@index([dueDate]) } model CAPAEvent { id String @id @default(cuid()) capaId String event String note String? createdAt DateTime @default(now()) capa CAPA @relation(fields: [capaId], references: [id], onDelete: Cascade) @@index([capaId]) } // ─── AUDITS ─────────────────────────────────────────────────────────────────── // type: INTERNAL | SUPPLIER | EXTERNAL // status: SCHEDULED | IN_PROGRESS | COMPLETED | CANCELLED model Audit { id String @id @default(cuid()) ref String @unique name String type String @default("INTERNAL") status String @default("SCHEDULED") leadId String scope String? scheduledAt DateTime completedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt lead User @relation("AuditLead", fields: [leadId], references: [id]) findings Finding[] @@index([status]) @@index([scheduledAt]) } // severity: OBSERVATION | MINOR | MAJOR | CRITICAL // status: OPEN | IN_PROGRESS | CLOSED model Finding { id String @id @default(cuid()) auditId String description String severity String @default("MINOR") status String @default("OPEN") category String? dueDate DateTime? closedAt DateTime? createdAt DateTime @default(now()) audit Audit @relation(fields: [auditId], references: [id], onDelete: Cascade) @@index([auditId]) } // ─── NCR ────────────────────────────────────────────────────────────────────── // severity: OBSERVATION | MINOR | MAJOR (null = needs triage) // status: OPEN | INVESTIGATING | ESCALATED | RESOLVED model NCR { id String @id @default(cuid()) ref String @unique description String source String? severity String? status String @default("OPEN") raisedById String resolvedAt DateTime? resolution String? category String? notified Boolean @default(false) notifiedAt DateTime? capaId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt raisedBy User @relation("NCRRaisedBy", fields: [raisedById], references: [id]) capa CAPA? @relation(fields: [capaId], references: [id]) @@index([status]) @@index([createdAt]) } // ─── RESOLUTIONS LIBRARY ────────────────────────────────────────────────────── // Filed whenever an NCR or quality escape is resolved with a category. // Searched for "similar past fix" matching on new issues. model Resolution { id String @id @default(cuid()) title String category String resolution String linkedRef String createdAt DateTime @default(now()) @@index([category]) } // ─── DOCUMENTS ──────────────────────────────────────────────────────────────── // status: CURRENT | PENDING_REVIEW | EXPIRED | ARCHIVED // category: SOP | POLICY | FORM | WORK_INSTRUCTION | RECORD model Document { id String @id @default(cuid()) ref String @unique title String category String @default("SOP") revision String @default("A") status String @default("CURRENT") fileUrl String? ownerId String? reviewDate DateTime? approvedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([status]) @@index([category]) } // ─── RISK REGISTER ──────────────────────────────────────────────────────────── // level: LOW | MEDIUM | HIGH | CRITICAL model Risk { id String @id @default(cuid()) title String description String? likelihood Int @default(1) impact Int @default(1) score Int @default(1) level String @default("LOW") owner String? controls String? reviewDate DateTime? accepted Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([level]) } // ─── SUPPLIERS ──────────────────────────────────────────────────────────────── // status: APPROVED | UNDER_REVIEW | SUSPENDED | INACTIVE // tier: TIER_1 | TIER_2 | TIER_3 model Supplier { id String @id @default(cuid()) name String category String? tier String @default("TIER_2") status String @default("UNDER_REVIEW") score Float? contact String? email String? lastAudit DateTime? nextAudit DateTime? notes String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([status]) } // ─── FIRST BUILD FORMS ──────────────────────────────────────────────────────── // FieldType: SHORT_TEXT | LONG_TEXT | NUMBER | DATE | SINGLE_CHOICE | MULTI_CHOICE | RATING | PHOTO // FormStatus: DRAFT | ACTIVE | SUSPENDED | REVIEW_READY | STANDARD_SET | ARCHIVED model BuildForm { id String @id @default(cuid()) name String product String? description String? status String @default("DRAFT") minSubmissions Int @default(10) createdById String? publishedAt DateTime? suspendedAt DateTime? archivedAt DateTime? clonedFromId String? clonedFromName String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt fields FormField[] submissions FormSubmission[] standard QualityStandard? @@index([status]) } model FormField { id String @id @default(cuid()) formId String label String type String @default("SHORT_TEXT") hint String? options String @default("[]") // JSON-encoded string[] (SQLite has no scalar lists) required Boolean @default(false) trackStd Boolean @default(true) order Int @default(0) form BuildForm @relation(fields: [formId], references: [id], onDelete: Cascade) @@index([formId]) } model FormSubmission { id String @id @default(cuid()) formId String submittedBy String data String // JSON-encoded answers createdAt DateTime @default(now()) form BuildForm @relation(fields: [formId], references: [id]) user User @relation(fields: [submittedBy], references: [id]) @@index([formId]) @@index([createdAt]) } model QualityStandard { id String @id @default(cuid()) formId String @unique title String specs String // JSON-encoded status String @default("DRAFT") approvedBy String? approvedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt form BuildForm @relation(fields: [formId], references: [id]) } // ─── SETTINGS ───────────────────────────────────────────────────────────────── model Setting { key String @id value String updatedAt DateTime @updatedAt } // ─── CLIENT RELEASE / SHIPMENTS ──────────────────────────────────────────────── // "Good status" packages — grouped by product/batch/date — that get // batch-emailed to clients once everything has passed QC. // Send access: PRODUCTION_LEAD, LOGISTICS_LEAD, ADMIN. // ShipmentItem.type: FORM_DATA | NCR_FIX | AUDIT | OTHER model Shipment { id String @id @default(cuid()) ref String @unique product String batch String client String clientEmail String? shippedAt DateTime sentAt DateTime? sentTo String? createdById String? createdAt DateTime @default(now()) items ShipmentItem[] escapes QualityEscape[] @@index([product, batch]) } model ShipmentItem { id String @id @default(cuid()) shipmentId String label String type String @default("OTHER") included Boolean @default(true) order Int @default(0) shipment Shipment @relation(fields: [shipmentId], references: [id], onDelete: Cascade) @@index([shipmentId]) } // ─── CLIENT ISSUES (QUALITY ESCAPES) ────────────────────────────────────────── // A defect that passed QC and reached the client. Reuses the NCR // investigation/resolution flow, but linked to a shipment, and resolving // one can add an entry to the living shipping standard. // severity: OBSERVATION | MINOR | MAJOR (null = needs triage) // status: OPEN | INVESTIGATING | RESOLVED | ESCALATED model QualityEscape { id String @id @default(cuid()) ref String @unique shipmentId String description String contact String? severity String? status String @default("OPEN") resolution String? category String? capaId String? standardItemAdded String? createdAt DateTime @default(now()) resolvedAt DateTime? shipment Shipment @relation(fields: [shipmentId], references: [id]) capa CAPA? @relation(fields: [capaId], references: [id]) @@index([status]) } // ─── LIVING SHIPPING STANDARD ───────────────────────────────────────────────── // What must be true before a product gets "good status". Baseline items // plus items added automatically when a quality escape is resolved. model ShippingStandardItem { id String @id @default(cuid()) text String source String @default("Baseline") order Int @default(0) createdAt DateTime @default(now()) }