This commit is contained in:
jason
2026-04-20 15:49:01 -05:00
parent 381a31d607
commit b98837a72c
46 changed files with 8883 additions and 37 deletions
@@ -0,0 +1,289 @@
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL PRIMARY KEY,
"role" TEXT NOT NULL,
"name" TEXT NOT NULL,
"email" TEXT,
"passwordHash" TEXT,
"pinHash" TEXT,
"active" BOOLEAN NOT NULL DEFAULT true,
"failedAttempts" INTEGER NOT NULL DEFAULT 0,
"lockedUntil" DATETIME,
"lastLoginAt" DATETIME,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"tokenHash" TEXT NOT NULL,
"deviceLabel" TEXT,
"userAgent" TEXT,
"ipAddress" TEXT,
"expiresAt" DATETIME NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastSeenAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Machine" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"kind" TEXT NOT NULL,
"location" TEXT,
"notes" TEXT,
"active" BOOLEAN NOT NULL DEFAULT true,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "OperationTemplate" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"machineId" TEXT,
"defaultSettings" TEXT,
"defaultInstructions" TEXT,
"qcRequired" BOOLEAN NOT NULL DEFAULT false,
"active" BOOLEAN NOT NULL DEFAULT true,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "OperationTemplate_machineId_fkey" FOREIGN KEY ("machineId") REFERENCES "Machine" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Project" (
"id" TEXT NOT NULL PRIMARY KEY,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"customerCode" TEXT,
"dueDate" DATETIME,
"status" TEXT NOT NULL DEFAULT 'planning',
"notes" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "Assembly" (
"id" TEXT NOT NULL PRIMARY KEY,
"projectId" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"qty" INTEGER NOT NULL DEFAULT 1,
"notes" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Assembly_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Part" (
"id" TEXT NOT NULL PRIMARY KEY,
"assemblyId" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"material" TEXT,
"qty" INTEGER NOT NULL DEFAULT 1,
"notes" TEXT,
"stepFileId" TEXT,
"drawingFileId" TEXT,
"cutFileId" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Part_assemblyId_fkey" FOREIGN KEY ("assemblyId") REFERENCES "Assembly" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "Part_stepFileId_fkey" FOREIGN KEY ("stepFileId") REFERENCES "FileAsset" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "Part_drawingFileId_fkey" FOREIGN KEY ("drawingFileId") REFERENCES "FileAsset" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "Part_cutFileId_fkey" FOREIGN KEY ("cutFileId") REFERENCES "FileAsset" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Operation" (
"id" TEXT NOT NULL PRIMARY KEY,
"partId" TEXT NOT NULL,
"sequence" INTEGER NOT NULL,
"templateId" TEXT,
"name" TEXT NOT NULL,
"machineId" TEXT,
"settings" TEXT,
"materialNotes" TEXT,
"instructions" TEXT,
"qcRequired" BOOLEAN NOT NULL DEFAULT false,
"status" TEXT NOT NULL DEFAULT 'pending',
"qrToken" TEXT NOT NULL,
"claimedByUserId" TEXT,
"claimedAt" DATETIME,
"completedAt" DATETIME,
"plannedMinutes" INTEGER,
"plannedUnits" INTEGER,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Operation_partId_fkey" FOREIGN KEY ("partId") REFERENCES "Part" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "Operation_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "OperationTemplate" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "Operation_machineId_fkey" FOREIGN KEY ("machineId") REFERENCES "Machine" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "Operation_claimedByUserId_fkey" FOREIGN KEY ("claimedByUserId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "TimeLog" (
"id" TEXT NOT NULL PRIMARY KEY,
"operationId" TEXT NOT NULL,
"operatorId" TEXT NOT NULL,
"startedAt" DATETIME NOT NULL,
"endedAt" DATETIME,
"unitsProcessed" INTEGER,
"note" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "TimeLog_operationId_fkey" FOREIGN KEY ("operationId") REFERENCES "Operation" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "TimeLog_operatorId_fkey" FOREIGN KEY ("operatorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "QCRecord" (
"id" TEXT NOT NULL PRIMARY KEY,
"operationId" TEXT NOT NULL,
"operatorId" TEXT NOT NULL,
"kind" TEXT NOT NULL,
"measurements" TEXT,
"passed" BOOLEAN NOT NULL,
"notes" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "QCRecord_operationId_fkey" FOREIGN KEY ("operationId") REFERENCES "Operation" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "QCRecord_operatorId_fkey" FOREIGN KEY ("operatorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Fastener" (
"id" TEXT NOT NULL PRIMARY KEY,
"projectId" TEXT NOT NULL,
"partNumber" TEXT NOT NULL,
"description" TEXT NOT NULL,
"qty" INTEGER NOT NULL,
"supplier" TEXT,
"unitCost" REAL,
"notes" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Fastener_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "PurchaseOrder" (
"id" TEXT NOT NULL PRIMARY KEY,
"projectId" TEXT NOT NULL,
"vendor" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'draft',
"sentAt" DATETIME,
"receivedAt" DATETIME,
"notes" TEXT,
"pdfFileId" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "PurchaseOrder_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "PurchaseOrder_pdfFileId_fkey" FOREIGN KEY ("pdfFileId") REFERENCES "FileAsset" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "POLine" (
"id" TEXT NOT NULL PRIMARY KEY,
"poId" TEXT NOT NULL,
"fastenerId" TEXT NOT NULL,
"qty" INTEGER NOT NULL,
"unitCost" REAL,
"receivedQty" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "POLine_poId_fkey" FOREIGN KEY ("poId") REFERENCES "PurchaseOrder" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "POLine_fastenerId_fkey" FOREIGN KEY ("fastenerId") REFERENCES "Fastener" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "FileAsset" (
"id" TEXT NOT NULL PRIMARY KEY,
"kind" TEXT NOT NULL,
"originalName" TEXT NOT NULL,
"path" TEXT NOT NULL,
"sizeBytes" INTEGER NOT NULL,
"mimeType" TEXT,
"sha256" TEXT NOT NULL,
"uploadedBy" TEXT,
"uploadedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateTable
CREATE TABLE "AuditLog" (
"id" TEXT NOT NULL PRIMARY KEY,
"actorId" TEXT,
"action" TEXT NOT NULL,
"entity" TEXT NOT NULL,
"entityId" TEXT,
"before" TEXT,
"after" TEXT,
"ipAddress" TEXT,
"at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AuditLog_actorId_fkey" FOREIGN KEY ("actorId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE INDEX "User_role_active_idx" ON "User"("role", "active");
-- CreateIndex
CREATE UNIQUE INDEX "Session_tokenHash_key" ON "Session"("tokenHash");
-- CreateIndex
CREATE INDEX "Session_userId_idx" ON "Session"("userId");
-- CreateIndex
CREATE INDEX "Session_expiresAt_idx" ON "Session"("expiresAt");
-- CreateIndex
CREATE UNIQUE INDEX "Machine_name_key" ON "Machine"("name");
-- CreateIndex
CREATE UNIQUE INDEX "OperationTemplate_name_key" ON "OperationTemplate"("name");
-- CreateIndex
CREATE UNIQUE INDEX "Project_code_key" ON "Project"("code");
-- CreateIndex
CREATE UNIQUE INDEX "Assembly_projectId_code_key" ON "Assembly"("projectId", "code");
-- CreateIndex
CREATE UNIQUE INDEX "Part_assemblyId_code_key" ON "Part"("assemblyId", "code");
-- CreateIndex
CREATE UNIQUE INDEX "Operation_qrToken_key" ON "Operation"("qrToken");
-- CreateIndex
CREATE INDEX "Operation_status_idx" ON "Operation"("status");
-- CreateIndex
CREATE INDEX "Operation_claimedByUserId_idx" ON "Operation"("claimedByUserId");
-- CreateIndex
CREATE UNIQUE INDEX "Operation_partId_sequence_key" ON "Operation"("partId", "sequence");
-- CreateIndex
CREATE INDEX "TimeLog_operationId_idx" ON "TimeLog"("operationId");
-- CreateIndex
CREATE INDEX "TimeLog_operatorId_idx" ON "TimeLog"("operatorId");
-- CreateIndex
CREATE INDEX "QCRecord_operationId_idx" ON "QCRecord"("operationId");
-- CreateIndex
CREATE INDEX "Fastener_projectId_idx" ON "Fastener"("projectId");
-- CreateIndex
CREATE UNIQUE INDEX "FileAsset_sha256_key" ON "FileAsset"("sha256");
-- CreateIndex
CREATE INDEX "AuditLog_entity_entityId_idx" ON "AuditLog"("entity", "entityId");
-- CreateIndex
CREATE INDEX "AuditLog_at_idx" ON "AuditLog"("at");
+3
View File
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "sqlite"
+308
View File
@@ -0,0 +1,308 @@
// 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])
}