Files
MOPC-Portal/src/server/routers/assignment/assignment-notifications.ts

139 lines
4.8 KiB
TypeScript
Raw Normal View History

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<string, number> = {}
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<number, string[]>()
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 }
}),
})