Initial commit

This commit is contained in:
twotalesanimation
2026-05-19 22:20:29 +02:00
commit 0fbe856dce
173 changed files with 38316 additions and 0 deletions
@@ -0,0 +1,347 @@
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('ADMIN', 'PRODUCER', 'SUPERVISOR', 'ARTIST', 'CLIENT');
-- CreateEnum
CREATE TYPE "ProjectStatus" AS ENUM ('ACTIVE', 'ON_HOLD', 'COMPLETED', 'ARCHIVED');
-- CreateEnum
CREATE TYPE "ShotStatus" AS ENUM ('WAITING', 'IN_PROGRESS', 'INTERNAL_REVIEW', 'CLIENT_REVIEW', 'REVISIONS', 'APPROVED', 'FINAL');
-- CreateEnum
CREATE TYPE "ShotPriority" AS ENUM ('LOW', 'NORMAL', 'HIGH', 'URGENT');
-- CreateEnum
CREATE TYPE "ApprovalStatus" AS ENUM ('PENDING_REVIEW', 'APPROVED', 'REJECTED', 'NEEDS_CHANGES');
-- CreateEnum
CREATE TYPE "ReviewStatus" AS ENUM ('PENDING', 'INTERNAL_APPROVED', 'CLIENT_APPROVED', 'NEEDS_CHANGES', 'FINAL_APPROVED');
-- CreateEnum
CREATE TYPE "NotificationType" AS ENUM ('VERSION_UPLOADED', 'FEEDBACK_ADDED', 'SHOT_APPROVED', 'SHOT_REJECTED', 'COMMENT_REPLY', 'MENTION', 'REVISION_REQUESTED');
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"name" TEXT,
"email" TEXT NOT NULL,
"emailVerified" TIMESTAMP(3),
"image" TEXT,
"passwordHash" TEXT,
"role" "Role" NOT NULL DEFAULT 'ARTIST',
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "accounts" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
CONSTRAINT "accounts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sessions" (
"id" TEXT NOT NULL,
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
CONSTRAINT "sessions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "verification_tokens" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL
);
-- CreateTable
CREATE TABLE "clients" (
"id" TEXT NOT NULL,
"company" TEXT NOT NULL,
"contactPerson" TEXT NOT NULL,
"email" TEXT NOT NULL,
"phone" TEXT,
"notes" TEXT,
"logoUrl" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "clients_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "client_access" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"clientId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "client_access_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "projects" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"code" TEXT NOT NULL,
"description" TEXT,
"status" "ProjectStatus" NOT NULL DEFAULT 'ACTIVE',
"dueDate" TIMESTAMP(3),
"startDate" TIMESTAMP(3),
"clientId" TEXT NOT NULL,
"producerId" TEXT,
"supervisorId" TEXT,
"slackWebhook" TEXT,
"slackChannel" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "projects_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "shots" (
"id" TEXT NOT NULL,
"shotCode" TEXT NOT NULL,
"sequence" TEXT,
"description" TEXT,
"status" "ShotStatus" NOT NULL DEFAULT 'WAITING',
"priority" "ShotPriority" NOT NULL DEFAULT 'NORMAL',
"artistId" TEXT,
"projectId" TEXT NOT NULL,
"frameStart" INTEGER,
"frameEnd" INTEGER,
"fps" DOUBLE PRECISION NOT NULL DEFAULT 24,
"dueDate" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "shots_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "versions" (
"id" TEXT NOT NULL,
"versionNumber" INTEGER NOT NULL,
"shotId" TEXT NOT NULL,
"artistId" TEXT,
"fileUrl" TEXT NOT NULL,
"fileName" TEXT NOT NULL,
"fileSize" BIGINT,
"mimeType" TEXT,
"thumbnailUrl" TEXT,
"posterUrl" TEXT,
"proxyUrl" TEXT,
"fps" DOUBLE PRECISION NOT NULL DEFAULT 24,
"duration" DOUBLE PRECISION,
"frameCount" INTEGER,
"width" INTEGER,
"height" INTEGER,
"notes" TEXT,
"approvalStatus" "ApprovalStatus" NOT NULL DEFAULT 'PENDING_REVIEW',
"reviewStatus" "ReviewStatus" NOT NULL DEFAULT 'PENDING',
"isLatest" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "versions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "comments" (
"id" TEXT NOT NULL,
"versionId" TEXT NOT NULL,
"authorId" TEXT NOT NULL,
"frameNumber" INTEGER NOT NULL,
"timestamp" DOUBLE PRECISION NOT NULL,
"text" TEXT NOT NULL,
"isResolved" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "comments_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "comment_replies" (
"id" TEXT NOT NULL,
"commentId" TEXT NOT NULL,
"authorId" TEXT NOT NULL,
"text" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "comment_replies_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "annotations" (
"id" TEXT NOT NULL,
"versionId" TEXT NOT NULL,
"commentId" TEXT,
"authorId" TEXT NOT NULL,
"frameNumber" INTEGER NOT NULL,
"drawingData" JSONB NOT NULL,
"color" TEXT NOT NULL DEFAULT '#ef4444',
"isVisible" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "annotations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "approvals" (
"id" TEXT NOT NULL,
"versionId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"status" "ApprovalStatus" NOT NULL,
"notes" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "approvals_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "notifications" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" "NotificationType" NOT NULL,
"title" TEXT NOT NULL,
"message" TEXT NOT NULL,
"data" JSONB,
"isRead" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "notifications_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "review_sessions" (
"id" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"token" TEXT NOT NULL,
"label" TEXT,
"email" TEXT,
"expiresAt" TIMESTAMP(3) NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"accessCount" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "review_sessions_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE UNIQUE INDEX "accounts_provider_providerAccountId_key" ON "accounts"("provider", "providerAccountId");
-- CreateIndex
CREATE UNIQUE INDEX "sessions_sessionToken_key" ON "sessions"("sessionToken");
-- CreateIndex
CREATE UNIQUE INDEX "verification_tokens_token_key" ON "verification_tokens"("token");
-- CreateIndex
CREATE UNIQUE INDEX "verification_tokens_identifier_token_key" ON "verification_tokens"("identifier", "token");
-- CreateIndex
CREATE UNIQUE INDEX "clients_email_key" ON "clients"("email");
-- CreateIndex
CREATE UNIQUE INDEX "client_access_userId_clientId_key" ON "client_access"("userId", "clientId");
-- CreateIndex
CREATE UNIQUE INDEX "projects_code_key" ON "projects"("code");
-- CreateIndex
CREATE UNIQUE INDEX "shots_projectId_shotCode_key" ON "shots"("projectId", "shotCode");
-- CreateIndex
CREATE UNIQUE INDEX "versions_shotId_versionNumber_key" ON "versions"("shotId", "versionNumber");
-- CreateIndex
CREATE UNIQUE INDEX "review_sessions_token_key" ON "review_sessions"("token");
-- AddForeignKey
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "client_access" ADD CONSTRAINT "client_access_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "client_access" ADD CONSTRAINT "client_access_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "clients"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "projects" ADD CONSTRAINT "projects_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "clients"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "projects" ADD CONSTRAINT "projects_producerId_fkey" FOREIGN KEY ("producerId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "projects" ADD CONSTRAINT "projects_supervisorId_fkey" FOREIGN KEY ("supervisorId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "shots" ADD CONSTRAINT "shots_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "shots" ADD CONSTRAINT "shots_artistId_fkey" FOREIGN KEY ("artistId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "versions" ADD CONSTRAINT "versions_shotId_fkey" FOREIGN KEY ("shotId") REFERENCES "shots"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "versions" ADD CONSTRAINT "versions_artistId_fkey" FOREIGN KEY ("artistId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "comments" ADD CONSTRAINT "comments_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "versions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "comments" ADD CONSTRAINT "comments_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "comment_replies" ADD CONSTRAINT "comment_replies_commentId_fkey" FOREIGN KEY ("commentId") REFERENCES "comments"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "comment_replies" ADD CONSTRAINT "comment_replies_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "annotations" ADD CONSTRAINT "annotations_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "versions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "annotations" ADD CONSTRAINT "annotations_commentId_fkey" FOREIGN KEY ("commentId") REFERENCES "comments"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "annotations" ADD CONSTRAINT "annotations_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "approvals" ADD CONSTRAINT "approvals_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "versions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "approvals" ADD CONSTRAINT "approvals_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "notifications" ADD CONSTRAINT "notifications_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "review_sessions" ADD CONSTRAINT "review_sessions_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,107 @@
-- CreateEnum
CREATE TYPE "TaskStatus" AS ENUM ('TODO', 'IN_PROGRESS', 'INTERNAL_REVIEW', 'CLIENT_REVIEW', 'CHANGES', 'DONE');
-- CreateEnum
CREATE TYPE "TaskType" AS ENUM ('TRACK', 'ROTO', 'KEY', 'COMP', 'FX', 'LIGHTING', 'RENDER', 'ANIMATION', 'MODEL', 'TEXTURE', 'RIG', 'LOOKDEV', 'GENERAL');
-- AlterEnum
-- This migration adds more than one value to an enum.
-- With PostgreSQL versions 11 and earlier, this is not possible
-- in a single migration. This can be worked around by creating
-- multiple migrations, each migration adding only one value to
-- the enum.
ALTER TYPE "NotificationType" ADD VALUE 'TASK_ASSIGNED';
ALTER TYPE "NotificationType" ADD VALUE 'TASK_OVERDUE';
ALTER TYPE "NotificationType" ADD VALUE 'TASK_APPROVED';
ALTER TYPE "NotificationType" ADD VALUE 'TASK_CHANGES_REQUESTED';
ALTER TYPE "NotificationType" ADD VALUE 'TASK_READY_FOR_REVIEW';
-- DropForeignKey
ALTER TABLE "projects" DROP CONSTRAINT "projects_clientId_fkey";
-- DropIndex
DROP INDEX "versions_shotId_versionNumber_key";
-- AlterTable
ALTER TABLE "projects" ALTER COLUMN "clientId" DROP NOT NULL;
-- AlterTable
ALTER TABLE "versions" ADD COLUMN "isClientVisible" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "sharedAt" TIMESTAMP(3),
ADD COLUMN "sharedById" TEXT,
ADD COLUMN "taskId" TEXT,
ALTER COLUMN "shotId" DROP NOT NULL;
-- CreateTable
CREATE TABLE "assets" (
"id" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"assetCode" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"status" "ShotStatus" NOT NULL DEFAULT 'WAITING',
"priority" "ShotPriority" NOT NULL DEFAULT 'NORMAL',
"leadId" TEXT,
"dueDate" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "assets_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "tasks" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"type" "TaskType" NOT NULL DEFAULT 'GENERAL',
"status" "TaskStatus" NOT NULL DEFAULT 'TODO',
"priority" "ShotPriority" NOT NULL DEFAULT 'NORMAL',
"dueDate" TIMESTAMP(3),
"estimatedHours" DOUBLE PRECISION,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"shotId" TEXT,
"assetId" TEXT,
"assignedArtistId" TEXT,
"createdById" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "tasks_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "assets_projectId_assetCode_key" ON "assets"("projectId", "assetCode");
-- AddForeignKey
ALTER TABLE "projects" ADD CONSTRAINT "projects_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "clients"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "versions" ADD CONSTRAINT "versions_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "tasks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "versions" ADD CONSTRAINT "versions_sharedById_fkey" FOREIGN KEY ("sharedById") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "assets" ADD CONSTRAINT "assets_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "assets" ADD CONSTRAINT "assets_leadId_fkey" FOREIGN KEY ("leadId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_shotId_fkey" FOREIGN KEY ("shotId") REFERENCES "shots"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_assignedArtistId_fkey" FOREIGN KEY ("assignedArtistId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,29 @@
-- Migration: Task-First Workflow Refactor
-- 1. Delete old shot-based versions (versions linked to shots, not tasks)
DELETE FROM "versions" WHERE "shotId" IS NOT NULL AND "taskId" IS NULL;
-- 2. Drop defaults before type changes (they hold a reference to the enum)
ALTER TABLE "shots" ALTER COLUMN "status" DROP DEFAULT;
ALTER TABLE "assets" ALTER COLUMN "status" DROP DEFAULT;
-- 3. Cast status columns to text so we can rename values
ALTER TABLE "shots" ALTER COLUMN "status" TYPE TEXT;
ALTER TABLE "assets" ALTER COLUMN "status" TYPE TEXT;
-- 4. Rename old enum values in the data
UPDATE "shots" SET "status" = 'IN_REVIEW' WHERE "status" IN ('INTERNAL_REVIEW', 'CLIENT_REVIEW');
UPDATE "shots" SET "status" = 'COMPLETE' WHERE "status" IN ('APPROVED', 'FINAL');
UPDATE "assets" SET "status" = 'IN_REVIEW' WHERE "status" IN ('INTERNAL_REVIEW', 'CLIENT_REVIEW');
UPDATE "assets" SET "status" = 'COMPLETE' WHERE "status" IN ('APPROVED', 'FINAL');
-- 5. Drop old enum type (no dependents remain)
DROP TYPE "ShotStatus";
-- 6. Create new enum type with 5 values
CREATE TYPE "ShotStatus" AS ENUM ('WAITING', 'IN_PROGRESS', 'IN_REVIEW', 'REVISIONS', 'COMPLETE');
-- 7. Convert columns back to the new enum and restore defaults
ALTER TABLE "shots" ALTER COLUMN "status" TYPE "ShotStatus" USING "status"::"ShotStatus";
ALTER TABLE "shots" ALTER COLUMN "status" SET DEFAULT 'WAITING';
ALTER TABLE "assets" ALTER COLUMN "status" TYPE "ShotStatus" USING "status"::"ShotStatus";
ALTER TABLE "assets" ALTER COLUMN "status" SET DEFAULT 'WAITING';
@@ -0,0 +1,11 @@
-- CreateEnum
CREATE TYPE "ProjectType" AS ENUM ('STANDARD', 'EPISODIC');
-- AlterTable projects: add showId and projectType
ALTER TABLE "projects" ADD COLUMN "showId" TEXT NOT NULL DEFAULT '';
ALTER TABLE "projects" ADD COLUMN "projectType" "ProjectType" NOT NULL DEFAULT 'STANDARD';
-- AlterTable shots: add scene, episode, shotNumber
ALTER TABLE "shots" ADD COLUMN "scene" TEXT NOT NULL DEFAULT '';
ALTER TABLE "shots" ADD COLUMN "episode" TEXT;
ALTER TABLE "shots" ADD COLUMN "shotNumber" INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "shots" ADD COLUMN "thumbnailUrl" TEXT;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "mustChangePassword" BOOLEAN NOT NULL DEFAULT false;
@@ -0,0 +1,44 @@
-- DropForeignKey
ALTER TABLE "annotations" DROP CONSTRAINT "annotations_authorId_fkey";
-- DropForeignKey
ALTER TABLE "approvals" DROP CONSTRAINT "approvals_userId_fkey";
-- DropForeignKey
ALTER TABLE "comment_replies" DROP CONSTRAINT "comment_replies_authorId_fkey";
-- DropForeignKey
ALTER TABLE "comments" DROP CONSTRAINT "comments_authorId_fkey";
-- DropForeignKey
ALTER TABLE "tasks" DROP CONSTRAINT "tasks_createdById_fkey";
-- AlterTable
ALTER TABLE "annotations" ALTER COLUMN "authorId" DROP NOT NULL;
-- AlterTable
ALTER TABLE "approvals" ALTER COLUMN "userId" DROP NOT NULL;
-- AlterTable
ALTER TABLE "comment_replies" ALTER COLUMN "authorId" DROP NOT NULL;
-- AlterTable
ALTER TABLE "comments" ALTER COLUMN "authorId" DROP NOT NULL;
-- AlterTable
ALTER TABLE "tasks" ALTER COLUMN "createdById" DROP NOT NULL;
-- AddForeignKey
ALTER TABLE "comments" ADD CONSTRAINT "comments_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "comment_replies" ADD CONSTRAINT "comment_replies_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "annotations" ADD CONSTRAINT "annotations_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "approvals" ADD CONSTRAINT "approvals_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,10 @@
-- AlterTable
ALTER TABLE "tasks" ADD COLUMN "scheduleNotes" TEXT,
ADD COLUMN "scheduledEndDate" TIMESTAMP(3),
ADD COLUMN "scheduledStartDate" TIMESTAMP(3);
-- CreateIndex
CREATE INDEX "tasks_scheduledStartDate_idx" ON "tasks"("scheduledStartDate");
-- CreateIndex
CREATE INDEX "tasks_scheduledEndDate_idx" ON "tasks"("scheduledEndDate");
+3
View File
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
+472
View File
@@ -0,0 +1,472 @@
// 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
IN_REVIEW
REVISIONS
COMPLETE
}
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
}
// ─────────────────────────────────────────────
// 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")
@@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)
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[]
reviewSessions ReviewSession[]
@@map("projects")
}
model Shot {
id String @id @default(cuid())
shotCode String
scene String @default("")
episode String?
shotNumber Int @default(0)
sequence String?
description 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?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
artist User? @relation("ArtistShots", fields: [artistId], references: [id])
versions Version[]
tasks Task[]
@@unique([projectId, shotCode])
@@map("shots")
}
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?
expiresAt DateTime
isActive Boolean @default(true)
accessCount Int @default(0)
createdAt DateTime @default(now())
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@map("review_sessions")
}
+373
View File
@@ -0,0 +1,373 @@
import {
PrismaClient,
Role,
ProjectStatus,
ShotStatus,
ShotPriority,
ApprovalStatus,
TaskStatus,
TaskType,
} from "@prisma/client";
import bcrypt from "bcryptjs";
const db = new PrismaClient();
async function main() {
console.log("🌱 Seeding VFX Review database...");
// ── Admin user ──────────────────────────────────────
const adminPassword = await bcrypt.hash("admin123", 12);
const admin = await db.user.upsert({
where: { email: "admin@vfxreview.local" },
update: {},
create: {
name: "Admin User",
email: "admin@vfxreview.local",
passwordHash: adminPassword,
role: Role.ADMIN,
},
});
// ── Producer ─────────────────────────────────────────
const producerPassword = await bcrypt.hash("producer123", 12);
const producer = await db.user.upsert({
where: { email: "producer@vfxreview.local" },
update: {},
create: {
name: "Sarah Chen",
email: "producer@vfxreview.local",
passwordHash: producerPassword,
role: Role.PRODUCER,
},
});
// ── Supervisor ───────────────────────────────────────
const supervisorPassword = await bcrypt.hash("supervisor123", 12);
const supervisor = await db.user.upsert({
where: { email: "supervisor@vfxreview.local" },
update: {},
create: {
name: "James Park",
email: "supervisor@vfxreview.local",
passwordHash: supervisorPassword,
role: Role.SUPERVISOR,
},
});
// ── Artist ──────────────────────────────────────────
const artistPassword = await bcrypt.hash("artist123", 12);
const artist = await db.user.upsert({
where: { email: "artist@vfxreview.local" },
update: {},
create: {
name: "Maya Torres",
email: "artist@vfxreview.local",
passwordHash: artistPassword,
role: Role.ARTIST,
},
});
// ── Client user ──────────────────────────────────────
const clientPassword = await bcrypt.hash("client123", 12);
const clientUser = await db.user.upsert({
where: { email: "client@studio.com" },
update: {},
create: {
name: "Alex Morgan",
email: "client@studio.com",
passwordHash: clientPassword,
role: Role.CLIENT,
},
});
// ── Client record ────────────────────────────────────
const client = await db.client.upsert({
where: { email: "client@studio.com" },
update: {},
create: {
company: "Stellar Productions",
contactPerson: "Alex Morgan",
email: "client@studio.com",
phone: "+1 (555) 000-0001",
notes: "Demo client for seed data.",
},
});
// Link client user to client record
await db.clientAccess.upsert({
where: { userId_clientId: { userId: clientUser.id, clientId: client.id } },
update: {},
create: { userId: clientUser.id, clientId: client.id },
});
// ── Project ──────────────────────────────────────────
const project = await db.project.upsert({
where: { code: "NOVA-2025" },
update: {},
create: {
name: "Project Nova",
code: "NOVA-2025",
description: "Feature film visual effects - Act 2",
status: ProjectStatus.ACTIVE,
clientId: client.id,
producerId: producer.id,
supervisorId: supervisor.id,
dueDate: new Date("2025-09-01"),
startDate: new Date("2025-01-15"),
},
});
// ── Shots ────────────────────────────────────────────
const shotData = [
{
shotCode: "SH010",
sequence: "010",
description: "Wide establishing shot of the space station exterior.",
status: ShotStatus.IN_REVIEW,
priority: ShotPriority.HIGH,
},
{
shotCode: "SH020",
sequence: "010",
description: "Close-up of astronaut helmet reflection.",
status: ShotStatus.IN_REVIEW,
priority: ShotPriority.HIGH,
},
{
shotCode: "SH035",
sequence: "020",
description: "Explosion with debris field.",
status: ShotStatus.REVISIONS,
priority: ShotPriority.URGENT,
},
{
shotCode: "SH050",
sequence: "020",
description: "Hero flying through asteroid belt.",
status: ShotStatus.IN_PROGRESS,
priority: ShotPriority.NORMAL,
},
{
shotCode: "SH060",
sequence: "030",
description: "Earth from orbit, cloud layer simulation.",
status: ShotStatus.COMPLETE,
priority: ShotPriority.NORMAL,
},
{
shotCode: "SH070",
sequence: "030",
description: "Warp jump effect - tunnel of light.",
status: ShotStatus.WAITING,
priority: ShotPriority.LOW,
},
];
for (const shot of shotData) {
await db.shot.upsert({
where: { projectId_shotCode: { projectId: project.id, shotCode: shot.shotCode } },
update: {},
create: {
...shot,
projectId: project.id,
artistId: artist.id,
fps: 24,
frameStart: 1001,
frameEnd: 1080,
},
});
}
// ── Tasks ─────────────────────────────────────────────
// Helper: find-or-create task by title + projectId
async function upsertTask(data: {
title: string;
type: TaskType;
status: TaskStatus;
priority: ShotPriority;
sortOrder: number;
projectId: string;
shotId?: string;
assetId?: string;
assignedArtistId?: string;
dueDate?: Date;
estimatedHours?: number;
}) {
const existing = await db.task.findFirst({
where: { projectId: data.projectId, title: data.title, shotId: data.shotId ?? null, assetId: data.assetId ?? null },
});
if (existing) return existing;
return db.task.create({
data: { ...data, createdById: producer.id },
});
}
const shots = await db.shot.findMany({ where: { projectId: project.id } });
const firstShot = shots.find((s) => s.shotCode === "SH010")!;
const sh020 = shots.find((s) => s.shotCode === "SH020")!;
const sh035 = shots.find((s) => s.shotCode === "SH035")!;
// ── Tasks must be created first (versions belong to tasks) ──
// Create tasks early so we can attach versions to them
const sh010CompTask = await upsertTask({ title: "SH010 Comp", type: TaskType.COMP, status: TaskStatus.CLIENT_REVIEW, priority: ShotPriority.HIGH, sortOrder: 2, projectId: project.id, shotId: firstShot.id, assignedArtistId: artist.id, dueDate: new Date("2025-06-15"), estimatedHours: 24 });
// ── Versions now belong to tasks, not shots ──
const existingV1 = await db.version.findFirst({
where: { taskId: sh010CompTask.id, versionNumber: 1 },
});
if (!existingV1) {
await db.version.create({
data: {
versionNumber: 1,
taskId: sh010CompTask.id,
artistId: artist.id,
fileUrl: "/placeholder/sh010_comp_v001.mp4",
fileName: "sh010_comp_v001.mp4",
fps: 24,
duration: 3.375,
frameCount: 81,
width: 1920,
height: 1080,
notes: "Initial blocking pass. Camera move rough.",
approvalStatus: ApprovalStatus.NEEDS_CHANGES,
isLatest: false,
},
});
}
let latestVersion = await db.version.findFirst({
where: { taskId: sh010CompTask.id, versionNumber: 2 },
});
if (!latestVersion) {
latestVersion = await db.version.create({
data: {
versionNumber: 2,
taskId: sh010CompTask.id,
artistId: artist.id,
fileUrl: "/placeholder/sh010_comp_v002.mp4",
fileName: "sh010_comp_v002.mp4",
fps: 24,
duration: 3.375,
frameCount: 81,
width: 1920,
height: 1080,
notes: "Revised camera move. Fixed comp integration. Added lens flares.",
approvalStatus: ApprovalStatus.PENDING_REVIEW,
isClientVisible: true,
isLatest: true,
},
});
}
// ── Sample Comments ──────────────────────────────────
const c1 = await db.comment.create({
data: {
versionId: latestVersion.id,
authorId: supervisor.id,
frameNumber: 22,
timestamp: 22 / 24,
text: "The tracking slips on frame 22 — the station feels like it's drifting slightly to the left. Can you lock this down?",
},
});
await db.commentReply.create({
data: {
commentId: c1.id,
authorId: artist.id,
text: "Got it — I'll re-track from scratch using the new markers. Should have a fix by EOD.",
},
});
await db.comment.create({
data: {
versionId: latestVersion.id,
authorId: clientUser.id,
frameNumber: 55,
timestamp: 55 / 24,
text: "Love the lens flare here! Can we make it 20% brighter and hold it two frames longer?",
},
});
await db.comment.create({
data: {
versionId: latestVersion.id,
authorId: supervisor.id,
frameNumber: 70,
timestamp: 70 / 24,
text: "Edge matte at the bottom — there's a clean line around the station hull. Needs feathering.",
isResolved: true,
},
});
// ── Assets ────────────────────────────────────────────
const assetSpaceship = await db.asset.upsert({
where: { projectId_assetCode: { projectId: project.id, assetCode: "SHIP-01" } },
update: {},
create: {
projectId: project.id,
assetCode: "SHIP-01",
name: "Hero Spaceship",
description: "Main hero vehicle — used in SH010, SH050.",
status: ShotStatus.IN_PROGRESS,
priority: ShotPriority.HIGH,
leadId: supervisor.id,
dueDate: new Date("2025-07-01"),
},
});
const assetStation = await db.asset.upsert({
where: { projectId_assetCode: { projectId: project.id, assetCode: "STATION-01" } },
update: {},
create: {
projectId: project.id,
assetCode: "STATION-01",
name: "Space Station",
description: "BG environment asset for SH010.",
status: ShotStatus.COMPLETE,
priority: ShotPriority.NORMAL,
leadId: supervisor.id,
dueDate: new Date("2025-06-01"),
},
});
// ── Shot & Asset Tasks ────────────────────────────────
// SH010 tasks (SH010 Comp was already created above for version attachment)
await upsertTask({ title: "SH010 Track", type: TaskType.TRACK, status: TaskStatus.DONE, priority: ShotPriority.HIGH, sortOrder: 0, projectId: project.id, shotId: firstShot.id, assignedArtistId: artist.id, dueDate: new Date("2025-04-01"), estimatedHours: 8 });
await upsertTask({ title: "SH010 Roto", type: TaskType.ROTO, status: TaskStatus.IN_PROGRESS, priority: ShotPriority.HIGH, sortOrder: 1, projectId: project.id, shotId: firstShot.id, assignedArtistId: artist.id, dueDate: new Date("2025-05-20"), estimatedHours: 16 });
// SH010 Comp is sh010CompTask (created earlier)
// SH020 tasks
await upsertTask({ title: "SH020 Track", type: TaskType.TRACK, status: TaskStatus.DONE, priority: ShotPriority.HIGH, sortOrder: 0, projectId: project.id, shotId: sh020.id, assignedArtistId: artist.id, dueDate: new Date("2025-04-15"), estimatedHours: 4 });
await upsertTask({ title: "SH020 Comp", type: TaskType.COMP, status: TaskStatus.INTERNAL_REVIEW, priority: ShotPriority.HIGH, sortOrder: 1, projectId: project.id, shotId: sh020.id, assignedArtistId: artist.id, dueDate: new Date("2025-05-25"), estimatedHours: 20 });
// SH035 tasks
await upsertTask({ title: "SH035 FX Explosion", type: TaskType.FX, status: TaskStatus.CHANGES, priority: ShotPriority.URGENT, sortOrder: 0, projectId: project.id, shotId: sh035.id, assignedArtistId: artist.id, dueDate: new Date("2025-05-10"), estimatedHours: 32 });
await upsertTask({ title: "SH035 Comp", type: TaskType.COMP, status: TaskStatus.TODO, priority: ShotPriority.URGENT, sortOrder: 1, projectId: project.id, shotId: sh035.id, dueDate: new Date("2025-06-01"), estimatedHours: 16 });
// SHIP-01 asset tasks
await upsertTask({ title: "SHIP-01 Model", type: TaskType.MODEL, status: TaskStatus.DONE, priority: ShotPriority.HIGH, sortOrder: 0, projectId: project.id, assetId: assetSpaceship.id, assignedArtistId: artist.id, dueDate: new Date("2025-03-01"), estimatedHours: 40 });
await upsertTask({ title: "SHIP-01 Texture", type: TaskType.TEXTURE, status: TaskStatus.IN_PROGRESS, priority: ShotPriority.HIGH, sortOrder: 1, projectId: project.id, assetId: assetSpaceship.id, assignedArtistId: artist.id, dueDate: new Date("2025-06-01"), estimatedHours: 24 });
await upsertTask({ title: "SHIP-01 Rig", type: TaskType.RIG, status: TaskStatus.TODO, priority: ShotPriority.NORMAL, sortOrder: 2, projectId: project.id, assetId: assetSpaceship.id, dueDate: new Date("2025-07-01"), estimatedHours: 20 });
await upsertTask({ title: "SHIP-01 Lookdev", type: TaskType.LOOKDEV, status: TaskStatus.TODO, priority: ShotPriority.NORMAL, sortOrder: 3, projectId: project.id, assetId: assetSpaceship.id, dueDate: new Date("2025-07-15"), estimatedHours: 16 });
// STATION-01 asset tasks
await upsertTask({ title: "STATION-01 Model", type: TaskType.MODEL, status: TaskStatus.DONE, priority: ShotPriority.NORMAL, sortOrder: 0, projectId: project.id, assetId: assetStation.id, assignedArtistId: artist.id, dueDate: new Date("2025-02-15"), estimatedHours: 60 });
await upsertTask({ title: "STATION-01 Lookdev", type: TaskType.LOOKDEV, status: TaskStatus.DONE, priority: ShotPriority.NORMAL, sortOrder: 1, projectId: project.id, assetId: assetStation.id, assignedArtistId: artist.id, dueDate: new Date("2025-03-15"), estimatedHours: 20 });
console.log("✅ Seed complete!");
console.log("\n📋 Demo credentials:");
console.log(" Admin: admin@vfxreview.local / admin123");
console.log(" Producer: producer@vfxreview.local / producer123");
console.log(" Supervisor: supervisor@vfxreview.local / supervisor123");
console.log(" Artist: artist@vfxreview.local / artist123");
console.log(" Client: client@studio.com / client123");
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await db.$disconnect();
});