refactor: tech debt batch 3 — type safety + assignment router split
All checks were successful
Build and Push Docker Image / build (push) Successful in 13m4s
All checks were successful
Build and Push Docker Image / build (push) Successful in 13m4s
#5 — Replaced 55x PrismaClient | any with proper Prisma types across 8 files - Service files: PrismaClient | any → PrismaClient, tx: any → Prisma.TransactionClient - Fixed 4 real bugs uncovered by typing: - mentor-workspace.ts: wrong FK fields (mentorAssignmentId → workspaceId, role → senderRole) - ai-shortlist.ts: untyped string passed to CompetitionCategory enum filter - result-lock.ts: unknown passed where Prisma.InputJsonValue required #9 — Split assignment.ts (2,775 lines) into 6 focused files: - shared.ts (93 lines) — MOVABLE_EVAL_STATUSES, buildBatchNotifications, getCandidateJurors - assignment-crud.ts (473 lines) — 8 core CRUD procedures - assignment-suggestions.ts (880 lines) — AI suggestions + job runner - assignment-notifications.ts (138 lines) — 2 notification procedures - assignment-redistribution.ts (1,162 lines) — 8 reassign/transfer procedures - index.ts (15 lines) — barrel export with router merge, zero frontend changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
93
src/server/routers/assignment/shared.ts
Normal file
93
src/server/routers/assignment/shared.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
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 },
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user