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
+278
View File
@@ -0,0 +1,278 @@
"use server";
import { auth } from "@/auth";
import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { ShotStatus, ShotPriority } from "@prisma/client";
const createShotSchema = z.object({
scene: z.string().min(1).max(50).regex(/^[A-Z0-9_]+$/i, "Alphanumeric and underscore only"),
episode: z.string().max(50).optional(),
description: z.string().optional(),
projectId: z.string().cuid(),
artistId: z.string().cuid().optional().or(z.literal("")),
priority: z.nativeEnum(ShotPriority).default("NORMAL"),
fps: z.number().default(24),
frameStart: z.number().int().optional(),
frameEnd: z.number().int().optional(),
dueDate: z.string().optional(),
thumbnailUrl: z.string().optional(),
});
export async function createShot(data: z.infer<typeof createShotSchema>) {
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 parsed = createShotSchema.parse(data);
const scene = parsed.scene.toUpperCase();
const episode = parsed.episode?.toUpperCase() ?? null;
// Fetch project for showId and projectType
const project = await db.project.findUnique({
where: { id: parsed.projectId },
select: { showId: true, projectType: true },
});
if (!project) throw new Error("Project not found");
if (!project.showId) {
throw new Error("Project has no Show ID set. Please edit the project to add one.");
}
// For episodic projects, episode is required
if (project.projectType === "EPISODIC" && !episode) {
throw new Error("Episode is required for episodic projects.");
}
// Determine shot number scope: projectId + scene (+ episode for episodic)
const scopeWhere = {
projectId: parsed.projectId,
scene,
...(project.projectType === "EPISODIC" ? { episode } : {}),
};
const maxShot = await db.shot.findFirst({
where: scopeWhere,
orderBy: { shotNumber: "desc" },
select: { shotNumber: true },
});
const shotNumber = (maxShot?.shotNumber ?? 0) + 10;
const paddedNumber = shotNumber.toString().padStart(4, "0");
// Build shot code per naming convention
const shotCode =
project.projectType === "EPISODIC" && episode
? `${project.showId}_${episode}_${scene}_${paddedNumber}`
: `${project.showId}_${scene}_${paddedNumber}`;
const shot = await db.shot.create({
data: {
shotCode,
scene,
episode,
shotNumber,
description: parsed.description,
projectId: parsed.projectId,
artistId: parsed.artistId || undefined,
priority: parsed.priority,
fps: parsed.fps,
frameStart: parsed.frameStart,
frameEnd: parsed.frameEnd,
dueDate: parsed.dueDate ? new Date(parsed.dueDate) : undefined,
thumbnailUrl: parsed.thumbnailUrl,
},
});
revalidatePath(`/projects/${parsed.projectId}`);
return { success: true, shot };
}
// ── Update Shot ───────────────────────────────────────────────────────────────
const updateShotSchema = z.object({
shotId: z.string().cuid(),
description: z.string().optional(),
status: z.nativeEnum(ShotStatus).optional(),
priority: z.nativeEnum(ShotPriority).optional(),
fps: z.number().optional(),
frameStart: z.number().int().optional().nullable(),
frameEnd: z.number().int().optional().nullable(),
dueDate: z.string().optional().nullable(),
artistId: z.string().cuid().optional().nullable().or(z.literal("")),
thumbnailUrl: z.string().optional().nullable(),
});
export async function updateShot(data: z.infer<typeof updateShotSchema>) {
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 parsed = updateShotSchema.parse(data);
const { shotId, dueDate, artistId, ...rest } = parsed;
const shot = await db.shot.update({
where: { id: shotId },
data: {
...rest,
dueDate: dueDate ? new Date(dueDate) : dueDate === null ? null : undefined,
artistId: artistId === "" ? null : artistId,
},
});
revalidatePath(`/projects/${shot.projectId}`);
revalidatePath(`/projects/${shot.projectId}/shots/${shotId}`);
return { success: true, shot };
}
// ── CSV Import ────────────────────────────────────────────────────────────────
export async function importShotsFromCsv(
projectId: string,
rows: Array<{
scene: string;
episode?: string;
description?: string;
priority?: string;
fps?: number;
frameStart?: number;
frameEnd?: number;
}>
) {
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 project = await db.project.findUnique({
where: { id: projectId },
select: { showId: true, projectType: true },
});
if (!project) throw new Error("Project not found");
if (!project.showId) throw new Error("Project has no Show ID. Add one in Project Settings first.");
const VALID_PRIORITIES = ["LOW", "NORMAL", "HIGH", "CRITICAL"];
const created: string[] = [];
const errors: string[] = [];
for (const row of rows) {
try {
const scene = row.scene.trim().toUpperCase();
if (!scene) { errors.push("Empty scene name — skipped"); continue; }
const episode = row.episode?.trim().toUpperCase() || null;
if (project.projectType === "EPISODIC" && !episode) {
errors.push(`${scene}: episode required for episodic project — skipped`);
continue;
}
const rawPriority = row.priority?.trim().toUpperCase();
const priority = VALID_PRIORITIES.includes(rawPriority ?? "")
? (rawPriority as ShotPriority)
: ShotPriority.NORMAL;
const scopeWhere = {
projectId,
scene,
...(project.projectType === "EPISODIC" ? { episode } : {}),
};
const maxShot = await db.shot.findFirst({
where: scopeWhere,
orderBy: { shotNumber: "desc" },
select: { shotNumber: true },
});
const shotNumber = (maxShot?.shotNumber ?? 0) + 10;
const paddedNumber = shotNumber.toString().padStart(4, "0");
const shotCode =
project.projectType === "EPISODIC" && episode
? `${project.showId}_${episode}_${scene}_${paddedNumber}`
: `${project.showId}_${scene}_${paddedNumber}`;
await db.shot.create({
data: {
shotCode,
scene,
episode,
shotNumber,
description: row.description?.trim() || undefined,
projectId,
priority,
fps: row.fps ?? 24,
frameStart: row.frameStart ?? undefined,
frameEnd: row.frameEnd ?? undefined,
},
});
created.push(shotCode);
} catch (e: unknown) {
errors.push(`${row.scene}: ${e instanceof Error ? e.message : "Unknown error"}`);
}
}
revalidatePath(`/projects/${projectId}`);
return { success: true, created, errors };
}
export async function updateShotStatus(
shotId: string,
status: ShotStatus
) {
const session = await auth();
if (!session?.user) throw new Error("Unauthorized");
const shot = await db.shot.update({
where: { id: shotId },
data: { status },
include: { project: true },
});
revalidatePath(`/projects/${shot.projectId}`);
revalidatePath(`/projects/${shot.projectId}/shots/${shotId}`);
return { success: true };
}
export async function getShotById(shotId: string) {
const session = await auth();
if (!session?.user) throw new Error("Unauthorized");
return db.shot.findUnique({
where: { id: shotId },
include: {
project: { include: { client: true } },
artist: { select: { id: true, name: true, email: true, image: true } },
versions: {
include: {
artist: { select: { id: true, name: true, image: true } },
_count: { select: { comments: true } },
},
orderBy: { versionNumber: "desc" },
},
},
});
}
export async function getShotsByProject(projectId: string) {
const session = await auth();
if (!session?.user) throw new Error("Unauthorized");
return db.shot.findMany({
where: { projectId },
include: {
artist: { select: { id: true, name: true, image: true } },
versions: {
where: { isLatest: true },
take: 1,
include: { _count: { select: { comments: true } } },
},
_count: { select: { versions: true } },
},
orderBy: [{ sequence: "asc" }, { shotCode: "asc" }],
});
}