Auto-reassign projects when juror declares conflict of interest
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m51s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m51s
When a juror declares COI, the system now automatically: - Finds an eligible replacement juror (not at capacity, no COI, not already assigned) - Deletes the conflicted assignment and creates a new one - Notifies the replacement juror and admins - Load-balances by picking the juror with fewest current assignments Also adds: - "Reassign (COI)" action in assignment table dropdown with COI badge indicator - Admin "Reassign to another juror" in COI review now triggers actual reassignment - Per-juror notify button is now always visible (not just on hover) - reassignCOI admin procedure for retroactive manual reassignment Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2411,7 +2411,7 @@ function JuryProgressTable({ roundId }: { roundId: string }) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
className="h-5 w-5 text-muted-foreground hover:text-foreground"
|
||||
disabled={notifyMutation.isPending}
|
||||
onClick={() => notifyMutation.mutate({ roundId, userId: juror.id })}
|
||||
>
|
||||
@@ -2671,6 +2671,17 @@ function IndividualAssignmentsTable({
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
const reassignCOIMutation = trpc.assignment.reassignCOI.useMutation({
|
||||
onSuccess: (data) => {
|
||||
utils.assignment.listByStage.invalidate({ roundId })
|
||||
utils.roundEngine.getProjectStates.invalidate({ roundId })
|
||||
utils.analytics.getJurorWorkload.invalidate({ roundId })
|
||||
utils.evaluation.listCOIByStage.invalidate({ roundId })
|
||||
toast.success(`Reassigned to ${data.newJurorName}`)
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
const createMutation = trpc.assignment.create.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.assignment.listByStage.invalidate({ roundId })
|
||||
@@ -2875,19 +2886,27 @@ function IndividualAssignmentsTable({
|
||||
>
|
||||
<span className="truncate">{a.user?.name || a.user?.email || 'Unknown'}</span>
|
||||
<span className="truncate text-muted-foreground">{a.project?.title || 'Unknown'}</span>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'text-[10px] justify-center',
|
||||
a.evaluation?.status === 'SUBMITTED'
|
||||
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
|
||||
: a.evaluation?.status === 'DRAFT'
|
||||
? 'bg-blue-50 text-blue-700 border-blue-200'
|
||||
: 'bg-gray-50 text-gray-600 border-gray-200',
|
||||
<div className="flex items-center gap-1">
|
||||
{a.conflictOfInterest?.hasConflict ? (
|
||||
<Badge variant="outline" className="text-[10px] justify-center bg-red-50 text-red-700 border-red-200">
|
||||
COI
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'text-[10px] justify-center',
|
||||
a.evaluation?.status === 'SUBMITTED'
|
||||
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
|
||||
: a.evaluation?.status === 'DRAFT'
|
||||
? 'bg-blue-50 text-blue-700 border-blue-200'
|
||||
: 'bg-gray-50 text-gray-600 border-gray-200',
|
||||
)}
|
||||
>
|
||||
{a.evaluation?.status || 'PENDING'}
|
||||
</Badge>
|
||||
)}
|
||||
>
|
||||
{a.evaluation?.status || 'PENDING'}
|
||||
</Badge>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7">
|
||||
@@ -2895,6 +2914,18 @@ function IndividualAssignmentsTable({
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{a.conflictOfInterest?.hasConflict && (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
onClick={() => reassignCOIMutation.mutate({ assignmentId: a.id })}
|
||||
disabled={reassignCOIMutation.isPending}
|
||||
>
|
||||
<UserPlus className="h-3.5 w-3.5 mr-2 text-blue-600" />
|
||||
Reassign (COI)
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
{a.evaluation && (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
@@ -3988,9 +4019,15 @@ function COIReviewSection({ roundId }: { roundId: string }) {
|
||||
)
|
||||
|
||||
const reviewMutation = trpc.evaluation.reviewCOI.useMutation({
|
||||
onSuccess: () => {
|
||||
onSuccess: (data) => {
|
||||
utils.evaluation.listCOIByStage.invalidate({ roundId })
|
||||
toast.success('COI review updated')
|
||||
utils.assignment.listByStage.invalidate({ roundId })
|
||||
utils.analytics.getJurorWorkload.invalidate({ roundId })
|
||||
if (data.reassignment) {
|
||||
toast.success(`Reassigned to ${data.reassignment.newJurorName}`)
|
||||
} else {
|
||||
toast.success('COI review updated')
|
||||
}
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user