import { db } from "@/lib/db"; import { NotificationType } from "@prisma/client"; export interface CreateNotificationPayload { userId: string; type: NotificationType; title: string; message: string; data?: Record; } /** * Create a single in-app notification. */ export async function createNotification( payload: CreateNotificationPayload ) { return db.notification.create({ data: { userId: payload.userId, type: payload.type, title: payload.title, message: payload.message, data: (payload.data ?? {}) as any, }, }); } /** * Create notifications for multiple users at once. */ export async function createNotifications( payloads: CreateNotificationPayload[] ) { return db.notification.createMany({ data: payloads.map((p) => ({ userId: p.userId, type: p.type, title: p.title, message: p.message, data: (p.data ?? {}) as any, })), }); } /** * Notify all ADMIN, PRODUCER, and SUPERVISOR users about a new version upload. */ export async function notifyNewVersionUploaded(params: { shotCode: string; versionNumber: number; projectId: string; versionId: string; artistName: string; }) { const staff = await db.user.findMany({ where: { role: { in: ["ADMIN", "PRODUCER", "SUPERVISOR"] }, isActive: true, }, select: { id: true }, }); const label = `v${String(params.versionNumber).padStart(3, "0")}`; await createNotifications( staff.map((u) => ({ userId: u.id, type: NotificationType.VERSION_UPLOADED, title: "New version uploaded", message: `${params.shotCode} ${label} uploaded by ${params.artistName}`, data: { versionId: params.versionId, shotCode: params.shotCode, versionNumber: params.versionNumber, }, })) ); } /** * Notify relevant users of an approval status change. */ export async function notifyApprovalChange(params: { artistId: string | null; shotCode: string; versionId: string; status: "APPROVED" | "REJECTED" | "NEEDS_CHANGES"; reviewerName: string; }) { if (!params.artistId) return; const typeMap = { APPROVED: NotificationType.SHOT_APPROVED, REJECTED: NotificationType.SHOT_REJECTED, NEEDS_CHANGES: NotificationType.REVISION_REQUESTED, } as const; const msgMap = { APPROVED: `${params.shotCode} was approved by ${params.reviewerName}`, REJECTED: `${params.shotCode} was rejected by ${params.reviewerName}`, NEEDS_CHANGES: `${params.shotCode} needs changes — ${params.reviewerName}`, } as const; await createNotification({ userId: params.artistId, type: typeMap[params.status], title: msgMap[params.status], message: msgMap[params.status], data: { versionId: params.versionId, shotCode: params.shotCode }, }); } /** * Notify an artist that a task has been assigned to them. */ export async function notifyTaskAssigned(params: { artistId: string; taskTitle: string; taskId: string; contextCode: string | null; assignedByName: string; }) { await createNotification({ userId: params.artistId, type: NotificationType.TASK_ASSIGNED, title: "New task assigned to you", message: `${params.assignedByName} assigned you: ${params.taskTitle}${ params.contextCode ? ` (${params.contextCode})` : "" }`, data: { taskId: params.taskId }, }); } /** * Notify supervisors/producers that a task is ready for internal review. */ export async function notifyTaskReadyForReview(params: { taskId: string; taskTitle: string; contextCode: string | null; artistName: string; projectId: string; taskUrl: string; }) { const staff = await db.user.findMany({ where: { role: { in: ["ADMIN", "PRODUCER", "SUPERVISOR"] }, isActive: true, }, select: { id: true }, }); const label = params.contextCode ? `${params.contextCode} — ` : ""; await createNotifications( staff.map((u) => ({ userId: u.id, type: NotificationType.TASK_READY_FOR_REVIEW, title: "Task ready for review", message: `${label}${params.taskTitle} marked ready by ${params.artistName}`, data: { taskId: params.taskId, taskUrl: params.taskUrl }, })) ); } /** * Notify an artist that a task has been approved. */ export async function notifyTaskApproved(params: { artistId: string; taskTitle: string; taskId: string; contextCode: string | null; reviewerName: string; }) { await createNotification({ userId: params.artistId, type: NotificationType.TASK_APPROVED, title: "Task approved", message: `${params.reviewerName} approved: ${params.taskTitle}${ params.contextCode ? ` (${params.contextCode})` : "" }`, data: { taskId: params.taskId }, }); } /** * Notify an artist that changes were requested on their task. */ export async function notifyTaskChangesRequested(params: { artistId: string; taskTitle: string; taskId: string; contextCode: string | null; reviewerName: string; }) { await createNotification({ userId: params.artistId, type: NotificationType.TASK_CHANGES_REQUESTED, title: "Changes requested on task", message: `${params.reviewerName} requested changes: ${params.taskTitle}${ params.contextCode ? ` (${params.contextCode})` : "" }`, data: { taskId: params.taskId }, }); } /** * Notify a comment author about a new reply. */ export async function notifyCommentReply(params: { commentAuthorId: string; replierName: string; shotCode: string; frameNumber: number; versionId: string; }) { await createNotification({ userId: params.commentAuthorId, type: NotificationType.COMMENT_REPLY, title: "New reply to your comment", message: `${params.replierName} replied on ${params.shotCode} frame ${params.frameNumber}`, data: { versionId: params.versionId, shotCode: params.shotCode, frameNumber: params.frameNumber, }, }); } /** One new comment, as the recipients of its notification need it. */ export interface NewCommentNote { versionId: string; shotCode: string; frameNumber: number; /** The shot's artist and, for a task version, the task's artist. */ artistIds: (string | null | undefined)[]; } /** * Who hears about new comments, and what they read. Pure so it can be tested: * every active staff member plus each note's artist, never the author. * Notes are grouped per version so a pasted round of client feedback is one * notification per shot rather than one per line. */ export function buildCommentNotifications(params: { notes: NewCommentNote[]; authorId: string; authorName: string; /** "client" = typed on a client review link; "whatsapp" = pasted by staff. */ origin?: "client" | "whatsapp"; staffIds: string[]; }): CreateNotificationPayload[] { const byVersion = new Map(); for (const n of params.notes) { byVersion.set(n.versionId, [...(byVersion.get(n.versionId) ?? []), n]); } const who = params.origin === "client" ? `Client ${params.authorName}` : params.origin === "whatsapp" ? `Client feedback (logged by ${params.authorName})` : params.authorName; const title = params.origin ? "New client feedback" : "New feedback"; const payloads: CreateNotificationPayload[] = []; for (const [versionId, notes] of byVersion) { const { shotCode, frameNumber } = notes[0]; const message = notes.length === 1 ? `${who} commented on ${shotCode} at frame ${frameNumber}` : `${who} left ${notes.length} notes on ${shotCode}`; const recipients = new Set([ ...params.staffIds, ...notes.flatMap((n) => n.artistIds).filter((id): id is string => !!id), ]); recipients.delete(params.authorId); for (const userId of recipients) { payloads.push({ userId, type: NotificationType.FEEDBACK_ADDED, title, message, data: { versionId, shotCode, frameNumber }, }); } } return payloads; } /** * Notify staff and the shot's artist about new comments. Artists who are not * staff are only told when they are still active. */ export async function notifyCommentsAdded(params: { notes: NewCommentNote[]; authorId: string; authorName: string; origin?: "client" | "whatsapp"; }) { if (params.notes.length === 0) return; const artistIds = [ ...new Set(params.notes.flatMap((n) => n.artistIds).filter((id): id is string => !!id)), ]; const [staff, activeArtists] = await Promise.all([ db.user.findMany({ where: { role: { in: ["ADMIN", "PRODUCER", "SUPERVISOR"] }, isActive: true }, select: { id: true }, }), db.user.findMany({ where: { id: { in: artistIds }, isActive: true, role: { not: "CLIENT" } }, select: { id: true }, }), ]); const active = new Set(activeArtists.map((u) => u.id)); const payloads = buildCommentNotifications({ ...params, notes: params.notes.map((n) => ({ ...n, artistIds: n.artistIds.filter((id) => id && active.has(id)) })), staffIds: staff.map((u) => u.id), }); if (payloads.length) await createNotifications(payloads); }