Assemble QMS app + SQLite refactor + Unraid single-container deploy
Build and Push Docker Image / build (push) Successful in 1m12s
Build and Push Docker Image / build (push) Successful in 1m12s
Reconstruct the full app from init-source overlays (base + fix-1..6 + update-1..3, last-wins) at the repo root, complete the missing pieces so it builds and runs, and stage the Unraid deployment. App completion: - types/index.ts: former Prisma enums as string-literal unions + AppUser - pages/_app.tsx + styles/globals.css (mount AppProvider/ToastProvider) - API routes: auth/login, auth/me, users, submissions (+REVIEW_READY notify), forms (list/create), notifications - scripts/create-admin.js: idempotent first-admin bootstrap - 14 unbuilt nav targets stubbed via ComingSoon placeholder SQLite refactor (single-container, no external DB): - schema provider -> sqlite; enums -> String; Json -> String; FormField.options String[] -> JSON-encoded String - lib/forms.ts (de)serialises options at the DB boundary - drop mode:"insensitive" (unsupported on SQLite) - enum imports repointed from @prisma/client to @/types Deploy: - multi-stage Dockerfile (next build -> prod runner), docker-entrypoint.sh (prisma db push -> create-admin -> next start), .dockerignore - docker-compose.yml: br0 10.2.0.x, /mnt/user/appdata/qms -> /data volume - README rewritten for the Unraid/Gitea Actions flow; .env scrubbed of the live Supabase credential; vercel.json removed Verified: next build clean (41 routes); live SQLite round-trip of login/session, form options array, and submission -> REVIEW_READY. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,431 @@
|
||||
// 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())
|
||||
}
|
||||
Reference in New Issue
Block a user