feat: per-round balanced-scoring toggle in side sheet

A Switch at the top of the project side panel writes
useBalancedRanking onto Round.configJson via the existing round.update
mutation. The flip is shared across all viewers because the value
lives in the round's persisted config; hydration runs on every
roundData refetch so the UI converges quickly when another admin
flips it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-04-27 13:20:21 +02:00
parent 0680a5d601
commit 387f84c338

View File

@@ -37,6 +37,7 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Slider } from '@/components/ui/slider' import { Slider } from '@/components/ui/slider'
import { Switch } from '@/components/ui/switch'
import { import {
Collapsible, Collapsible,
CollapsibleContent, CollapsibleContent,
@@ -271,6 +272,7 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
const [localCriteriaText, setLocalCriteriaText] = useState<string>('') const [localCriteriaText, setLocalCriteriaText] = useState<string>('')
const [localScoreWeight, setLocalScoreWeight] = useState(5) const [localScoreWeight, setLocalScoreWeight] = useState(5)
const [localPassRateWeight, setLocalPassRateWeight] = useState(5) const [localPassRateWeight, setLocalPassRateWeight] = useState(5)
const [useBalanced, setUseBalanced] = useState(true)
const weightsInitialized = useRef(false) const weightsInitialized = useRef(false)
// ─── Sensors ────────────────────────────────────────────────────────────── // ─── Sensors ──────────────────────────────────────────────────────────────
@@ -471,18 +473,32 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
}, [evalForm]) }, [evalForm])
// ─── Init local weights + criteriaText from round config ────────────────── // ─── Init local weights + criteriaText from round config ──────────────────
// useBalanced is hydrated on every roundData refetch (it has its own toggle
// that persists immediately), so it sits outside the once-only guard.
useEffect(() => { useEffect(() => {
if (!weightsInitialized.current && roundData?.configJson) { if (!roundData?.configJson) return
const cfg = roundData.configJson as Record<string, unknown> const cfg = roundData.configJson as Record<string, unknown>
const saved = (cfg.criteriaWeights ?? {}) as Record<string, number> setUseBalanced((cfg.useBalancedRanking as boolean | undefined) ?? true)
setLocalWeights(saved) if (weightsInitialized.current) return
setLocalCriteriaText((cfg.rankingCriteria as string) ?? '') const saved = (cfg.criteriaWeights ?? {}) as Record<string, number>
setLocalScoreWeight((cfg.scoreWeight as number) ?? 5) setLocalWeights(saved)
setLocalPassRateWeight((cfg.passRateWeight as number) ?? 5) setLocalCriteriaText((cfg.rankingCriteria as string) ?? '')
weightsInitialized.current = true setLocalScoreWeight((cfg.scoreWeight as number) ?? 5)
} setLocalPassRateWeight((cfg.passRateWeight as number) ?? 5)
weightsInitialized.current = true
}, [roundData]) }, [roundData])
// ─── Persist the balanced-ranking toggle immediately ─────────────────────
const persistUseBalanced = (next: boolean) => {
setUseBalanced(next)
if (!roundData?.configJson) return
const cfg = roundData.configJson as Record<string, unknown>
updateRoundMutation.mutate({
id: roundId,
configJson: { ...cfg, useBalancedRanking: next },
})
}
// ─── Save weights + criteria text to round config ───────────────────────── // ─── Save weights + criteria text to round config ─────────────────────────
const saveRankingConfig = () => { const saveRankingConfig = () => {
if (!roundData?.configJson) return if (!roundData?.configJson) return
@@ -1001,6 +1017,16 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
</div> </div>
) : projectDetail ? ( ) : projectDetail ? (
<div className="mt-6 space-y-6"> <div className="mt-6 space-y-6">
{/* Balanced-ranking toggle (per-round; persists across viewers) */}
<div className="flex items-center justify-between rounded-lg border p-3">
<div className="flex flex-col">
<span className="text-sm font-medium">Use balanced scoring for ranking</span>
<span className="text-xs text-muted-foreground">
Corrects for per-juror grading style. Off uses raw averages.
</span>
</div>
<Switch checked={useBalanced} onCheckedChange={persistUseBalanced} />
</div>
{/* Stats summary */} {/* Stats summary */}
{projectDetail.stats && ( {projectDetail.stats && (
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-3">