Fix reassignment scoping bug + add reassignment history
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled

Bug fix: reassignDroppedJuror, reassignAfterCOI, and getSuggestions all
fell back to querying ALL JURY_MEMBER users globally when the round had
no juryGroupId. This caused projects to be assigned to jurors who are no
longer active in the jury pool. Now scopes to jury group members when
available, otherwise to jurors already assigned to the round.

Also adds getSuggestions jury group scoping (matching runAIAssignmentJob).

New feature: Reassignment History panel on admin round page (collapsible)
shows per-project detail of where dropped/COI-reassigned projects went.
Reconstructs retroactive data from audit log timestamps + MANUAL
assignments for pre-fix entries. Future entries log full move details.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt
2026-02-20 14:18:49 +01:00
parent 0607d79484
commit 0d0571ebf2
2 changed files with 343 additions and 12 deletions

View File

@@ -86,6 +86,8 @@ import {
Eye,
Pencil,
Mail,
History,
ChevronRight,
} from 'lucide-react'
import {
Command,
@@ -1920,6 +1922,9 @@ export default function RoundDetailPage() {
<ScoreDistribution roundId={roundId} />
</div>
{/* Reassignment History (collapsible) */}
<ReassignmentHistory roundId={roundId} />
{/* Card 2: Assignments — with action buttons in header */}
<Card>
<CardHeader>
@@ -2487,6 +2492,106 @@ function JuryProgressTable({ roundId }: { roundId: string }) {
)
}
// ── Reassignment History ─────────────────────────────────────────────────
function ReassignmentHistory({ roundId }: { roundId: string }) {
const [expanded, setExpanded] = useState(false)
const { data: events, isLoading } = trpc.assignment.getReassignmentHistory.useQuery(
{ roundId },
{ enabled: expanded },
)
return (
<Card>
<CardHeader
className="cursor-pointer select-none"
onClick={() => setExpanded(!expanded)}
>
<CardTitle className="text-base flex items-center gap-2">
<History className="h-4 w-4" />
Reassignment History
<ChevronRight className={cn('h-4 w-4 ml-auto transition-transform', expanded && 'rotate-90')} />
</CardTitle>
<CardDescription>Juror dropout and COI reassignment audit trail</CardDescription>
</CardHeader>
{expanded && (
<CardContent>
{isLoading ? (
<div className="space-y-3">
{[1, 2].map((i) => <Skeleton key={i} className="h-16 w-full" />)}
</div>
) : !events || events.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-6">
No reassignment events for this round
</p>
) : (
<div className="space-y-4 max-h-[500px] overflow-y-auto">
{events.map((event) => (
<div key={event.id} className="border rounded-lg p-3 space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Badge variant={event.type === 'DROPOUT' ? 'destructive' : 'secondary'}>
{event.type === 'DROPOUT' ? 'Juror Dropout' : 'COI Reassignment'}
</Badge>
<span className="text-sm font-medium">
{event.droppedJuror.name}
</span>
</div>
<span className="text-xs text-muted-foreground">
{new Date(event.timestamp).toLocaleString()}
</span>
</div>
<p className="text-xs text-muted-foreground">
By {event.performedBy.name || event.performedBy.email} {event.movedCount} project(s) reassigned
{event.failedCount > 0 && `, ${event.failedCount} failed`}
</p>
{event.moves.length > 0 && (
<div className="mt-2">
<table className="w-full text-xs">
<thead>
<tr className="text-muted-foreground border-b">
<th className="text-left py-1 font-medium">Project</th>
<th className="text-left py-1 font-medium">Reassigned To</th>
</tr>
</thead>
<tbody>
{event.moves.map((move, i) => (
<tr key={i} className="border-b last:border-0">
<td className="py-1.5 pr-2 max-w-[250px] truncate">
{move.projectTitle}
</td>
<td className="py-1.5 font-medium">
{move.newJurorName}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{event.failedProjects.length > 0 && (
<div className="mt-1">
<p className="text-xs font-medium text-destructive">Could not reassign:</p>
<ul className="text-xs text-muted-foreground list-disc list-inside">
{event.failedProjects.map((p, i) => (
<li key={i}>{p}</li>
))}
</ul>
</div>
)}
</div>
))}
</div>
)}
</CardContent>
)}
</Card>
)
}
// ── Score Distribution ───────────────────────────────────────────────────
function ScoreDistribution({ roundId }: { roundId: string }) {