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
+394
View File
@@ -0,0 +1,394 @@
"use server";
import { auth } from "@/auth";
import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { TaskStatus, TaskType, ShotPriority } from "@prisma/client";
import { recalcShotStatus } from "@/lib/shot-status";
import {
notifyTaskAssigned,
notifyTaskReadyForReview,
notifyTaskApproved,
notifyTaskChangesRequested,
} from "@/lib/notifications";
import {
slackNotifyTaskAssigned,
slackNotifyTaskReadyForReview,
} from "@/lib/slack";
const createTaskSchema = z.object({
title: z.string().min(1, "Title is required").max(200),
description: z.string().optional(),
type: z.nativeEnum(TaskType).default("GENERAL"),
priority: z.nativeEnum(ShotPriority).default("NORMAL"),
dueDate: z.string().optional(),
estimatedHours: z.coerce.number().positive().optional().or(z.literal("")),
shotId: z.string().cuid().optional().or(z.literal("")),
assetId: z.string().cuid().optional().or(z.literal("")),
assignedArtistId: z.string().cuid().optional().or(z.literal("")),
projectId: z.string().cuid(),
});
export async function createTask(data: z.infer<typeof createTaskSchema>) {
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 = createTaskSchema.parse(data);
// Get sort order (append at end)
const lastTask = await db.task.findFirst({
where: {
shotId: parsed.shotId || undefined,
assetId: parsed.assetId || undefined,
},
orderBy: { sortOrder: "desc" },
select: { sortOrder: true },
});
const task = await db.task.create({
data: {
title: parsed.title,
description: parsed.description,
type: parsed.type,
priority: parsed.priority,
dueDate: parsed.dueDate ? new Date(parsed.dueDate) : undefined,
estimatedHours: parsed.estimatedHours ? Number(parsed.estimatedHours) : undefined,
sortOrder: (lastTask?.sortOrder ?? -1) + 1,
shotId: parsed.shotId || undefined,
assetId: parsed.assetId || undefined,
assignedArtistId: parsed.assignedArtistId || undefined,
createdById: session.user.id,
projectId: parsed.projectId,
},
});
// Recalculate shot status when a new task is added
if (parsed.shotId) {
await recalcShotStatus(parsed.shotId).catch(() => {});
}
// Notifications: task assigned
if (parsed.assignedArtistId) {
const project = await db.project.findUnique({
where: { id: parsed.projectId },
select: { name: true, code: true, slackWebhook: true },
});
const contextShot = parsed.shotId
? await db.shot.findUnique({ where: { id: parsed.shotId }, select: { shotCode: true } })
: null;
const contextAsset = parsed.assetId
? await db.asset.findUnique({ where: { id: parsed.assetId }, select: { assetCode: true } })
: null;
const contextCode = contextShot?.shotCode ?? contextAsset?.assetCode ?? null;
const taskUrl = `${process.env.NEXTAUTH_URL ?? ""}/tasks/${task.id}`;
const assignedByName = session.user.name ?? session.user.email ?? "Someone";
const artist = await db.user.findUnique({
where: { id: parsed.assignedArtistId },
select: { name: true, email: true },
});
await notifyTaskAssigned({
artistId: parsed.assignedArtistId,
taskTitle: task.title,
taskId: task.id,
contextCode,
assignedByName,
}).catch(() => {});
if (project?.slackWebhook) {
await slackNotifyTaskAssigned(project.slackWebhook, {
taskTitle: task.title,
contextCode,
artistName: artist?.name ?? artist?.email ?? "artist",
assignedByName,
projectName: project.name,
taskUrl,
}).catch(() => {});
}
}
revalidatePath(`/projects/${parsed.projectId}`);
if (parsed.shotId) revalidatePath(`/projects/${parsed.projectId}/shots/${parsed.shotId}`);
return { success: true, task };
}
const updateTaskSchema = z.object({
title: z.string().min(1).max(200).optional(),
description: z.string().optional(),
type: z.nativeEnum(TaskType).optional(),
status: z.nativeEnum(TaskStatus).optional(),
priority: z.nativeEnum(ShotPriority).optional(),
dueDate: z.string().nullable().optional(),
estimatedHours: z.coerce.number().positive().nullable().optional(),
assignedArtistId: z.string().cuid().nullable().optional(),
sortOrder: z.number().int().optional(),
});
export async function updateTask(taskId: string, data: z.infer<typeof updateTaskSchema>) {
const session = await auth();
if (!session?.user) throw new Error("Unauthorized");
const parsed = updateTaskSchema.parse(data);
const task = await db.task.findUnique({
where: { id: taskId },
include: {
shot: { select: { shotCode: true } },
asset: { select: { assetCode: true } },
project: { select: { name: true, slackWebhook: true } },
},
});
if (!task) throw new Error("Task not found");
const updated = await db.task.update({
where: { id: taskId },
data: {
...(parsed.title !== undefined && { title: parsed.title }),
...(parsed.description !== undefined && { description: parsed.description }),
...(parsed.type !== undefined && { type: parsed.type }),
...(parsed.status !== undefined && { status: parsed.status }),
...(parsed.priority !== undefined && { priority: parsed.priority }),
...(parsed.dueDate !== undefined && {
dueDate: parsed.dueDate ? new Date(parsed.dueDate) : null,
}),
...(parsed.estimatedHours !== undefined && { estimatedHours: parsed.estimatedHours }),
...(parsed.assignedArtistId !== undefined && { assignedArtistId: parsed.assignedArtistId }),
...(parsed.sortOrder !== undefined && { sortOrder: parsed.sortOrder }),
},
});
// Notify new assignee when assignee changes
if (
parsed.assignedArtistId !== undefined &&
parsed.assignedArtistId !== task.assignedArtistId &&
parsed.assignedArtistId !== null
) {
const contextCode = task.shot?.shotCode ?? task.asset?.assetCode ?? null;
const assignedByName = session.user.name ?? session.user.email ?? "Someone";
const artist = await db.user.findUnique({
where: { id: parsed.assignedArtistId },
select: { name: true, email: true },
});
await notifyTaskAssigned({
artistId: parsed.assignedArtistId,
taskTitle: task.title,
taskId,
contextCode,
assignedByName,
}).catch(() => {});
if (task.project.slackWebhook) {
const taskUrl = `${process.env.NEXTAUTH_URL ?? ""}/tasks/${taskId}`;
await slackNotifyTaskAssigned(task.project.slackWebhook, {
taskTitle: task.title,
contextCode,
artistName: artist?.name ?? artist?.email ?? "artist",
assignedByName,
projectName: task.project.name,
taskUrl,
}).catch(() => {});
}
}
revalidatePath(`/projects/${task.projectId}`);
revalidatePath(`/tasks/${taskId}`);
return { success: true, task: updated };
}
export async function updateTaskStatus(taskId: string, status: TaskStatus) {
const session = await auth();
if (!session?.user) throw new Error("Unauthorized");
const task = await db.task.findUnique({
where: { id: taskId },
include: {
shot: { select: { id: true, shotCode: true } },
asset: { select: { assetCode: true } },
project: { select: { id: true, name: true, slackWebhook: true } },
},
});
if (!task) throw new Error("Task not found");
const updated = await db.task.update({
where: { id: taskId },
data: { status },
});
const contextCode = task.shot?.shotCode ?? task.asset?.assetCode ?? null;
const taskUrl = `${process.env.NEXTAUTH_URL ?? ""}/tasks/${taskId}`;
const actorName = session.user.name ?? session.user.email ?? "Someone";
// Task moved to internal review — notify supervisors/producers
if (status === "INTERNAL_REVIEW") {
await notifyTaskReadyForReview({
taskId,
taskTitle: task.title,
contextCode,
artistName: actorName,
projectId: task.projectId,
taskUrl,
}).catch(() => {});
if (task.project.slackWebhook) {
await slackNotifyTaskReadyForReview(task.project.slackWebhook, {
taskTitle: task.title,
contextCode,
artistName: actorName,
projectName: task.project.name,
taskUrl,
}).catch(() => {});
}
}
// Task approved (moved to DONE by reviewer) — notify artist
if (status === "DONE" && task.assignedArtistId && task.assignedArtistId !== session.user.id) {
await notifyTaskApproved({
artistId: task.assignedArtistId,
taskTitle: task.title,
taskId,
contextCode,
reviewerName: actorName,
}).catch(() => {});
}
// Changes requested — notify artist
if (status === "CHANGES" && task.assignedArtistId && task.assignedArtistId !== session.user.id) {
await notifyTaskChangesRequested({
artistId: task.assignedArtistId,
taskTitle: task.title,
taskId,
contextCode,
reviewerName: actorName,
}).catch(() => {});
}
// Recalculate shot status from task states
if (task.shot) {
await recalcShotStatus(task.shot.id).catch(() => {});
}
revalidatePath(`/projects/${task.projectId}`);
revalidatePath(`/tasks/${taskId}`);
return { success: true };
}
export async function deleteTask(taskId: 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 task = await db.task.findUnique({ where: { id: taskId } });
if (!task) throw new Error("Task not found");
const shotId = task.shotId;
await db.task.delete({ where: { id: taskId } });
// Recalculate shot status after task removal
if (shotId) {
await recalcShotStatus(shotId).catch(() => {});
}
revalidatePath(`/projects/${task.projectId}`);
return { success: true };
}
export async function getTaskById(taskId: string) {
const session = await auth();
if (!session?.user) throw new Error("Unauthorized");
return db.task.findUnique({
where: { id: taskId },
include: {
shot: { select: { id: true, shotCode: true, projectId: true } },
asset: { select: { id: true, assetCode: true, name: true, projectId: true } },
assignedArtist: { select: { id: true, name: true, email: true, image: true } },
createdBy: { select: { id: true, name: true, email: true } },
project: { select: { id: true, name: true, code: true } },
versions: {
orderBy: { versionNumber: "desc" },
include: {
artist: { select: { id: true, name: true, image: true } },
_count: { select: { comments: true } },
},
},
_count: { select: { versions: true } },
},
});
}
export async function getProjectTasks(projectId: string) {
const session = await auth();
if (!session?.user) throw new Error("Unauthorized");
return db.task.findMany({
where: { projectId },
orderBy: [{ shotId: "asc" }, { sortOrder: "asc" }],
include: {
shot: { select: { id: true, shotCode: true } },
asset: { select: { id: true, assetCode: true, name: true } },
assignedArtist: { select: { id: true, name: true, email: true, image: true } },
_count: { select: { versions: true } },
},
});
}
export async function getShotTasks(shotId: string) {
const session = await auth();
if (!session?.user) throw new Error("Unauthorized");
return db.task.findMany({
where: { shotId },
orderBy: { sortOrder: "asc" },
include: {
assignedArtist: { select: { id: true, name: true, email: true, image: true } },
_count: { select: { versions: true } },
versions: {
take: 1,
orderBy: { versionNumber: "desc" },
select: { id: true, versionNumber: true, approvalStatus: true, createdAt: true },
},
},
});
}
export async function getAssetTasks(assetId: string) {
const session = await auth();
if (!session?.user) throw new Error("Unauthorized");
return db.task.findMany({
where: { assetId },
orderBy: { sortOrder: "asc" },
include: {
assignedArtist: { select: { id: true, name: true, email: true, image: true } },
_count: { select: { versions: true } },
versions: {
take: 1,
orderBy: { versionNumber: "desc" },
select: { id: true, versionNumber: true, approvalStatus: true, createdAt: true },
},
},
});
}
export async function getMyTasks() {
const session = await auth();
if (!session?.user) throw new Error("Unauthorized");
return db.task.findMany({
where: { assignedArtistId: session.user.id },
orderBy: [{ status: "asc" }, { dueDate: "asc" }],
include: {
shot: { select: { id: true, shotCode: true } },
asset: { select: { id: true, assetCode: true, name: true } },
project: { select: { id: true, name: true, code: true } },
_count: { select: { versions: true } },
},
});
}