+178
-1
@@ -4,7 +4,8 @@ import { auth } from "@/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { z } from "zod";
|
||||
import { ShotStatus, ShotPriority } from "@prisma/client";
|
||||
import { ShotStatus, ShotPriority, ShotApprovalStatus } from "@prisma/client";
|
||||
import { recalcShotStatus } from "@/lib/shot-status";
|
||||
|
||||
const createShotSchema = z.object({
|
||||
scene: z.string().min(1).max(50).regex(/^[A-Z0-9_]+$/i, "Alphanumeric and underscore only"),
|
||||
@@ -201,6 +202,8 @@ const updateShotSchema = z.object({
|
||||
description: z.string().optional(),
|
||||
status: z.nativeEnum(ShotStatus).optional(),
|
||||
priority: z.nativeEnum(ShotPriority).optional(),
|
||||
shotApprovalStatus: z.nativeEnum(ShotApprovalStatus).optional(),
|
||||
sharedWithClient: z.boolean().optional(),
|
||||
fps: z.number().optional(),
|
||||
frameStart: z.number().int().optional().nullable(),
|
||||
frameEnd: z.number().int().optional().nullable(),
|
||||
@@ -569,3 +572,177 @@ export async function renameFootagePlate(plateId: string, label: string) {
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ── Internal Approval ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Supervisor/Producer/Admin: mark a shot as internally approved.
|
||||
* Sets shotApprovalStatus = INTERNALLY_APPROVED, sharedWithClient = false.
|
||||
* Shot status becomes READY_FOR_CLIENT.
|
||||
*/
|
||||
export async function internallyApproveShot(shotId: 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 shot = await db.shot.findUnique({
|
||||
where: { id: shotId },
|
||||
select: { projectId: true },
|
||||
});
|
||||
if (!shot) throw new Error("Shot not found");
|
||||
|
||||
await db.shot.update({
|
||||
where: { id: shotId },
|
||||
data: {
|
||||
shotApprovalStatus: "INTERNALLY_APPROVED",
|
||||
sharedWithClient: false,
|
||||
status: "READY_FOR_CLIENT",
|
||||
},
|
||||
});
|
||||
|
||||
revalidatePath(`/projects/${shot.projectId}`);
|
||||
revalidatePath(`/projects/${shot.projectId}/shots/${shotId}`);
|
||||
revalidatePath(`/shot-status`);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ── Share / Unshare With Client ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Producer/Supervisor/Admin: share an internally-approved shot with the client.
|
||||
* Sets sharedWithClient = true → status becomes CLIENT_REVIEW.
|
||||
*/
|
||||
export async function shareWithClient(shotId: 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 shot = await db.shot.findUnique({
|
||||
where: { id: shotId },
|
||||
select: { projectId: true, shotApprovalStatus: true },
|
||||
});
|
||||
if (!shot) throw new Error("Shot not found");
|
||||
if (shot.shotApprovalStatus !== "INTERNALLY_APPROVED") {
|
||||
throw new Error("Shot must be internally approved before sharing with client");
|
||||
}
|
||||
|
||||
await db.shot.update({
|
||||
where: { id: shotId },
|
||||
data: { sharedWithClient: true, status: "CLIENT_REVIEW" },
|
||||
});
|
||||
|
||||
revalidatePath(`/projects/${shot.projectId}`);
|
||||
revalidatePath(`/projects/${shot.projectId}/shots/${shotId}`);
|
||||
revalidatePath(`/shot-status`);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Producer/Supervisor/Admin: remove a shot from client review.
|
||||
* Sets sharedWithClient = false → status becomes READY_FOR_CLIENT.
|
||||
*/
|
||||
export async function unshareFromClient(shotId: 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 shot = await db.shot.findUnique({
|
||||
where: { id: shotId },
|
||||
select: { projectId: true },
|
||||
});
|
||||
if (!shot) throw new Error("Shot not found");
|
||||
|
||||
await db.shot.update({
|
||||
where: { id: shotId },
|
||||
data: { sharedWithClient: false, status: "READY_FOR_CLIENT" },
|
||||
});
|
||||
|
||||
revalidatePath(`/projects/${shot.projectId}`);
|
||||
revalidatePath(`/projects/${shot.projectId}/shots/${shotId}`);
|
||||
revalidatePath(`/shot-status`);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ── Shot-Level Client Approval ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Internal: handle client approving the shot.
|
||||
* Sets shotApprovalStatus = CLIENT_APPROVED → status becomes COMPLETE.
|
||||
*/
|
||||
export async function clientApproveShot(shotId: string) {
|
||||
const session = await auth();
|
||||
if (!session?.user) throw new Error("Unauthorized");
|
||||
if (!["ADMIN", "PRODUCER", "SUPERVISOR", "CLIENT"].includes(session.user.role)) {
|
||||
throw new Error("Insufficient permissions");
|
||||
}
|
||||
|
||||
const shot = await db.shot.findUnique({
|
||||
where: { id: shotId },
|
||||
select: { projectId: true, sharedWithClient: true },
|
||||
});
|
||||
if (!shot) throw new Error("Shot not found");
|
||||
if (!shot.sharedWithClient) throw new Error("Shot is not shared with client");
|
||||
|
||||
await db.shot.update({
|
||||
where: { id: shotId },
|
||||
data: { shotApprovalStatus: "CLIENT_APPROVED", status: "COMPLETE" },
|
||||
});
|
||||
|
||||
revalidatePath(`/projects/${shot.projectId}`);
|
||||
revalidatePath(`/projects/${shot.projectId}/shots/${shotId}`);
|
||||
revalidatePath(`/shot-status`);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal: handle client requesting changes on a shot.
|
||||
* Resets shotApprovalStatus = PENDING, sharedWithClient = false.
|
||||
* Recalculates shot status (will become REVISIONS if tasks set to CHANGES).
|
||||
*/
|
||||
export async function clientRequestShotChanges(shotId: string, taskId?: string) {
|
||||
const session = await auth();
|
||||
if (!session?.user) throw new Error("Unauthorized");
|
||||
if (!["ADMIN", "PRODUCER", "SUPERVISOR", "CLIENT"].includes(session.user.role)) {
|
||||
throw new Error("Insufficient permissions");
|
||||
}
|
||||
|
||||
const shot = await db.shot.findUnique({
|
||||
where: { id: shotId },
|
||||
select: { projectId: true },
|
||||
});
|
||||
if (!shot) throw new Error("Shot not found");
|
||||
|
||||
await db.$transaction(async (tx) => {
|
||||
// Reset shot approval
|
||||
await tx.shot.update({
|
||||
where: { id: shotId },
|
||||
data: { shotApprovalStatus: "PENDING", sharedWithClient: false },
|
||||
});
|
||||
|
||||
// If a specific task is called out, mark it CHANGES; otherwise mark all non-DONE tasks
|
||||
if (taskId) {
|
||||
await tx.task.update({
|
||||
where: { id: taskId },
|
||||
data: { status: "CHANGES" },
|
||||
});
|
||||
} else {
|
||||
await tx.task.updateMany({
|
||||
where: { shotId, status: { not: "DONE" } },
|
||||
data: { status: "CHANGES" },
|
||||
});
|
||||
}
|
||||
|
||||
await recalcShotStatus(shotId, tx);
|
||||
});
|
||||
|
||||
revalidatePath(`/projects/${shot.projectId}`);
|
||||
revalidatePath(`/projects/${shot.projectId}/shots/${shotId}`);
|
||||
revalidatePath(`/shot-status`);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user