"use server"; import { auth } from "@/auth"; import { db } from "@/lib/db"; import { revalidatePath } from "next/cache"; import { z } from "zod"; import { recalcShotStatus } from "@/lib/shot-status"; import { notifyNewVersionUploaded } from "@/lib/notifications"; import { slackNotifyVersionUploaded } from "@/lib/slack"; const createVersionSchema = z.object({ taskId: z.string().cuid(), fileUrl: z.string().min(1), fileName: z.string(), fileSize: z.number().optional(), mimeType: z.string().optional(), thumbnailUrl: z.string().min(1).optional().or(z.literal("")), fps: z.number().default(24), duration: z.number().optional(), frameCount: z.number().int().optional(), width: z.number().int().optional(), height: z.number().int().optional(), notes: z.string().optional(), }); export async function createVersion(data: z.infer) { const session = await auth(); if (!session?.user) throw new Error("Unauthorized"); const parsed = createVersionSchema.parse(data); // All versions must belong to a task const task = await db.task.findUnique({ where: { id: parsed.taskId }, include: { project: true, shot: { select: { id: true, shotCode: true } } }, }); if (!task) throw new Error("Task not found"); const projectId = task.projectId; const slackWebhook = task.project.slackWebhook ?? undefined; const projectName = task.project.name; const shotCode = task.shot?.shotCode; // Mark all existing versions for this task as not latest await db.version.updateMany({ where: { taskId: parsed.taskId }, data: { isLatest: false } }); // Move task to INTERNAL_REVIEW on upload await db.task.update({ where: { id: parsed.taskId }, data: { status: "INTERNAL_REVIEW" }, }); // Recalculate shot status if (task.shot) { await recalcShotStatus(task.shot.id).catch(() => {}); } // Get the next version number for this task const lastVersion = await db.version.findFirst({ where: { taskId: parsed.taskId }, orderBy: { versionNumber: "desc" }, }); const versionNumber = (lastVersion?.versionNumber ?? 0) + 1; // Create the new version const version = await db.version.create({ data: { versionNumber, taskId: parsed.taskId, artistId: session.user.id, fileUrl: parsed.fileUrl, fileName: parsed.fileName, fileSize: parsed.fileSize ? BigInt(parsed.fileSize) : undefined, mimeType: parsed.mimeType, thumbnailUrl: parsed.thumbnailUrl || undefined, fps: parsed.fps, duration: parsed.duration, frameCount: parsed.frameCount, width: parsed.width, height: parsed.height, notes: parsed.notes, isLatest: true, }, }); // Send notifications const user = await db.user.findUnique({ where: { id: session.user.id } }); const versionLabelStr = `v${String(versionNumber).padStart(3, "0")}`; if (shotCode) { await notifyNewVersionUploaded({ shotCode, versionNumber, projectId, versionId: version.id, artistName: user?.name ?? session.user.email ?? "Artist", }); } // Slack notification if (slackWebhook && shotCode) { const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"; await slackNotifyVersionUploaded(slackWebhook, { shotCode, versionLabel: versionLabelStr, artistName: user?.name ?? "Artist", projectName, reviewUrl: `${appUrl}/review/${version.id}`, }); } revalidatePath(`/projects/${projectId}`); if (task.shot) revalidatePath(`/projects/${projectId}/shots/${task.shot.id}`); revalidatePath(`/tasks/${parsed.taskId}`); return { success: true, version, versionNumber }; } /** * Share a version with the client — marks it as client-visible, * records who shared it, and moves the associated task to CLIENT_REVIEW. */ export async function shareVersionWithClient(versionId: string) { const session = await auth(); if (!session?.user) throw new Error("Unauthorized"); if (!["ADMIN", "PRODUCER", "SUPERVISOR"].includes(session.user.role)) { throw new Error("Insufficient permissions"); } const version = await db.version.findUnique({ where: { id: versionId }, select: { id: true, taskId: true, isClientVisible: true }, }); if (!version) throw new Error("Version not found"); await db.version.update({ where: { id: versionId }, data: { isClientVisible: true, sharedAt: new Date(), sharedById: session.user.id, }, }); // Move task to CLIENT_REVIEW if it is in INTERNAL_REVIEW; recalc shot status if (version.taskId) { const task = await db.task.findUnique({ where: { id: version.taskId }, select: { status: true, projectId: true, shotId: true }, }); if (task && task.status === "INTERNAL_REVIEW") { await db.task.update({ where: { id: version.taskId }, data: { status: "CLIENT_REVIEW" }, }); if (task.shotId) { await recalcShotStatus(task.shotId).catch(() => {}); } revalidatePath(`/tasks/${version.taskId}`); revalidatePath(`/projects/${task.projectId}`); } } revalidatePath(`/review/${versionId}`); return { success: true }; } export async function getVersionById(versionId: string) { const session = await auth(); if (!session?.user) throw new Error("Unauthorized"); return db.version.findUnique({ where: { id: versionId }, include: { task: { include: { project: { include: { client: true } }, shot: true, asset: true, versions: { orderBy: { versionNumber: "desc" }, select: { id: true, versionNumber: true, approvalStatus: true, createdAt: true }, }, }, }, artist: { select: { id: true, name: true, email: true, image: true } }, approvals: { include: { user: { select: { id: true, name: true, role: true } } }, orderBy: { createdAt: "desc" }, }, }, }); } export async function getVersionComments(versionId: string) { return db.comment.findMany({ where: { versionId }, include: { author: { select: { id: true, name: true, email: true, image: true, role: true } }, replies: { include: { author: { select: { id: true, name: true, email: true, image: true, role: true } }, }, orderBy: { createdAt: "asc" }, }, annotations: { select: { id: true, frameNumber: true, drawingData: true, color: true, isVisible: true, authorId: true }, }, }, orderBy: { frameNumber: "asc" }, }); }