550 lines
14 KiB
Plaintext
550 lines
14 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
// ─── USERS & AUTH ─────────────────────────────────────────────────────────────
|
|
|
|
enum Role {
|
|
ADMIN
|
|
QC
|
|
PRODUCTION
|
|
PRODUCTION_LEAD
|
|
LOGISTICS_LEAD
|
|
MANAGEMENT
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
email String @unique
|
|
name String
|
|
password String
|
|
role Role @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 Json?
|
|
after Json?
|
|
createdAt DateTime @default(now())
|
|
user User @relation(fields: [userId], references: [id])
|
|
|
|
@@index([entity, entityId])
|
|
@@index([userId])
|
|
@@index([createdAt])
|
|
}
|
|
|
|
// ─── NOTIFICATIONS ────────────────────────────────────────────────────────────
|
|
|
|
enum NotifType {
|
|
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 NotifType
|
|
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 ─────────────────────────────────────────────────────────────────────
|
|
|
|
enum CAPAPriority {
|
|
CRITICAL
|
|
HIGH
|
|
MEDIUM
|
|
LOW
|
|
}
|
|
|
|
enum CAPAStatus {
|
|
OPEN
|
|
IN_PROGRESS
|
|
OVERDUE
|
|
CLOSED
|
|
}
|
|
|
|
model CAPA {
|
|
id String @id @default(cuid())
|
|
ref String @unique
|
|
title String
|
|
description String?
|
|
priority CAPAPriority @default(MEDIUM)
|
|
status CAPAStatus @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 ───────────────────────────────────────────────────────────────────
|
|
|
|
enum AuditType {
|
|
INTERNAL
|
|
SUPPLIER
|
|
EXTERNAL
|
|
}
|
|
|
|
enum AuditStatus {
|
|
SCHEDULED
|
|
IN_PROGRESS
|
|
COMPLETED
|
|
CANCELLED
|
|
}
|
|
|
|
model Audit {
|
|
id String @id @default(cuid())
|
|
ref String @unique
|
|
name String
|
|
type AuditType @default(INTERNAL)
|
|
status AuditStatus @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])
|
|
}
|
|
|
|
enum FindingSeverity {
|
|
OBSERVATION
|
|
MINOR
|
|
MAJOR
|
|
CRITICAL
|
|
}
|
|
|
|
enum FindingStatus {
|
|
OPEN
|
|
IN_PROGRESS
|
|
CLOSED
|
|
}
|
|
|
|
model Finding {
|
|
id String @id @default(cuid())
|
|
auditId String
|
|
description String
|
|
severity FindingSeverity @default(MINOR)
|
|
status FindingStatus @default(OPEN)
|
|
category String?
|
|
dueDate DateTime?
|
|
closedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
audit Audit @relation(fields: [auditId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([auditId])
|
|
}
|
|
|
|
// ─── NCR ──────────────────────────────────────────────────────────────────────
|
|
|
|
enum NCRSeverity {
|
|
OBSERVATION
|
|
MINOR
|
|
MAJOR
|
|
}
|
|
|
|
enum NCRStatus {
|
|
OPEN
|
|
INVESTIGATING
|
|
ESCALATED
|
|
RESOLVED
|
|
}
|
|
|
|
model NCR {
|
|
id String @id @default(cuid())
|
|
ref String @unique
|
|
description String
|
|
source String?
|
|
severity NCRSeverity?
|
|
status NCRStatus @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 ────────────────────────────────────────────────────────────────
|
|
|
|
enum DocStatus {
|
|
CURRENT
|
|
PENDING_REVIEW
|
|
EXPIRED
|
|
ARCHIVED
|
|
}
|
|
|
|
enum DocCategory {
|
|
SOP
|
|
POLICY
|
|
FORM
|
|
WORK_INSTRUCTION
|
|
RECORD
|
|
}
|
|
|
|
model Document {
|
|
id String @id @default(cuid())
|
|
ref String @unique
|
|
title String
|
|
category DocCategory @default(SOP)
|
|
revision String @default("A")
|
|
status DocStatus @default(CURRENT)
|
|
fileUrl String?
|
|
ownerId String?
|
|
reviewDate DateTime?
|
|
approvedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([status])
|
|
@@index([category])
|
|
}
|
|
|
|
// ─── RISK REGISTER ────────────────────────────────────────────────────────────
|
|
|
|
enum RiskLevel {
|
|
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 RiskLevel @default(LOW)
|
|
owner String?
|
|
controls String?
|
|
reviewDate DateTime?
|
|
accepted Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([level])
|
|
}
|
|
|
|
// ─── SUPPLIERS ────────────────────────────────────────────────────────────────
|
|
|
|
enum SupplierStatus {
|
|
APPROVED
|
|
UNDER_REVIEW
|
|
SUSPENDED
|
|
INACTIVE
|
|
}
|
|
|
|
enum SupplierTier {
|
|
TIER_1
|
|
TIER_2
|
|
TIER_3
|
|
}
|
|
|
|
model Supplier {
|
|
id String @id @default(cuid())
|
|
name String
|
|
category String?
|
|
tier SupplierTier @default(TIER_2)
|
|
status SupplierStatus @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 ────────────────────────────────────────────────────────
|
|
|
|
enum FieldType {
|
|
SHORT_TEXT
|
|
LONG_TEXT
|
|
NUMBER
|
|
DATE
|
|
SINGLE_CHOICE
|
|
MULTI_CHOICE
|
|
RATING
|
|
PHOTO
|
|
}
|
|
|
|
enum FormStatus {
|
|
DRAFT
|
|
ACTIVE
|
|
SUSPENDED
|
|
REVIEW_READY
|
|
STANDARD_SET
|
|
ARCHIVED
|
|
}
|
|
|
|
model BuildForm {
|
|
id String @id @default(cuid())
|
|
name String
|
|
product String?
|
|
description String?
|
|
status FormStatus @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 FieldType @default(SHORT_TEXT)
|
|
hint String?
|
|
options String[]
|
|
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 Json
|
|
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 Json
|
|
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.
|
|
|
|
enum ShipmentItemType {
|
|
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 ShipmentItemType @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.
|
|
// Report access: PRODUCTION_LEAD, LOGISTICS_LEAD, ADMIN.
|
|
|
|
enum EscapeStatus {
|
|
OPEN
|
|
INVESTIGATING
|
|
RESOLVED
|
|
ESCALATED
|
|
}
|
|
|
|
model QualityEscape {
|
|
id String @id @default(cuid())
|
|
ref String @unique
|
|
shipmentId String
|
|
description String
|
|
contact String?
|
|
severity NCRSeverity?
|
|
status EscapeStatus @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())
|
|
}
|
|
|