import { z } from 'zod' import { TRPCError } from '@trpc/server' import { router, adminProcedure } from '../../trpc' import { createBulkNotifications, NotificationTypes } from '../../services/in-app-notification' import { logAudit } from '@/server/utils/audit' export const assignmentNotificationsRouter = router({ /** * Notify all jurors of their current assignments for a round (admin only). * Sends in-app notifications (emails are handled by maybeSendEmail via createBulkNotifications). */ notifyJurorsOfAssignments: adminProcedure .input(z.object({ roundId: z.string() })) .mutation(async ({ ctx, input }) => { const round = await ctx.prisma.round.findUniqueOrThrow({ where: { id: input.roundId }, select: { name: true, windowCloseAt: true }, }) // Get all assignments grouped by user const assignments = await ctx.prisma.assignment.findMany({ where: { roundId: input.roundId }, select: { userId: true }, }) if (assignments.length === 0) { return { sent: 0, jurorCount: 0 } } // Count assignments per user const userCounts: Record = {} for (const a of assignments) { userCounts[a.userId] = (userCounts[a.userId] || 0) + 1 } const deadline = round.windowCloseAt ? new Date(round.windowCloseAt).toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', }) : undefined // Create in-app notifications grouped by project count const usersByProjectCount = new Map() for (const [userId, projectCount] of Object.entries(userCounts)) { const existing = usersByProjectCount.get(projectCount) || [] existing.push(userId) usersByProjectCount.set(projectCount, existing) } let totalSent = 0 for (const [projectCount, userIds] of usersByProjectCount) { if (userIds.length === 0) continue await createBulkNotifications({ userIds, type: NotificationTypes.BATCH_ASSIGNED, title: `${projectCount} Projects Assigned`, message: `You have been assigned ${projectCount} project${projectCount > 1 ? 's' : ''} to evaluate for ${round.name || 'this round'}.`, linkUrl: `/jury/competitions`, linkLabel: 'View Assignments', metadata: { projectCount, roundName: round.name, deadline }, }) totalSent += userIds.length } await logAudit({ prisma: ctx.prisma, userId: ctx.user.id, action: 'NOTIFY_JURORS_OF_ASSIGNMENTS', entityType: 'Round', entityId: input.roundId, detailsJson: { jurorCount: Object.keys(userCounts).length, totalAssignments: assignments.length, }, ipAddress: ctx.ip, userAgent: ctx.userAgent, }) return { sent: totalSent, jurorCount: Object.keys(userCounts).length } }), notifySingleJurorOfAssignments: adminProcedure .input(z.object({ roundId: z.string(), userId: z.string() })) .mutation(async ({ ctx, input }) => { const round = await ctx.prisma.round.findUniqueOrThrow({ where: { id: input.roundId }, select: { name: true, windowCloseAt: true }, }) const assignments = await ctx.prisma.assignment.findMany({ where: { roundId: input.roundId, userId: input.userId }, select: { id: true }, }) if (assignments.length === 0) { throw new TRPCError({ code: 'NOT_FOUND', message: 'No assignments found for this juror in this round' }) } const projectCount = assignments.length const deadline = round.windowCloseAt ? new Date(round.windowCloseAt).toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', }) : undefined await createBulkNotifications({ userIds: [input.userId], type: NotificationTypes.BATCH_ASSIGNED, title: `${projectCount} Projects Assigned`, message: `You have been assigned ${projectCount} project${projectCount > 1 ? 's' : ''} to evaluate for ${round.name || 'this round'}.`, linkUrl: `/jury/competitions`, linkLabel: 'View Assignments', metadata: { projectCount, roundName: round.name, deadline }, }) await logAudit({ prisma: ctx.prisma, userId: ctx.user.id, action: 'NOTIFY_SINGLE_JUROR_OF_ASSIGNMENTS', entityType: 'Round', entityId: input.roundId, detailsJson: { targetUserId: input.userId, assignmentCount: projectCount, }, ipAddress: ctx.ip, userAgent: ctx.userAgent, }) return { sent: 1, projectCount } }), })