94 lines
3.3 KiB
TypeScript
94 lines
3.3 KiB
TypeScript
|
|
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<string, number>,
|
||
|
|
stageName: string | null | undefined,
|
||
|
|
deadline: string | undefined,
|
||
|
|
): Promise<void> {
|
||
|
|
const usersByProjectCount = new Map<number, string[]>()
|
||
|
|
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<CandidateJuror[]> {
|
||
|
|
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 },
|
||
|
|
})
|
||
|
|
}
|