Initial commit
This commit is contained in:
@@ -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 } },
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user