"use server"; import { auth } from "@/auth"; import { db } from "@/lib/db"; import { revalidatePath } from "next/cache"; import { z } from "zod"; import { findShot, parseBoardText } from "@/lib/board-parse"; // The board is studio-internal. Anyone on staff can add and tick off lines; // it is a shared to-do list, not an approval surface. async function requireStaff() { const session = await auth(); if (!session?.user) throw new Error("Unauthorized"); if (session.user.role === "CLIENT") throw new Error("Insufficient permissions"); return session.user; } async function boardContext(projectId: string) { const [shots, users] = await Promise.all([ db.shot.findMany({ where: { projectId }, select: { id: true, shotCode: true, clientCode: true }, }), db.user.findMany({ where: { isActive: true, role: { not: "CLIENT" } }, select: { id: true, name: true, email: true }, }), ]); return { shots, users }; } const addSchema = z.object({ projectId: z.string().cuid(), input: z.string().min(1).max(20000), group: z.string().max(100).optional(), }); /** Adds one line, or a pasted block of lines, to a project's board. */ export async function addBoardItems(data: z.infer) { const user = await requireStaff(); const parsed = addSchema.parse(data); const items = parseBoardText(parsed.input, { ...(await boardContext(parsed.projectId)), group: parsed.group, }); if (items.length === 0) return { success: true, added: 0, unmatched: [] as string[] }; const last = await db.boardItem.findFirst({ where: { projectId: parsed.projectId }, orderBy: { sortOrder: "desc" }, select: { sortOrder: true }, }); const base = (last?.sortOrder ?? -1) + 1; await db.boardItem.createMany({ data: items.map((item, i) => ({ projectId: parsed.projectId, shotId: item.shotId, code: item.code, text: item.text, group: item.group, assigneeId: item.assigneeId, waitingOn: item.waitingOn, sortOrder: base + i, createdById: user.id, })), }); revalidatePath("/board"); return { success: true, added: items.length, unmatched: items.filter((i) => i.code && !i.shotId).map((i) => i.code as string), }; } const updateSchema = z.object({ text: z.string().max(5000).optional(), code: z.string().max(100).nullable().optional(), group: z.string().max(100).optional(), assigneeId: z.string().cuid().nullable().optional(), waitingOn: z.string().max(500).nullable().optional(), }); export async function updateBoardItem(id: string, data: z.infer) { await requireStaff(); const parsed = updateSchema.parse(data); const item = await db.boardItem.findUnique({ where: { id }, select: { projectId: true } }); if (!item) throw new Error("Board item not found"); // Changing the code re-links the line; a code that matches nothing is kept // as a flag, exactly as when it was first typed. let link: { code: string | null; shotId: string | null } | undefined; if (parsed.code !== undefined) { const code = parsed.code?.trim() || null; const shot = code ? findShot(code, (await boardContext(item.projectId)).shots) : null; link = { code: shot?.shotCode ?? code?.toUpperCase() ?? null, shotId: shot?.id ?? null }; } await db.boardItem.update({ where: { id }, data: { ...(parsed.text !== undefined && { text: parsed.text.trim() }), ...(parsed.group !== undefined && { group: parsed.group.trim() }), ...(parsed.assigneeId !== undefined && { assigneeId: parsed.assigneeId }), ...(parsed.waitingOn !== undefined && { waitingOn: parsed.waitingOn?.trim() || null }), ...link, }, }); revalidatePath("/board"); return { success: true }; } export async function setBoardItemDone(id: string, done: boolean) { const user = await requireStaff(); await db.boardItem.update({ where: { id }, data: done ? { doneAt: new Date(), doneById: user.id } : { doneAt: null, doneById: null }, }); revalidatePath("/board"); return { success: true }; } export async function deleteBoardItem(id: string) { await requireStaff(); await db.boardItem.delete({ where: { id } }); revalidatePath("/board"); return { success: true }; } const statusSchema = z.object({ projectId: z.string().cuid(), status: z.string().max(2000), }); export async function setProjectBoardStatus(data: z.infer) { await requireStaff(); const parsed = statusSchema.parse(data); const status = parsed.status.trim() || null; await db.project.update({ where: { id: parsed.projectId }, data: { boardStatus: status, boardStatusAt: status ? new Date() : null }, }); revalidatePath("/board"); return { success: true }; } const milestoneSchema = z.object({ projectId: z.string().cuid(), label: z.string().min(1).max(200), date: z.string().min(1), }); export async function addMilestone(data: z.infer) { await requireStaff(); const parsed = milestoneSchema.parse(data); const date = new Date(parsed.date); if (isNaN(date.getTime())) throw new Error("Invalid date"); await db.projectMilestone.create({ data: { projectId: parsed.projectId, label: parsed.label.trim(), date }, }); revalidatePath("/board"); return { success: true }; } export async function setMilestoneDone(id: string, done: boolean) { await requireStaff(); await db.projectMilestone.update({ where: { id }, data: { doneAt: done ? new Date() : null }, }); revalidatePath("/board"); return { success: true }; } export async function deleteMilestone(id: string) { await requireStaff(); await db.projectMilestone.delete({ where: { id } }); revalidatePath("/board"); return { success: true }; }