309 lines
9.4 KiB
Plaintext
309 lines
9.4 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?
|
|
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)
|
|
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")
|
|
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])
|
|
}
|