From aed5e078b375a1ee5f4517a56be16b296639d43b Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 27 Apr 2026 14:20:09 +0200 Subject: [PATCH] fix: resolve advance decision and surface country on rankings list The side panel only read Evaluation.binaryDecision, which is null when the round's evaluation form stores the 'proceed to next round' answer as a boolean criterion in criterionScoresJson (the current round's shape). project.getFullDetail now resolves a unified `decision` field per assignment using the same fallback pattern as the ranking router: prefer the column, fall back to a type='advance' (or legacy 'move to the next stage' boolean) criterion looked up by id in the active form. Also: project country in the rankings list now renders whenever it's present, not only when teamName is also set. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../admin/round/ranking-dashboard.tsx | 13 ++--- src/server/routers/project.ts | 52 ++++++++++++++++--- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/components/admin/round/ranking-dashboard.tsx b/src/components/admin/round/ranking-dashboard.tsx index f379afa..2528bab 100644 --- a/src/components/admin/round/ranking-dashboard.tsx +++ b/src/components/admin/round/ranking-dashboard.tsx @@ -169,10 +169,11 @@ function SortableProjectRow({

{projectInfo?.title ?? `Project …${projectId.slice(-6)}`}

- {projectInfo?.teamName && ( + {(projectInfo?.teamName || projectInfo?.country) && (

{projectInfo.teamName} - {projectInfo.country ? <> · : ''} + {projectInfo.teamName && projectInfo.country ? ' · ' : ''} + {projectInfo.country && }

)} @@ -1153,12 +1154,12 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran
{a.user?.name ?? a.user?.email ?? 'Unknown'}
- {a.evaluation?.binaryDecision != null && ( + {a.evaluation?.decision != null && ( - {a.evaluation.binaryDecision ? 'Yes' : 'No'} + {a.evaluation.decision ? 'Yes' : 'No'} )} Score: {a.evaluation?.globalScore?.toFixed(1) ?? '—'} diff --git a/src/server/routers/project.ts b/src/server/routers/project.ts index a8c2339..7e48465 100644 --- a/src/server/routers/project.ts +++ b/src/server/routers/project.ts @@ -1305,6 +1305,27 @@ export const projectRouter = router({ }), ]) + // Resolve the boolean "advance to next round" criterion id for each round + // so we can fall back to criterionScoresJson when binaryDecision is null. + // Mirrors ranking.ts logic: prefer type='advance', else legacy boolean + // criterion labeled "move to the next stage". + const roundIdsInUse = Array.from(new Set(assignments.map((a) => a.round.id))) + const advanceCriterionByRound = new Map() + if (roundIdsInUse.length > 0) { + const forms = await ctx.prisma.evaluationForm.findMany({ + where: { roundId: { in: roundIdsInUse }, isActive: true }, + select: { roundId: true, criteriaJson: true }, + }) + for (const form of forms) { + if (advanceCriterionByRound.has(form.roundId)) continue + const criteria = (form.criteriaJson as Array<{ id: string; type?: string; label?: string }> | null) ?? [] + const found = + criteria.find((c) => c.type === 'advance') ?? + criteria.find((c) => c.type === 'boolean' && c.label?.toLowerCase().includes('move to the next stage')) + advanceCriterionByRound.set(form.roundId, found?.id ?? null) + } + } + // Compute evaluation stats let stats = null if (submittedEvaluations.length > 0) { @@ -1355,13 +1376,30 @@ export const projectRouter = router({ })) ), Promise.all( - assignments.map(async (a) => ({ - ...a, - user: { - ...a.user, - avatarUrl: await getUserAvatarUrl(a.user.profileImageKey, a.user.profileImageProvider), - }, - })) + assignments.map(async (a) => { + // Resolve decision: column first, criterion fallback second. + let decision: boolean | null = a.evaluation?.binaryDecision ?? null + if (decision == null && a.evaluation) { + const advCritId = advanceCriterionByRound.get(a.round.id) ?? null + if (advCritId) { + const scores = a.evaluation.criterionScoresJson as Record | null + if (scores) { + const val = scores[advCritId] + if (typeof val === 'boolean') decision = val + else if (val === 'true') decision = true + else if (val === 'false') decision = false + } + } + } + return { + ...a, + user: { + ...a.user, + avatarUrl: await getUserAvatarUrl(a.user.profileImageKey, a.user.profileImageProvider), + }, + evaluation: a.evaluation ? { ...a.evaluation, decision } : null, + } + }) ), projectRaw.mentorAssignment ? (async () => ({