From 09cc49d9208a7009b5d38fca2978b80155e8fcd1 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 21 Feb 2026 20:16:15 +0100 Subject: [PATCH] Fix score distribution chart and add auto-assign for transfer dialog - Fix bar chart CSS: percentage heights now resolve correctly with flex-1 and absolute bottom-anchored bars - Add Auto-assign button to transfer assignments dialog that distributes projects across eligible jurors balanced by load, preferring jurors who haven't completed all evaluations and are under their cap Co-Authored-By: Claude Opus 4.6 --- .../(admin)/admin/rounds/[roundId]/page.tsx | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/app/(admin)/admin/rounds/[roundId]/page.tsx b/src/app/(admin)/admin/rounds/[roundId]/page.tsx index 2c0aa2b..06b397c 100644 --- a/src/app/(admin)/admin/rounds/[roundId]/page.tsx +++ b/src/app/(admin)/admin/rounds/[roundId]/page.tsx @@ -89,6 +89,7 @@ import { History, ChevronRight, ArrowRightLeft, + Sparkles, } from 'lucide-react' import { Command, @@ -2651,6 +2652,48 @@ function TransferAssignmentsDialog({ const [destOverrides, setDestOverrides] = useState>({}) const [forceOverCap, setForceOverCap] = useState(false) + // Auto-assign: distribute assignments across eligible candidates balanced by load + const handleAutoAssign = () => { + if (!candidateData) return + const movable = candidateData.assignments.filter((a) => a.movable) + if (movable.length === 0) return + + // Simulate load starting from each candidate's current load + const simLoad = new Map() + for (const c of candidateData.candidates) { + simLoad.set(c.userId, c.currentLoad) + } + + const overrides: Record = {} + + for (const assignment of movable) { + const eligible = candidateData.candidates + .filter((c) => c.eligibleProjectIds.includes(assignment.projectId)) + + if (eligible.length === 0) continue + + // Sort: prefer not-all-completed, then under cap, then lowest simulated load + const sorted = [...eligible].sort((a, b) => { + // Prefer jurors who haven't completed all evaluations + if (a.allCompleted !== b.allCompleted) return a.allCompleted ? 1 : -1 + const loadA = simLoad.get(a.userId) ?? 0 + const loadB = simLoad.get(b.userId) ?? 0 + // Prefer jurors under their cap + const overCapA = loadA >= a.cap ? 1 : 0 + const overCapB = loadB >= b.cap ? 1 : 0 + if (overCapA !== overCapB) return overCapA - overCapB + // Then pick the least loaded + return loadA - loadB + }) + + const best = sorted[0] + overrides[assignment.id] = best.userId + simLoad.set(best.userId, (simLoad.get(best.userId) ?? 0) + 1) + } + + setDestOverrides(overrides) + } + const transferMutation = trpc.assignment.transferAssignments.useMutation({ onSuccess: (data) => { utils.assignment.listByStage.invalidate({ roundId }) @@ -2799,6 +2842,12 @@ function TransferAssignmentsDialog({

No eligible candidates found.

) : ( <> +
+ +
{candidateData.assignments.filter((a) => a.movable).map((assignment) => { const currentDest = destOverrides[assignment.id] ||