Add per-round assignment constraints (min/max per judge)
- Add minAssignmentsPerJuror and maxAssignmentsPerJuror fields to Round model - Update assignment router: - Calculate effective max from user override or round default - Add forceOverride parameter for manual assignment beyond limits - Update getSuggestions to use round constraints with min target bonus - Update getAISuggestions to pass constraints to AI service - Update AI assignment service: - Add minAssignmentsPerJuror to constraints interface - Update fallback algorithm with under-min bonus scoring - New score weights: 50% expertise, 30% load, 20% under-min bonus - Update round router: - Add new constraint fields to create/update schemas - Add validation for min <= max constraint - Update admin UI: - Add min/max constraint fields to round edit page - Remove hardcoded maxPerJuror from assignments page - Add migration files for production deployment: - User.bio field for judge/mentor profiles - Round assignment constraint fields Constraint hierarchy: 1. User.maxAssignments (if set) overrides round default 2. Round.maxAssignmentsPerJuror is the default cap 3. Admin can force-override any limit with confirmation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -70,6 +70,8 @@ export const roundRouter = router({
|
||||
name: z.string().min(1).max(255),
|
||||
roundType: z.enum(['FILTERING', 'EVALUATION', 'LIVE_EVENT']).default('EVALUATION'),
|
||||
requiredReviews: z.number().int().min(1).max(10).default(3),
|
||||
minAssignmentsPerJuror: z.number().int().min(1).max(50).default(5),
|
||||
maxAssignmentsPerJuror: z.number().int().min(1).max(100).default(20),
|
||||
sortOrder: z.number().int().optional(),
|
||||
settingsJson: z.record(z.unknown()).optional(),
|
||||
votingStartAt: z.date().optional(),
|
||||
@@ -78,6 +80,14 @@ export const roundRouter = router({
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
// Validate assignment constraints
|
||||
if (input.minAssignmentsPerJuror > input.maxAssignmentsPerJuror) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Min assignments per juror must be less than or equal to max',
|
||||
})
|
||||
}
|
||||
|
||||
// Validate dates
|
||||
if (input.votingStartAt && input.votingEndAt) {
|
||||
if (input.votingEndAt <= input.votingStartAt) {
|
||||
@@ -154,6 +164,8 @@ export const roundRouter = router({
|
||||
slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/).optional().nullable(),
|
||||
roundType: z.enum(['FILTERING', 'EVALUATION', 'LIVE_EVENT']).optional(),
|
||||
requiredReviews: z.number().int().min(1).max(10).optional(),
|
||||
minAssignmentsPerJuror: z.number().int().min(1).max(50).optional(),
|
||||
maxAssignmentsPerJuror: z.number().int().min(1).max(100).optional(),
|
||||
submissionDeadline: z.date().optional().nullable(),
|
||||
votingStartAt: z.date().optional().nullable(),
|
||||
votingEndAt: z.date().optional().nullable(),
|
||||
@@ -174,6 +186,22 @@ export const roundRouter = router({
|
||||
}
|
||||
}
|
||||
|
||||
// Validate assignment constraints if either is provided
|
||||
if (data.minAssignmentsPerJuror !== undefined || data.maxAssignmentsPerJuror !== undefined) {
|
||||
const existingRound = await ctx.prisma.round.findUnique({
|
||||
where: { id },
|
||||
select: { minAssignmentsPerJuror: true, maxAssignmentsPerJuror: true, status: true },
|
||||
})
|
||||
const newMin = data.minAssignmentsPerJuror ?? existingRound?.minAssignmentsPerJuror ?? 5
|
||||
const newMax = data.maxAssignmentsPerJuror ?? existingRound?.maxAssignmentsPerJuror ?? 20
|
||||
if (newMin > newMax) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Min assignments per juror must be less than or equal to max',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we should auto-activate (if voting start is in the past and round is DRAFT)
|
||||
const now = new Date()
|
||||
let autoActivate = false
|
||||
|
||||
Reference in New Issue
Block a user