Compare commits
2 Commits
5a609457c2
...
09cc49d920
| Author | SHA1 | Date | |
|---|---|---|---|
| 09cc49d920 | |||
| 351d8144d9 |
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user