// 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" } datasource db { provider = "postgresql" } // ─── Auth / Users ──────────────────────────────────────────────────────────── model User { id String @id @default(cuid()) name String? email String @unique emailVerified DateTime? image String? password String? role UserRole @default(ARTIST) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt accounts Account[] sessions Session[] jobs Job[] @relation("JobCreator") comments Comment[] @@map("users") } model Account { id String @id @default(cuid()) userId String type String provider String providerAccountId String refresh_token String? access_token String? expires_at Int? token_type String? scope String? id_token String? 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") } enum UserRole { ADMIN PRODUCER SUPERVISOR ARTIST } // ─── Clients ───────────────────────────────────────────────────────────────── model Client { id String @id @default(cuid()) companyName String contactPerson String? email String? phone String? vatNumber String? companyRegNumber String? billingAddress String? notes String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt jobs Job[] invoices Invoice[] @@map("clients") } // ─── Jobs / Quotes ──────────────────────────────────────────────────────────── model Job { id String @id @default(cuid()) projectTitle String projectCode String @unique showId String @default("") jobType JobType @default(STANDARD) clientId String createdById String? issueDate DateTime @default(now()) expiryDate DateTime? currency String @default("ZAR") status JobStatus @default(DRAFT) description String? internalNotes String? markupPercent Decimal @default(0) @db.Decimal(10, 4) taxPercent Decimal @default(0) @db.Decimal(10, 4) discountPercent Decimal @default(0) @db.Decimal(10, 4) productionFeePercent Decimal @default(0) @db.Decimal(10, 4) pullManagementFeePercent Decimal @default(0) @db.Decimal(10, 4) customHeaderHtml String? customFooterHtml String? termsAndConditions String? startDate DateTime? estimatedCompletionDate DateTime? version Int @default(1) parentJobId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt client Client @relation(fields: [clientId], references: [id]) createdBy User? @relation("JobCreator", fields: [createdById], references: [id]) parentJob Job? @relation("JobVersions", fields: [parentJobId], references: [id], onDelete: SetNull) versions Job[] @relation("JobVersions") shots Shot[] assets Asset[] supervisions Supervision[] previses Previs[] comments Comment[] history JobHistory[] paymentSchedule PaymentSchedule? invoices Invoice[] @@map("jobs") } enum JobStatus { DRAFT SENT APPROVED REJECTED ARCHIVED } enum JobType { STANDARD EPISODIC } model JobHistory { id String @id @default(cuid()) jobId String field String oldValue String? newValue String? changedAt DateTime @default(now()) changedBy String? job Job @relation(fields: [jobId], references: [id], onDelete: Cascade) @@map("job_history") } model Comment { id String @id @default(cuid()) jobId String userId String? body String createdAt DateTime @default(now()) job Job @relation(fields: [jobId], references: [id], onDelete: Cascade) user User? @relation(fields: [userId], references: [id]) @@map("comments") } // ─── Rate Cards ─────────────────────────────────────────────────────────────── model RateCard { id String @id @default(cuid()) name String department String? defaultDailyRate Decimal @default(0) @db.Decimal(10, 4) hoursPerDay Decimal @default(8) @db.Decimal(5, 2) currency String @default("ZAR") notes String? isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt lineItems LineItem[] @@map("rate_cards") } // ─── Shot Templates ─────────────────────────────────────────────────────────── model ShotTemplate { id String @id @default(cuid()) name String description String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt lineItemTemplates ShotLineItemTemplate[] @@map("shot_templates") } model ShotLineItemTemplate { id String @id @default(cuid()) templateId String taskName String artistRole String? defaultHours Decimal @default(0) @db.Decimal(10, 4) rateCardId String? notes String? sortOrder Int @default(0) template ShotTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade) @@map("shot_line_item_templates") } // ─── Asset Templates ────────────────────────────────────────────────────────── model AssetTemplate { id String @id @default(cuid()) name String description String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt lineItemTemplates AssetLineItemTemplate[] @@map("asset_templates") } model AssetLineItemTemplate { id String @id @default(cuid()) templateId String taskName String artistRole String? defaultHours Decimal @default(0) @db.Decimal(10, 4) rateCardId String? notes String? sortOrder Int @default(0) template AssetTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade) @@map("asset_line_item_templates") } // ─── Shots ──────────────────────────────────────────────────────────────────── model Shot { id String @id @default(cuid()) jobId String shotCode String shotNumber Int @default(0) scene String @default("") episode String? shotGroup String? description String? durationSeconds Int? complexityMultiplier Decimal @default(1) @db.Decimal(5, 4) notes String? sortOrder Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt job Job @relation(fields: [jobId], references: [id], onDelete: Cascade) lineItems LineItem[] @@map("shots") } // ─── Assets ─────────────────────────────────────────────────────────────────── model Asset { id String @id @default(cuid()) jobId String assetName String assetType String? description String? complexityMultiplier Decimal @default(1) @db.Decimal(5, 4) notes String? sortOrder Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt job Job @relation(fields: [jobId], references: [id], onDelete: Cascade) lineItems LineItem[] @@map("assets") } // ─── Supervision ───────────────────────────────────────────────────────────── model Supervision { id String @id @default(cuid()) jobId String supervisionTitle String shootName String? location String? shootDates String? numShootDays Int @default(0) travelDays Int @default(0) prepDays Int @default(0) wrapDays Int @default(0) description String? complexityMultiplier Decimal @default(1) @db.Decimal(5, 4) notes String? sortOrder Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt job Job @relation(fields: [jobId], references: [id], onDelete: Cascade) lineItems LineItem[] @@map("supervisions") } model SupervisionTemplate { id String @id @default(cuid()) name String description String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt lineItemTemplates SupervisionLineItemTemplate[] @@map("supervision_templates") } model SupervisionLineItemTemplate { id String @id @default(cuid()) templateId String taskName String artistRole String? defaultHours Decimal @default(0) @db.Decimal(10, 4) rateCardId String? notes String? sortOrder Int @default(0) template SupervisionTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade) @@map("supervision_line_item_templates") } // ─── Previs ─────────────────────────────────────────────────────────────────── model Previs { id String @id @default(cuid()) jobId String sequenceName String previsCode String? sequenceType String @default("Previs") description String? duration String? frameRange String? revisionCount Int @default(0) deliveryStage String @default("Blocking") complexityMultiplier Decimal @default(1) @db.Decimal(5, 4) notes String? sortOrder Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt job Job @relation(fields: [jobId], references: [id], onDelete: Cascade) lineItems LineItem[] @@map("previses") } model PrevisTemplate { id String @id @default(cuid()) name String description String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt lineItemTemplates PrevisLineItemTemplate[] @@map("previs_templates") } model PrevisLineItemTemplate { id String @id @default(cuid()) templateId String taskName String artistRole String? defaultHours Decimal @default(0) @db.Decimal(10, 4) rateCardId String? notes String? sortOrder Int @default(0) template PrevisTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade) @@map("previs_line_item_templates") } // ─── Line Items ─────────────────────────────────────────────────────────────── model LineItem { id String @id @default(cuid()) shotId String? assetId String? supervisionId String? previsId String? rateCardId String? taskName String artistRole String? // Inputs quantityHours Decimal @default(0) @db.Decimal(10, 4) dailyRate Decimal @default(0) @db.Decimal(10, 4) hoursPerDay Decimal @default(8) @db.Decimal(5, 2) numArtists Decimal @default(1) @db.Decimal(5, 2) numDays Decimal @default(0) @db.Decimal(10, 4) // Overrides manualHourlyRate Decimal? @db.Decimal(10, 4) manualSubtotal Decimal? @db.Decimal(14, 4) // Adjustments markupPercent Decimal @default(0) @db.Decimal(10, 4) discountPercent Decimal @default(0) @db.Decimal(10, 4) taxPercent Decimal @default(0) @db.Decimal(10, 4) // Stored computed totals (recalculated server-side before PDF) hourlyRate Decimal @default(0) @db.Decimal(10, 4) subtotal Decimal @default(0) @db.Decimal(14, 4) markupAmount Decimal @default(0) @db.Decimal(14, 4) discountAmount Decimal @default(0) @db.Decimal(14, 4) taxAmount Decimal @default(0) @db.Decimal(14, 4) total Decimal @default(0) @db.Decimal(14, 4) notes String? sortOrder Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt shot Shot? @relation(fields: [shotId], references: [id], onDelete: Cascade) asset Asset? @relation(fields: [assetId], references: [id], onDelete: Cascade) supervision Supervision? @relation(fields: [supervisionId], references: [id], onDelete: Cascade) previs Previs? @relation(fields: [previsId], references: [id], onDelete: Cascade) rateCard RateCard? @relation(fields: [rateCardId], references: [id]) @@map("line_items") } // ─── Task Templates ─────────────────────────────────────────────────────────── model TaskTemplate { id String @id @default(cuid()) name String taskName String artistRole String? rateCardId String? quantityHours Decimal @default(0) @db.Decimal(10, 4) dailyRate Decimal @default(0) @db.Decimal(10, 4) hoursPerDay Decimal @default(8) @db.Decimal(5, 2) numArtists Decimal @default(1) @db.Decimal(5, 2) numDays Decimal @default(0) @db.Decimal(10, 4) manualHourlyRate Decimal? @db.Decimal(10, 4) manualSubtotal Decimal? @db.Decimal(14, 4) markupPercent Decimal @default(0) @db.Decimal(10, 4) discountPercent Decimal @default(0) @db.Decimal(10, 4) taxPercent Decimal @default(0) @db.Decimal(10, 4) notes String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("task_templates") } // ─── Studio Settings ────────────────────────────────────────────────────────── model StudioSettings { id String @id @default("default") studioName String @default("My VFX Studio") studioEmail String? studioPhone String? studioAddress String? logoUrl String? vatNumber String? companyRegNumber String? invoiceHeader String? invoiceFooter String? defaultTermsAndConditions String? defaultCurrency String @default("ZAR") defaultMarkupPercent Decimal @default(0) @db.Decimal(10, 4) defaultDiscountPercent Decimal @default(0) @db.Decimal(10, 4) defaultTaxPercent Decimal @default(0) @db.Decimal(10, 4) updatedAt DateTime @updatedAt @@map("studio_settings") } // ─── Payment Schedule ───────────────────────────────────────────────────────── enum ScheduleTemplate { STANDARD_FILM SMALL_COMMERCIAL EPISODIC CUSTOM } enum MilestoneStatus { DRAFT PENDING INVOICED PARTIALLY_PAID PAID OVERDUE CANCELLED } model PaymentSchedule { id String @id @default(cuid()) jobId String @unique template ScheduleTemplate @default(CUSTOM) notes String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt job Job @relation(fields: [jobId], references: [id], onDelete: Cascade) milestones PaymentMilestone[] @@map("payment_schedules") } model PaymentMilestone { id String @id @default(cuid()) scheduleId String title String description String? dueDate DateTime? percentOfTotal Decimal @default(0) @db.Decimal(10, 4) fixedAmount Decimal? @db.Decimal(14, 4) status MilestoneStatus @default(PENDING) paymentReceivedDate DateTime? notes String? sortOrder Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt schedule PaymentSchedule @relation(fields: [scheduleId], references: [id], onDelete: Cascade) invoice Invoice? @@map("payment_milestones") } // ─── Invoices ───────────────────────────────────────────────────────────────── enum InvoiceStatus { DRAFT SENT VIEWED PARTIALLY_PAID PAID OVERDUE CANCELLED } enum PaymentMethod { EFT CASH CREDIT_CARD BANK_TRANSFER CRYPTO OTHER } model Invoice { id String @id @default(cuid()) invoiceNumber String @unique clientId String jobId String milestoneId String? @unique status InvoiceStatus @default(DRAFT) issueDate DateTime @default(now()) dueDate DateTime? paymentTerms String? currency String @default("ZAR") subtotal Decimal @default(0) @db.Decimal(14, 4) taxPercent Decimal @default(0) @db.Decimal(10, 4) taxAmount Decimal @default(0) @db.Decimal(14, 4) totalAmount Decimal @default(0) @db.Decimal(14, 4) amountPaid Decimal @default(0) @db.Decimal(14, 4) amountDue Decimal @default(0) @db.Decimal(14, 4) notes String? internalNotes String? bankingDetails String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt client Client @relation(fields: [clientId], references: [id]) job Job @relation(fields: [jobId], references: [id], onDelete: Cascade) milestone PaymentMilestone? @relation(fields: [milestoneId], references: [id]) payments InvoicePayment[] @@map("invoices") } model InvoicePayment { id String @id @default(cuid()) invoiceId String amount Decimal @db.Decimal(14, 4) paymentDate DateTime paymentMethod PaymentMethod @default(EFT) reference String? notes String? createdAt DateTime @default(now()) invoice Invoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade) @@map("invoice_payments") } // ─── API Keys ───────────────────────────────────────────────────────────────── model ApiKey { id String @id @default(cuid()) name String /// First 16 chars of the key ("xbid_" + 11 random) — safe to display. prefix String @unique /// SHA-256 of the full key. The key itself is never stored. hash String @unique scopes String[] @default(["read", "write"]) lastUsedAt DateTime? expiresAt DateTime? revokedAt DateTime? createdById String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([revokedAt]) @@map("api_keys") }