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:
@@ -81,7 +81,7 @@ function AssignmentManagementContent({ roundId }: { roundId: string }) {
|
||||
const { data: assignments, isLoading: loadingAssignments } = trpc.assignment.listByRound.useQuery({ roundId })
|
||||
const { data: stats, isLoading: loadingStats } = trpc.assignment.getStats.useQuery({ roundId })
|
||||
const { data: suggestions, isLoading: loadingSuggestions, refetch: refetchSuggestions } = trpc.assignment.getSuggestions.useQuery(
|
||||
{ roundId, maxPerJuror: 10, minPerProject: 3 },
|
||||
{ roundId },
|
||||
{ enabled: !!round }
|
||||
)
|
||||
|
||||
|
||||
@@ -59,6 +59,8 @@ const updateRoundSchema = z
|
||||
.object({
|
||||
name: z.string().min(1, 'Name is required').max(255),
|
||||
requiredReviews: z.number().int().min(1).max(10),
|
||||
minAssignmentsPerJuror: z.number().int().min(1).max(50),
|
||||
maxAssignmentsPerJuror: z.number().int().min(1).max(100),
|
||||
votingStartAt: z.date().nullable().optional(),
|
||||
votingEndAt: z.date().nullable().optional(),
|
||||
})
|
||||
@@ -74,6 +76,13 @@ const updateRoundSchema = z
|
||||
path: ['votingEndAt'],
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(data) => data.minAssignmentsPerJuror <= data.maxAssignmentsPerJuror,
|
||||
{
|
||||
message: 'Min must be less than or equal to max',
|
||||
path: ['minAssignmentsPerJuror'],
|
||||
}
|
||||
)
|
||||
|
||||
type UpdateRoundForm = z.infer<typeof updateRoundSchema>
|
||||
|
||||
@@ -121,6 +130,8 @@ function EditRoundContent({ roundId }: { roundId: string }) {
|
||||
defaultValues: {
|
||||
name: '',
|
||||
requiredReviews: 3,
|
||||
minAssignmentsPerJuror: 5,
|
||||
maxAssignmentsPerJuror: 20,
|
||||
votingStartAt: null,
|
||||
votingEndAt: null,
|
||||
},
|
||||
@@ -132,6 +143,8 @@ function EditRoundContent({ roundId }: { roundId: string }) {
|
||||
form.reset({
|
||||
name: round.name,
|
||||
requiredReviews: round.requiredReviews,
|
||||
minAssignmentsPerJuror: round.minAssignmentsPerJuror,
|
||||
maxAssignmentsPerJuror: round.maxAssignmentsPerJuror,
|
||||
votingStartAt: round.votingStartAt ? new Date(round.votingStartAt) : null,
|
||||
votingEndAt: round.votingEndAt ? new Date(round.votingEndAt) : null,
|
||||
})
|
||||
@@ -161,6 +174,8 @@ function EditRoundContent({ roundId }: { roundId: string }) {
|
||||
id: roundId,
|
||||
name: data.name,
|
||||
requiredReviews: data.requiredReviews,
|
||||
minAssignmentsPerJuror: data.minAssignmentsPerJuror,
|
||||
maxAssignmentsPerJuror: data.maxAssignmentsPerJuror,
|
||||
roundType,
|
||||
settingsJson: roundSettings,
|
||||
votingStartAt: data.votingStartAt ?? null,
|
||||
@@ -277,6 +292,58 @@ function EditRoundContent({ roundId }: { roundId: string }) {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="minAssignmentsPerJuror"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Min Projects per Judge</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={50}
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(parseInt(e.target.value) || 1)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Target minimum projects each judge should receive
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="maxAssignmentsPerJuror"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Max Projects per Judge</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={100}
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(parseInt(e.target.value) || 1)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Maximum projects a judge can be assigned
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user