Files
vfxreview/app/api/client/[token]/comment/route.ts
T
twotalesanimation 41cf79abbe fix(notifications): tell staff about every new comment
Only the shot's assigned artist was notified of player comments, and
client-link comments and pasted WhatsApp feedback notified no one.
notifyCommentsAdded now notifies all active staff plus the shot/task
artist, never the author, from all three paths. A pasted round becomes
one notification per version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 09:06:26 +02:00

89 lines
3.1 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { slackNotifyNewFeedback } from "@/lib/slack";
import { notifyCommentsAdded } from "@/lib/notifications";
import { validateReviewToken, getOrCreateReviewUser } from "@/lib/review-auth";
export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ token: string }> }
) {
const { token } = await params;
const result = await validateReviewToken(token, req);
if (result.type === "requiresPassword") {
return NextResponse.json({ requiresPassword: true }, { status: 401 });
}
if (result.type === "invalid") {
return NextResponse.json({ error: "Invalid or expired review link" }, { status: 403 });
}
const session = result.session;
const body = await req.json();
const { versionId, frameNumber, timestamp, text } = body;
if (!versionId || frameNumber == null || timestamp == null || !text?.trim()) {
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
}
// Ensure the version belongs to this project
const version = await db.version.findUnique({
where: { id: versionId },
include: {
shot: { select: { projectId: true, shotCode: true, artistId: true, project: { select: { slackWebhook: true } } } },
task: { select: { projectId: true, title: true, assignedArtistId: true, project: { select: { slackWebhook: true } }, shot: { select: { shotCode: true } } } },
},
});
const projectId = version?.shot?.projectId ?? version?.task?.projectId;
if (!version || projectId !== session.projectId) {
return NextResponse.json({ error: "Version not found" }, { status: 404 });
}
// Resolve commenter identity
const user = await getOrCreateReviewUser(session, token);
const comment = await db.comment.create({
data: {
versionId,
authorId: user.id,
frameNumber,
timestamp,
text: text.trim(),
},
include: {
author: { select: { id: true, name: true, image: true, email: true } },
replies: true,
},
});
// Slack notification
const slackWebhook =
version.shot?.project?.slackWebhook ?? version.task?.project?.slackWebhook ?? null;
const shotCode =
version.shot?.shotCode ?? version.task?.shot?.shotCode ?? version.task?.title ?? "Task";
if (slackWebhook) {
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
await slackNotifyNewFeedback(slackWebhook, {
shotCode,
frameNumber,
authorName: user.name ?? user.email,
commentText: text.trim(),
reviewUrl: `${appUrl}/client/${token}/review/${versionId}`,
});
}
await notifyCommentsAdded({
notes: [{
versionId,
shotCode,
frameNumber,
artistIds: [version.shot?.artistId, version.task?.assignedArtistId],
}],
authorId: user.id,
authorName: user.name ?? user.email,
origin: "client",
}).catch((err) => console.error("[client comment] notify failed", err));
// canEdit mirrors the GET shape — a note you just wrote is always your own.
return NextResponse.json({ comment: { ...comment, canEdit: true } }, { status: 201 });
}