From 1dcc7a59901c0fdac75fd1cb0144e6f597507b90 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 19 Feb 2026 17:18:07 +0100 Subject: [PATCH] Add per-juror notify button in Jury Progress section Adds a mail icon on hover for each juror row in the Jury Progress table, allowing admins to send assignment notifications to individual jurors instead of only bulk-notifying all at once. Co-Authored-By: Claude Opus 4.6 --- .../(admin)/admin/rounds/[roundId]/page.tsx | 43 ++++++++++++--- src/server/routers/assignment.ts | 54 +++++++++++++++++++ 2 files changed, 90 insertions(+), 7 deletions(-) diff --git a/src/app/(admin)/admin/rounds/[roundId]/page.tsx b/src/app/(admin)/admin/rounds/[roundId]/page.tsx index 474c895..a57dd13 100644 --- a/src/app/(admin)/admin/rounds/[roundId]/page.tsx +++ b/src/app/(admin)/admin/rounds/[roundId]/page.tsx @@ -2363,11 +2363,18 @@ function JuryProgressTable({ roundId }: { roundId: string }) { { refetchInterval: 15_000 }, ) + const notifyMutation = trpc.assignment.notifySingleJurorOfAssignments.useMutation({ + onSuccess: (data) => { + toast.success(`Notified juror of ${data.projectCount} assignment(s)`) + }, + onError: (err) => toast.error(err.message), + }) + return ( Jury Progress - Evaluation completion per juror + Evaluation completion per juror. Click the mail icon to notify an individual juror. {isLoading ? ( @@ -2391,12 +2398,34 @@ function JuryProgressTable({ roundId }: { roundId: string }) { : 'bg-gray-300' return ( -
-
- {juror.name} - - {juror.completed}/{juror.assigned} ({pct}%) - +
+
+ {juror.name} +
+ + {juror.completed}/{juror.assigned} ({pct}%) + + + + + + +

Notify this juror of their assignments

+
+
+
{ + 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 } + }), })