// 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? stepFileId String? drawingFileId String? cutFileId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) parts Part[] stepFile FileAsset? @relation("AssemblyStep", fields: [stepFileId], references: [id], onDelete: SetNull) drawingFile FileAsset? @relation("AssemblyDrawing", fields: [drawingFileId], references: [id], onDelete: SetNull) cutFile FileAsset? @relation("AssemblyCut", fields: [cutFileId], references: [id], onDelete: SetNull) @@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? /// "work" (default) = regular production step, "qc" = dedicated inspection /// step whose entire purpose is pass/fail. QC ops always require a QC record /// on close and don't care about machines/units. kind String @default("work") // work | qc settings String? // JSON materialNotes String? instructions String? qcRequired Boolean @default(false) /// pending | in_progress | partial | completed | qc_failed /// qc_failed: operator submitted QC fail on close — step is blocked until /// an admin resets it via /api/v1/operations/:id/qc-reset. status String @default("pending") qrToken String @unique claimedByUserId String? claimedAt DateTime? completedAt DateTime? plannedMinutes Int? plannedUnits Int? /// Cumulative units recorded across every Start→Pause/Done cycle on this op. /// Incremented whenever an operator hands in a non-zero `unitsProcessed`. unitsCompleted Int @default(0) 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") assemblyStep Assembly[] @relation("AssemblyStep") assemblyDrawing Assembly[] @relation("AssemblyDrawing") assemblyCut Assembly[] @relation("AssemblyCut") 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]) }