Compare commits

...

2 Commits

Author SHA1 Message Date
09cc49d920 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
- 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>
2026-02-21 20:16:15 +01:00
351d8144d9 Fix score distribution chart bars not rendering in admin round page
CSS percentage heights require parent with resolved height. Changed layout
to use flex-1 with absolute bottom-anchored bars instead of percentage-height containers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 19:58:13 +01:00

View File

@@ -89,6 +89,7 @@ import {
History,
ChevronRight,
ArrowRightLeft,
Sparkles,
} from 'lucide-react'
import {
Command,
@@ -2651,6 +2652,48 @@ function TransferAssignmentsDialog({
const [destOverrides, setDestOverrides] = useState<Record<string, string>>({})
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({
onSuccess: (data) => {
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>
) : (
<>
<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">
{candidateData.assignments.filter((a) => a.movable).map((assignment) => {
const currentDest = destOverrides[assignment.id] ||
@@ -2997,19 +3046,19 @@ function ScoreDistribution({ roundId }: { roundId: string }) {
No evaluations submitted yet
</p>
) : (
<div className="flex items-end gap-1 h-32">
<div className="flex gap-1 h-32">
{dist.globalDistribution.map((bucket) => {
const heightPct = (bucket.count / maxCount) * 100
return (
<div key={bucket.score} className="flex-1 flex flex-col items-center gap-1">
<div key={bucket.score} className="flex-1 flex flex-col items-center gap-1 h-full">
<span className="text-[9px] text-muted-foreground">{bucket.count || ''}</span>
<div className="w-full relative rounded-t" style={{ height: `${Math.max(heightPct, 2)}%` }}>
<div className="w-full flex-1 relative">
<div className={cn(
'absolute inset-0 rounded-t transition-all',
'absolute inset-x-0 bottom-0 rounded-t transition-all',
bucket.score <= 3 ? 'bg-red-400' :
bucket.score <= 6 ? 'bg-amber-400' :
'bg-emerald-400',
)} />
)} style={{ height: `${Math.max(heightPct, 4)}%` }} />
</div>
<span className="text-[10px] text-muted-foreground">{bucket.score}</span>
</div>