Files
twotalesanimation 0fbe856dce Initial commit
2026-05-19 22:20:29 +02:00

213 lines
6.5 KiB
TypeScript

"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<typeof createVersionSchema>) {
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" },
});
}