374 lines
14 KiB
TypeScript
374 lines
14 KiB
TypeScript
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();
|
|
});
|