Fix reassignment scoping bug + add reassignment history
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
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:
@@ -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 }) {
|
||||
|
||||
Reference in New Issue
Block a user