// This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { provider = "prisma-client-js" binaryTargets = ["native", "linux-musl-arm64-openssl-3.0.x"] } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ───────────────────────────────────────────── // ENUMS // ───────────────────────────────────────────── enum Role { ADMIN PRODUCER SUPERVISOR ARTIST CLIENT } enum ProjectStatus { ACTIVE ON_HOLD COMPLETED ARCHIVED } enum ShotStatus { WAITING IN_PROGRESS INTERNAL_REVIEW READY_FOR_CLIENT CLIENT_REVIEW REVISIONS COMPLETE } enum ShotApprovalStatus { PENDING INTERNALLY_APPROVED CLIENT_APPROVED } enum ShotPriority { LOW NORMAL HIGH URGENT } enum ApprovalStatus { PENDING_REVIEW APPROVED REJECTED NEEDS_CHANGES } enum ReviewStatus { PENDING INTERNAL_APPROVED CLIENT_APPROVED NEEDS_CHANGES FINAL_APPROVED } enum NotificationType { VERSION_UPLOADED FEEDBACK_ADDED SHOT_APPROVED SHOT_REJECTED COMMENT_REPLY MENTION REVISION_REQUESTED TASK_ASSIGNED TASK_OVERDUE TASK_APPROVED TASK_CHANGES_REQUESTED TASK_READY_FOR_REVIEW } enum TaskStatus { TODO IN_PROGRESS INTERNAL_REVIEW CLIENT_REVIEW CHANGES DONE } enum TaskType { TRACK ROTO KEY COMP FX LIGHTING RENDER ANIMATION MODEL TEXTURE RIG LOOKDEV GENERAL } enum ProjectType { STANDARD EPISODIC } enum TakeQuality { HERO GOOD PRINT NO_GOOD FALSE_START } enum AttachmentFileType { IMAGE VIDEO HDRI LIDAR LENS_GRID PDF OTHER } enum AttachmentCategory { SLATE CAMERA_POSITION WIDE_REFERENCE LENS LIGHTING HDRI TRACKING TEXTURE WITNESS MISCELLANEOUS } enum ExportStatus { QUEUED RENDERING RENDER_FAILED VALIDATING VALIDATION_FAILED GENERATING_PREVIEW PREVIEW_FAILED READY_FOR_QC QC_FAILED READY_FOR_DELIVERY PACKAGED DELIVERED SUPERSEDED ARCHIVED CANCELLED } enum RenderJobStatus { QUEUED CLAIMED RUNNING COMPLETED FAILED CANCELLED EXPIRED } enum RenderJobType { AE_RENDER PREVIEW_ONLY DELIVERY_BUILD } // ───────────────────────────────────────────── // AUTH MODELS (NextAuth v5 compatible) // ───────────────────────────────────────────── model User { id String @id @default(cuid()) name String? email String @unique emailVerified DateTime? image String? passwordHash String? role Role @default(ARTIST) isActive Boolean @default(true) mustChangePassword Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // NextAuth relations accounts Account[] sessions Session[] // App relations producedProjects Project[] @relation("ProducerProjects") supervisedProjects Project[] @relation("SupervisorProjects") assignedShots Shot[] @relation("ArtistShots") uploadedVersions Version[] comments Comment[] commentReplies CommentReply[] annotations Annotation[] approvals Approval[] notifications Notification[] clientAccess ClientAccess[] assignedTasks Task[] @relation("TaskArtist") createdTasks Task[] @relation("TaskCreator") sharedVersions Version[] @relation("VersionSharedBy") ledAssets Asset[] @relation("AssetLead") createdShootDays ShootDay[] @relation("ShootDayCreator") createdTakes Take[] @relation("TakeCreator") uploadedTakeAttachments TakeAttachment[] @relation("TakeAttachmentUploader") @@map("users") } model Account { id String @id @default(cuid()) userId String type String provider String providerAccountId String refresh_token String? @db.Text access_token String? @db.Text expires_at Int? token_type String? scope String? id_token String? @db.Text session_state String? user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, providerAccountId]) @@map("accounts") } model Session { id String @id @default(cuid()) sessionToken String @unique userId String expires DateTime user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("sessions") } model VerificationToken { identifier String token String @unique expires DateTime @@unique([identifier, token]) @@map("verification_tokens") } // ───────────────────────────────────────────── // CORE DOMAIN MODELS // ───────────────────────────────────────────── model Client { id String @id @default(cuid()) company String contactPerson String email String @unique phone String? notes String? @db.Text logoUrl String? isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt projects Project[] clientAccess ClientAccess[] @@map("clients") } /// Links a USER with CLIENT role to the client record they represent model ClientAccess { id String @id @default(cuid()) userId String clientId String createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) client Client @relation(fields: [clientId], references: [id], onDelete: Cascade) @@unique([userId, clientId]) @@map("client_access") } model Project { id String @id @default(cuid()) name String code String @unique showId String @default("") projectType ProjectType @default(STANDARD) description String? @db.Text status ProjectStatus @default(ACTIVE) deliveryConfig Json? dueDate DateTime? startDate DateTime? clientId String? producerId String? supervisorId String? slackWebhook String? slackChannel String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt client Client? @relation(fields: [clientId], references: [id]) producer User? @relation("ProducerProjects", fields: [producerId], references: [id]) supervisor User? @relation("SupervisorProjects", fields: [supervisorId], references: [id]) shots Shot[] assets Asset[] tasks Task[] shotGroups ShotGroup[] reviewSessions ReviewSession[] episodeDueDates EpisodeDueDate[] shootDays ShootDay[] @@map("projects") } model EpisodeDueDate { id String @id @default(cuid()) projectId String episode String dueDate DateTime createdAt DateTime @default(now()) updatedAt DateTime @updatedAt project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) @@unique([projectId, episode]) @@map("episode_due_dates") } model Shot { id String @id @default(cuid()) shotCode String scene String @default("") episode String? shotNumber Int @default(0) sequence String? description String? @db.Text notes String? @db.Text status ShotStatus @default(WAITING) priority ShotPriority @default(NORMAL) artistId String? projectId String frameStart Int? frameEnd Int? fps Float @default(24) dueDate DateTime? thumbnailUrl String? originalFootageUrl String? originalFootageKey String? shotGroupId String? shotApprovalStatus ShotApprovalStatus @default(PENDING) sharedWithClient Boolean @default(false) // EDL / pull CSV fields sourceClip String? timecodeStart String? timecodeEnd String? clipDuration String? exrOutput String? // Sequence / picture lock timecodes seqTimecodeStart String? seqTimecodeEnd String? // High-res deliverable (stored in Hetzner Object Storage) highResKey String? highResFilename String? // Shot-level version tracking (e.g. v001, v002) shotVersion String @default("v001") // Key shot flag — highlighted for prioritisation isKeyShot Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) artist User? @relation("ArtistShots", fields: [artistId], references: [id]) shotGroup ShotGroup? @relation(fields: [shotGroupId], references: [id]) versions Version[] tasks Task[] exports Export[] footagePlates FootagePlate[] references ShotReference[] loggedTakes Take[] @relation("TakeToShot") @@unique([projectId, shotCode]) @@map("shots") } model FootagePlate { id String @id @default(cuid()) shotId String label String @default("") fileUrl String fileKey String @default("") fileName String @default("") fileSize BigInt? sortOrder Int @default(0) createdAt DateTime @default(now()) shot Shot @relation(fields: [shotId], references: [id], onDelete: Cascade) @@map("footage_plates") } model ShotReference { id String @id @default(cuid()) shotId String label String? fileUrl String fileKey String @default("") fileName String @default("") fileSize BigInt? sortOrder Int @default(0) createdAt DateTime @default(now()) shot Shot @relation(fields: [shotId], references: [id], onDelete: Cascade) @@map("shot_references") } model ShotGroup { id String @id @default(cuid()) name String projectId String sortOrder Int @default(0) createdAt DateTime @default(now()) project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) shots Shot[] @@unique([projectId, name]) @@map("shot_groups") } model Export { id String @id @default(cuid()) shotId String shot Shot @relation(fields: [shotId], references: [id], onDelete: Cascade) projectId String taskId String? versionId String? versionNumber Int versionString String status ExportStatus @default(QUEUED) statusChangedAt DateTime @default(now()) aepPath String compName String rendererType String outputDir String outputPattern String frameStart Int frameEnd Int fps Float width Int height Int colorspace String? deliveryMovPath String? previewMovKey String? thumbnailKey String? metadataKey String? exrFileCount Int? exrTotalBytes BigInt? checksum String? submittedById String? submittedByName String? supersededById String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt renderJobs RenderJob[] events ExportEvent[] @@unique([shotId, versionNumber]) @@index([status]) @@index([projectId, status]) @@map("exports") } model RenderJob { id String @id @default(cuid()) type RenderJobType @default(AE_RENDER) exportId String? export Export? @relation(fields: [exportId], references: [id], onDelete: Cascade) deliveryId String? attempt Int @default(1) maxAttempts Int @default(3) status RenderJobStatus @default(QUEUED) priority Int @default(50) manifest Json machineId String? claimedAt DateTime? leaseExpiresAt DateTime? startedAt DateTime? finishedAt DateTime? progress Float @default(0) currentFrame Int? totalFrames Int? etaSeconds Int? exitCode Int? errorMessage String? logTail String? logFileKey String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([status, priority, createdAt]) @@map("render_jobs") } model ExportEvent { id String @id @default(cuid()) exportId String export Export @relation(fields: [exportId], references: [id], onDelete: Cascade) fromStatus String? toStatus String actorType String actorId String? note String? createdAt DateTime @default(now()) @@index([exportId, createdAt]) @@map("export_events") } model Version { id String @id @default(cuid()) versionNumber Int shotId String? taskId String? artistId String? fileUrl String fileName String fileSize BigInt? mimeType String? thumbnailUrl String? posterUrl String? proxyUrl String? fps Float @default(24) duration Float? frameCount Int? width Int? height Int? notes String? @db.Text approvalStatus ApprovalStatus @default(PENDING_REVIEW) reviewStatus ReviewStatus @default(PENDING) isLatest Boolean @default(true) isClientVisible Boolean @default(false) sharedAt DateTime? sharedById String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt shot Shot? @relation(fields: [shotId], references: [id], onDelete: Cascade) task Task? @relation(fields: [taskId], references: [id], onDelete: Cascade) artist User? @relation(fields: [artistId], references: [id]) sharedBy User? @relation("VersionSharedBy", fields: [sharedById], references: [id]) comments Comment[] annotations Annotation[] approvals Approval[] @@map("versions") } model Comment { id String @id @default(cuid()) versionId String authorId String? frameNumber Int timestamp Float text String @db.Text isResolved Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt version Version @relation(fields: [versionId], references: [id], onDelete: Cascade) author User? @relation(fields: [authorId], references: [id], onDelete: SetNull) replies CommentReply[] annotations Annotation[] @@map("comments") } model CommentReply { id String @id @default(cuid()) commentId String authorId String? text String @db.Text createdAt DateTime @default(now()) updatedAt DateTime @updatedAt comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade) author User? @relation(fields: [authorId], references: [id], onDelete: SetNull) @@map("comment_replies") } /// Stores canvas drawing data as JSON (normalized 0-1 coordinates) model Annotation { id String @id @default(cuid()) versionId String commentId String? authorId String? frameNumber Int drawingData Json // Array of AnnotationShape objects color String @default("#ef4444") isVisible Boolean @default(true) createdAt DateTime @default(now()) version Version @relation(fields: [versionId], references: [id], onDelete: Cascade) comment Comment? @relation(fields: [commentId], references: [id], onDelete: SetNull) author User? @relation(fields: [authorId], references: [id], onDelete: SetNull) @@map("annotations") } model Approval { id String @id @default(cuid()) versionId String userId String? status ApprovalStatus notes String? @db.Text createdAt DateTime @default(now()) version Version @relation(fields: [versionId], references: [id], onDelete: Cascade) user User? @relation(fields: [userId], references: [id], onDelete: SetNull) @@map("approvals") } model Notification { id String @id @default(cuid()) userId String type NotificationType title String message String @db.Text data Json? // Extra context (versionId, shotCode, etc.) isRead Boolean @default(false) createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("notifications") } model Asset { id String @id @default(cuid()) projectId String assetCode String name String description String? @db.Text status ShotStatus @default(WAITING) priority ShotPriority @default(NORMAL) leadId String? dueDate DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) lead User? @relation("AssetLead", fields: [leadId], references: [id]) tasks Task[] @@unique([projectId, assetCode]) @@map("assets") } model Task { id String @id @default(cuid()) title String description String? @db.Text type TaskType @default(GENERAL) status TaskStatus @default(TODO) priority ShotPriority @default(NORMAL) dueDate DateTime? estimatedHours Float? sortOrder Int @default(0) shotId String? assetId String? assignedArtistId String? createdById String? projectId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt scheduledStartDate DateTime? scheduledEndDate DateTime? scheduleNotes String? @db.Text shot Shot? @relation(fields: [shotId], references: [id], onDelete: Cascade) asset Asset? @relation(fields: [assetId], references: [id], onDelete: Cascade) assignedArtist User? @relation("TaskArtist", fields: [assignedArtistId], references: [id]) createdBy User? @relation("TaskCreator", fields: [createdById], references: [id], onDelete: SetNull) project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) versions Version[] @@index([scheduledStartDate]) @@index([scheduledEndDate]) @@map("tasks") } /// Secure tokenized review link for clients model ReviewSession { id String @id @default(cuid()) projectId String token String @unique @default(cuid()) label String? email String? passwordHash String? expiresAt DateTime isActive Boolean @default(true) accessCount Int @default(0) allowedEpisodes String[] @default([]) createdAt DateTime @default(now()) project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) @@map("review_sessions") } /// Key-value store for system-wide configuration (e.g. storage credentials) model SystemConfig { key String @id value String @db.Text updatedAt DateTime @updatedAt @@map("system_config") } /// Physical display event log — polled by ESP32 dot-matrix display model DisplayEvent { id Int @id @default(autoincrement()) type String // APPROVED | CHANGES | COMMENT | REJECTED shotCode String by String // actor name (reviewer / commenter) createdAt DateTime @default(now()) @@index([createdAt]) @@map("display_events") } // ───────────────────────────────────────────── // SHOOT LOG MODELS // ───────────────────────────────────────────── model ShootDay { id String @id @default(cuid()) projectId String date DateTime unit String @default("A") label String? notes String? @db.Text createdById String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) createdBy User? @relation("ShootDayCreator", fields: [createdById], references: [id], onDelete: SetNull) setups Setup[] @@map("shoot_days") } model Setup { id String @id @default(cuid()) shootDayId String name String description String? @db.Text sortOrder Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt shootDay ShootDay @relation(fields: [shootDayId], references: [id], onDelete: Cascade) takes Take[] @@map("setups") } model Take { id String @id @default(cuid()) setupId String takeNumber Int // Optional link back to a pipeline shot pipelineShotId String? // General scene String? shotLabel String? unitLabel String? // Camera cameraLetter String? clipName String? roll String? cameraModel String? resolution String? codec String? fps Float? shutter String? iso Int? whiteBalance Int? colourSpace String? // Lens lensSet String? lens String? tStop String? filters String? isAnamorphic Boolean @default(false) // Tracking hasHdri Boolean @default(false) hasChromeBall Boolean @default(false) hasGreyBall Boolean @default(false) hasMacbeth Boolean @default(false) hasCleanPlate Boolean @default(false) hasSurvey Boolean @default(false) hasLidar Boolean @default(false) hasWitnessCamera Boolean @default(false) hasLensGrid Boolean @default(false) hasTexturePhotos Boolean @default(false) hasPhotogrammetry Boolean @default(false) // Environment weather String? sunDirection String? artificialLights String? @db.Text // Supervisor supervisorNotes String? @db.Text continuityNotes String? @db.Text vfxRequirements String? @db.Text quality TakeQuality @default(PRINT) createdById String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt setup Setup @relation(fields: [setupId], references: [id], onDelete: Cascade) pipelineShot Shot? @relation("TakeToShot", fields: [pipelineShotId], references: [id], onDelete: SetNull) createdBy User? @relation("TakeCreator", fields: [createdById], references: [id], onDelete: SetNull) attachments TakeAttachment[] @@unique([setupId, takeNumber]) @@map("takes") } model TakeAttachment { id String @id @default(cuid()) takeId String fileUrl String fileKey String @default("") fileName String fileSize BigInt? fileType AttachmentFileType @default(IMAGE) category AttachmentCategory @default(MISCELLANEOUS) caption String? sortOrder Int @default(0) uploadedById String? createdAt DateTime @default(now()) take Take @relation(fields: [takeId], references: [id], onDelete: Cascade) uploadedBy User? @relation("TakeAttachmentUploader", fields: [uploadedById], references: [id], onDelete: SetNull) @@map("take_attachments") } model SketchTemplate { id String @id @default(cuid()) name String fileUrl String fileKey String @default("") fileName String @default("") fileSize BigInt? sortOrder Int @default(0) createdAt DateTime @default(now()) @@map("sketch_templates") }