import type { PrismaClient } from '@prisma/client' import { createBulkNotifications, NotificationTypes } from '../../services/in-app-notification' /** Evaluation statuses that are safe to move (not yet finalized). */ export const MOVABLE_EVAL_STATUSES = ['NOT_STARTED', 'DRAFT'] as const /** * Groups a per-user assignment count map into batches by count, then sends * BATCH_ASSIGNED notifications via createBulkNotifications. * * @param userAssignmentCounts - map of userId → number of newly-assigned projects * @param stageName - display name of the round (for the notification message) * @param deadline - formatted deadline string (optional) */ export async function buildBatchNotifications( userAssignmentCounts: Record, stageName: string | null | undefined, deadline: string | undefined, ): Promise { const usersByProjectCount = new Map() for (const [userId, projectCount] of Object.entries(userAssignmentCounts)) { const existing = usersByProjectCount.get(projectCount) || [] existing.push(userId) usersByProjectCount.set(projectCount, existing) } 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 ${stageName || 'this stage'}.`, linkUrl: `/jury/competitions`, linkLabel: 'View Assignments', metadata: { projectCount, roundName: stageName, deadline, }, }) } } export type CandidateJuror = { id: string name: string | null email: string maxAssignments: number | null } /** * Builds the candidate juror pool for a round, scoped to the jury group if one * is assigned, otherwise falling back to all active JURY_MEMBER users who have * at least one assignment in the round. * * @param prisma - Prisma client (or transaction client) * @param roundId - round being processed * @param juryGroupId - optional jury group id from the round * @param excludeUserId - userId to exclude from results (the source / dropped juror) */ export async function getCandidateJurors( prisma: PrismaClient, roundId: string, juryGroupId: string | null | undefined, excludeUserId: string, ): Promise { if (juryGroupId) { const members = await prisma.juryGroupMember.findMany({ where: { juryGroupId }, include: { user: { select: { id: true, name: true, email: true, maxAssignments: true, status: true } }, }, }) return members .filter((m) => m.user.status === 'ACTIVE' && m.user.id !== excludeUserId) .map((m) => m.user) } const roundJurorIds = await prisma.assignment.findMany({ where: { roundId }, select: { userId: true }, distinct: ['userId'], }) const ids = roundJurorIds.map((a) => a.userId).filter((id) => id !== excludeUserId) if (ids.length === 0) return [] return prisma.user.findMany({ where: { id: { in: ids }, roles: { has: 'JURY_MEMBER' }, status: 'ACTIVE' }, select: { id: true, name: true, email: true, maxAssignments: true }, }) }