Fix score distribution chart and add auto-assign for transfer dialog
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m30s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m30s
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,7 @@ import {
|
|||||||
History,
|
History,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
ArrowRightLeft,
|
ArrowRightLeft,
|
||||||
|
Sparkles,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
@@ -2651,6 +2652,48 @@ function TransferAssignmentsDialog({
|
|||||||
const [destOverrides, setDestOverrides] = useState<Record<string, string>>({})
|
const [destOverrides, setDestOverrides] = useState<Record<string, string>>({})
|
||||||
const [forceOverCap, setForceOverCap] = useState(false)
|
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<string, number>()
|
||||||
|
for (const c of candidateData.candidates) {
|
||||||
|
simLoad.set(c.userId, c.currentLoad)
|
||||||
|
}
|
||||||
|
|
||||||
|
const overrides: Record<string, string> = {}
|
||||||
|
|
||||||
|
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({
|
const transferMutation = trpc.assignment.transferAssignments.useMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
utils.assignment.listByStage.invalidate({ roundId })
|
utils.assignment.listByStage.invalidate({ roundId })
|
||||||
@@ -2799,6 +2842,12 @@ function TransferAssignmentsDialog({
|
|||||||
<p className="text-sm text-muted-foreground text-center py-6">No eligible candidates found.</p>
|
<p className="text-sm text-muted-foreground text-center py-6">No eligible candidates found.</p>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<Button variant="outline" size="sm" onClick={handleAutoAssign}>
|
||||||
|
<Sparkles className="mr-1.5 h-3.5 w-3.5" />
|
||||||
|
Auto-assign
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<div className="space-y-2 max-h-[350px] overflow-y-auto">
|
<div className="space-y-2 max-h-[350px] overflow-y-auto">
|
||||||
{candidateData.assignments.filter((a) => a.movable).map((assignment) => {
|
{candidateData.assignments.filter((a) => a.movable).map((assignment) => {
|
||||||
const currentDest = destOverrides[assignment.id] ||
|
const currentDest = destOverrides[assignment.id] ||
|
||||||
|
|||||||
Reference in New Issue
Block a user