Files
mrp-qrcode/prisma/schema.prisma
T
jason bb452a59ae
Build and Push Docker Image / build (push) Successful in 1m4s
stage 8-complete
2026-04-21 14:21:53 -05:00

312 lines
9.7 KiB
Plaintext

// MRP QR Code system — database schema
// Provider: SQLite (enums stored as string constants; JSON stored as strings)
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
// ---------------------------------------------------------------------------
// Users & sessions
// ---------------------------------------------------------------------------
/// role: "admin" | "operator"
/// admins authenticate with email + password
/// operators authenticate with name + 4-digit PIN
model User {
id String @id @default(cuid())
role String
name String
email String? @unique
passwordHash String?
pinHash String?
active Boolean @default(true)
failedAttempts Int @default(0)
lockedUntil DateTime?
lastLoginAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sessions Session[]
timeLogs TimeLog[]
qcRecords QCRecord[]
auditLogs AuditLog[] @relation("ActorLogs")
claimedOps Operation[] @relation("ClaimedBy")
@@index([role, active])
}
model Session {
id String @id @default(cuid())
userId String
tokenHash String @unique
deviceLabel String?
userAgent String?
ipAddress String?
expiresAt DateTime
createdAt DateTime @default(now())
lastSeenAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([expiresAt])
}
// ---------------------------------------------------------------------------
// Shop floor: machines + operation templates
// ---------------------------------------------------------------------------
model Machine {
id String @id @default(cuid())
name String @unique
kind String // free-form: NCT_PUNCH | PRESS_BRAKE | RIVET | WELD | OTHER
location String?
notes String?
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
templates OperationTemplate[]
operations Operation[]
}
/// Reusable recipe an admin can pick from when authoring an operation on a part.
model OperationTemplate {
id String @id @default(cuid())
name String @unique
machineId String?
defaultSettings String? // JSON-encoded key/value
defaultInstructions String?
qcRequired Boolean @default(false)
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
machine Machine? @relation(fields: [machineId], references: [id], onDelete: SetNull)
operations Operation[]
}
// ---------------------------------------------------------------------------
// Project hierarchy: Project → Assembly → Part → Operation
// ---------------------------------------------------------------------------
model Project {
id String @id @default(cuid())
code String @unique
name String
customerCode String?
dueDate DateTime?
status String @default("planning") // planning | in_progress | completed | cancelled
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
assemblies Assembly[]
fasteners Fastener[]
purchaseOrders PurchaseOrder[]
}
model Assembly {
id String @id @default(cuid())
projectId String
code String
name String
qty Int @default(1)
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
parts Part[]
@@unique([projectId, code])
}
model Part {
id String @id @default(cuid())
assemblyId String
code String
name String
material String?
qty Int @default(1)
notes String?
stepFileId String?
drawingFileId String?
cutFileId String?
thumbnailFileId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
assembly Assembly @relation(fields: [assemblyId], references: [id], onDelete: Cascade)
stepFile FileAsset? @relation("PartStep", fields: [stepFileId], references: [id], onDelete: SetNull)
drawingFile FileAsset? @relation("PartDrawing", fields: [drawingFileId], references: [id], onDelete: SetNull)
cutFile FileAsset? @relation("PartCut", fields: [cutFileId], references: [id], onDelete: SetNull)
thumbnailFile FileAsset? @relation("PartThumbnail", fields: [thumbnailFileId], references: [id], onDelete: SetNull)
operations Operation[]
@@unique([assemblyId, code])
}
/// A single shop-floor step on a specific part. Each has its own QR traveler card.
/// Only one operator may hold the claim at a time (claimedByUserId set).
model Operation {
id String @id @default(cuid())
partId String
sequence Int
templateId String?
name String
machineId String?
settings String? // JSON
materialNotes String?
instructions String?
qcRequired Boolean @default(false)
status String @default("pending") // pending | in_progress | completed
qrToken String @unique
claimedByUserId String?
claimedAt DateTime?
completedAt DateTime?
plannedMinutes Int?
plannedUnits Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
part Part @relation(fields: [partId], references: [id], onDelete: Cascade)
template OperationTemplate? @relation(fields: [templateId], references: [id], onDelete: SetNull)
machine Machine? @relation(fields: [machineId], references: [id], onDelete: SetNull)
claimedBy User? @relation("ClaimedBy", fields: [claimedByUserId], references: [id], onDelete: SetNull)
timeLogs TimeLog[]
qcRecords QCRecord[]
@@unique([partId, sequence])
@@index([status])
@@index([claimedByUserId])
}
model TimeLog {
id String @id @default(cuid())
operationId String
operatorId String
startedAt DateTime
endedAt DateTime?
unitsProcessed Int?
note String?
createdAt DateTime @default(now())
operation Operation @relation(fields: [operationId], references: [id], onDelete: Cascade)
operator User @relation(fields: [operatorId], references: [id])
@@index([operationId])
@@index([operatorId])
}
model QCRecord {
id String @id @default(cuid())
operationId String
operatorId String
kind String // "inline" | "dedicated"
measurements String? // JSON
passed Boolean
notes String?
createdAt DateTime @default(now())
operation Operation @relation(fields: [operationId], references: [id], onDelete: Cascade)
operator User @relation(fields: [operatorId], references: [id])
@@index([operationId])
}
// ---------------------------------------------------------------------------
// Purchasing: fasteners + POs
// ---------------------------------------------------------------------------
model Fastener {
id String @id @default(cuid())
projectId String
partNumber String
description String
qty Int
supplier String?
unitCost Float?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
poLines POLine[]
@@index([projectId])
}
model PurchaseOrder {
id String @id @default(cuid())
projectId String
vendor String
status String @default("draft") // draft | sent | partial | received | cancelled
sentAt DateTime?
receivedAt DateTime?
notes String?
pdfFileId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
pdfFile FileAsset? @relation("PoPdf", fields: [pdfFileId], references: [id], onDelete: SetNull)
lines POLine[]
}
model POLine {
id String @id @default(cuid())
poId String
fastenerId String
qty Int
unitCost Float?
receivedQty Int @default(0)
po PurchaseOrder @relation(fields: [poId], references: [id], onDelete: Cascade)
fastener Fastener @relation(fields: [fastenerId], references: [id])
}
// ---------------------------------------------------------------------------
// Files & audit
// ---------------------------------------------------------------------------
model FileAsset {
id String @id @default(cuid())
kind String // step | pdf | dxf | svg | png | jpg | other
originalName String
path String // relative to UPLOAD_DIR
sizeBytes Int
mimeType String?
sha256 String @unique
uploadedBy String?
uploadedAt DateTime @default(now())
partStep Part[] @relation("PartStep")
partDrawing Part[] @relation("PartDrawing")
partCut Part[] @relation("PartCut")
partThumbnail Part[] @relation("PartThumbnail")
poPdfs PurchaseOrder[] @relation("PoPdf")
}
model AuditLog {
id String @id @default(cuid())
actorId String?
action String // e.g. "create", "update", "delete", "login", "claim_op", "close_op"
entity String
entityId String?
before String? // JSON
after String? // JSON
ipAddress String?
at DateTime @default(now())
actor User? @relation("ActorLogs", fields: [actorId], references: [id], onDelete: SetNull)
@@index([entity, entityId])
@@index([at])
}