diff --git a/src/components/admin/round/ranking-dashboard.tsx b/src/components/admin/round/ranking-dashboard.tsx index b0429d2..80cfe9e 100644 --- a/src/components/admin/round/ranking-dashboard.tsx +++ b/src/components/admin/round/ranking-dashboard.tsx @@ -390,8 +390,10 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran }, [projectStates]) // ─── localOrder init (once, with useRef guard) ──────────────────────────── + // Wait for evalScores too — the initial sort uses balanced (juror-corrected) + // averages, so we can't initialize until those are loaded. useEffect(() => { - if (!initialized.current && snapshot) { + if (!initialized.current && snapshot && evalScores) { const startup = (snapshot.startupRankingJson ?? []) as unknown as RankedProjectEntry[] const concept = (snapshot.conceptRankingJson ?? []) as unknown as RankedProjectEntry[] @@ -407,14 +409,18 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran const dedupedStartup = dedup(startup) const dedupedConcept = dedup(concept) - // Sort by avgGlobalScore descending (the metric displayed to the admin), with - // compositeScore as tiebreaker. This ensures the visible ordering matches the - // numbers on screen AND the threshold cutoff line lands correctly (it checks - // avgGlobalScore, so the list must be sorted by that same metric). + // Sort by balanced (juror-corrected) score descending, falling back to raw + // avgGlobalScore when no balanced score is available, then compositeScore as + // a final tiebreaker. The threshold cutoff line uses the same metric so the + // cutoff lands in the correct spot regardless of which score type is used. + const scoreFor = (projectId: string, raw: number | null | undefined) => + evalScores.balanced[projectId]?.balancedAverage ?? raw ?? 0 dedupedStartup.sort((a, b) => - (b.avgGlobalScore ?? 0) - (a.avgGlobalScore ?? 0) || b.compositeScore - a.compositeScore) + scoreFor(b.projectId, b.avgGlobalScore) - scoreFor(a.projectId, a.avgGlobalScore) + || b.compositeScore - a.compositeScore) dedupedConcept.sort((a, b) => - (b.avgGlobalScore ?? 0) - (a.avgGlobalScore ?? 0) || b.compositeScore - a.compositeScore) + scoreFor(b.projectId, b.avgGlobalScore) - scoreFor(a.projectId, a.avgGlobalScore) + || b.compositeScore - a.compositeScore) // Track original order for override detection (same effect = always in sync) const order: Record = {} @@ -455,7 +461,7 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran initialized.current = true } - }, [snapshot]) + }, [snapshot, evalScores]) // ─── numericCriteria from eval form ───────────────────────────────────── const numericCriteria = useMemo(() => { @@ -864,14 +870,19 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran : (evalConfig?.conceptAdvanceCount ?? 0)) const threshold = evalConfig?.advanceScoreThreshold ?? 0 + // Effective ranking score = balanced (juror-corrected) average, + // falling back to raw avgGlobalScore. Both the sort and the + // threshold check use this same value so the cutoff lands in + // the right spot. + const effectiveScore = (id: string) => { + const e = rankingMap.get(id) + return evalScores?.balanced[id]?.balancedAverage ?? e?.avgGlobalScore ?? 0 + } + let cutoffIndex = -1 if (isThresholdMode) { // Find the FIRST project that does NOT meet the threshold — cutoff goes before it. - // Works correctly because localOrder is sorted by avgGlobalScore (the same metric). - const firstFailIdx = localOrder[category].findIndex((id) => { - const e = rankingMap.get(id) - return (e?.avgGlobalScore ?? 0) < threshold - }) + const firstFailIdx = localOrder[category].findIndex((id) => effectiveScore(id) < threshold) if (firstFailIdx === -1) { // All meet threshold — cutoff after the last one cutoffIndex = localOrder[category].length - 1 @@ -902,10 +913,9 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
{localOrder[category].map((projectId, index) => { - const entry = rankingMap.get(projectId) - const projectAvg = entry?.avgGlobalScore ?? 0 + const projectScore = effectiveScore(projectId) const isAdvancing = isThresholdMode - ? projectAvg >= threshold + ? projectScore >= threshold : (advanceCount > 0 && index < advanceCount) const isCutoffRow = cutoffIndex >= 0 && index === cutoffIndex