Auto-reassign projects when juror declares conflict of interest
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:
Matt
2026-02-19 18:30:01 +01:00
parent 1dcc7a5990
commit 0ff84686f0
3 changed files with 259 additions and 17 deletions

View File

@@ -3,6 +3,7 @@ import { TRPCError } from '@trpc/server'
import { router, protectedProcedure, adminProcedure, juryProcedure } from '../trpc'
import { logAudit } from '@/server/utils/audit'
import { notifyAdmins, NotificationTypes } from '../services/in-app-notification'
import { reassignAfterCOI } from './assignment'
import { sendManualReminders } from '../services/evaluation-reminders'
import { generateSummary } from '@/server/services/ai-evaluation-summary'
@@ -536,7 +537,23 @@ export const evaluationRouter = router({
userAgent: ctx.userAgent,
})
return coi
// Auto-reassign the project to another eligible juror
let reassignment: { newJurorId: string; newJurorName: string } | null = null
if (input.hasConflict) {
try {
reassignment = await reassignAfterCOI({
assignmentId: input.assignmentId,
auditUserId: ctx.user.id,
auditIp: ctx.ip,
auditUserAgent: ctx.userAgent,
})
} catch (err) {
// Don't fail the COI declaration if reassignment fails
console.error('[COI] Auto-reassignment failed:', err)
}
}
return { ...coi, reassignment }
}),
/**
@@ -599,6 +616,17 @@ export const evaluationRouter = router({
},
})
// If admin chose "reassigned", trigger actual reassignment
let reassignment: { newJurorId: string; newJurorName: string } | null = null
if (input.reviewAction === 'reassigned') {
reassignment = await reassignAfterCOI({
assignmentId: coi.assignmentId,
auditUserId: ctx.user.id,
auditIp: ctx.ip,
auditUserAgent: ctx.userAgent,
})
}
// Audit log
await logAudit({
prisma: ctx.prisma,
@@ -611,12 +639,13 @@ export const evaluationRouter = router({
assignmentId: coi.assignmentId,
userId: coi.userId,
projectId: coi.projectId,
reassignedTo: reassignment?.newJurorId,
},
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
})
return coi
return { ...coi, reassignment }
}),
// =========================================================================