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

94 lines
3.3 KiB
TypeScript
Raw Normal View History

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