From 9a2c10a6f89c95611724f88f8d0be8c13c18f099 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 27 Apr 2026 13:15:47 +0200 Subject: [PATCH] fix: scope admin ranking dashboard side-sheet stats to current round The admin dashboard fetches its side-sheet detail from project.getFullDetail (not analytics.getProjectDetail as the audit assumed), and that procedure had the same cross-round contamination bug. Add an optional roundId to its input, filter the SUBMITTED-evaluations query when provided, and pass roundId from the dashboard's useQuery so the Avg Score / Pass Rate / Evaluators card now matches the per-juror list rendered below it. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/admin/round/ranking-dashboard.tsx | 2 +- src/server/routers/project.ts | 7 +++++-- tests/unit/juror-balance-round-scoping.test.ts | 10 ++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/components/admin/round/ranking-dashboard.tsx b/src/components/admin/round/ranking-dashboard.tsx index 80cfe9e..4655c4d 100644 --- a/src/components/admin/round/ranking-dashboard.tsx +++ b/src/components/admin/round/ranking-dashboard.tsx @@ -304,7 +304,7 @@ export function RankingDashboard({ competitionId: _competitionId, roundId }: Ran ) const { data: projectDetail, isLoading: detailLoading } = trpc.project.getFullDetail.useQuery( - { id: selectedProjectId! }, + { id: selectedProjectId!, roundId }, { enabled: !!selectedProjectId }, ) diff --git a/src/server/routers/project.ts b/src/server/routers/project.ts index 58e7deb..a8c2339 100644 --- a/src/server/routers/project.ts +++ b/src/server/routers/project.ts @@ -1249,7 +1249,7 @@ export const projectRouter = router({ * Reduces client-side waterfall by combining project.get + assignment.listByProject + evaluation.getProjectStats. */ getFullDetail: adminProcedure - .input(z.object({ id: z.string() })) + .input(z.object({ id: z.string(), roundId: z.string().optional() })) .query(async ({ ctx, input }) => { const [projectRaw, projectTags, assignments, submittedEvaluations] = await Promise.all([ ctx.prisma.project.findUniqueOrThrow({ @@ -1297,7 +1297,10 @@ export const projectRouter = router({ ctx.prisma.evaluation.findMany({ where: { status: 'SUBMITTED', - assignment: { projectId: input.id }, + assignment: { + projectId: input.id, + ...(input.roundId ? { roundId: input.roundId } : {}), + }, }, }), ]) diff --git a/tests/unit/juror-balance-round-scoping.test.ts b/tests/unit/juror-balance-round-scoping.test.ts index 191e2ae..fdb3fea 100644 --- a/tests/unit/juror-balance-round-scoping.test.ts +++ b/tests/unit/juror-balance-round-scoping.test.ts @@ -6,6 +6,7 @@ import { createTestEvaluation, createTestEvaluationForm, cleanupTestData, uid, } from '../helpers' import { analyticsRouter } from '../../src/server/routers/analytics' +import { projectRouter } from '../../src/server/routers/project' describe('analytics.getProjectDetail round scoping', () => { let programId: string @@ -69,6 +70,15 @@ describe('analytics.getProjectDetail round scoping', () => { const result = await caller.getProjectDetail({ id: projectId }) expect(result.stats!.totalEvaluations).toBe(5) }) + + it('project.getFullDetail also scopes stats to roundId when provided', async () => { + const caller = createCaller(projectRouter, admin) + const scoped = await caller.getFullDetail({ id: projectId, roundId: roundBId }) + expect(scoped.stats!.totalEvaluations).toBe(3) + expect(scoped.stats!.averageGlobalScore).toBeCloseTo(8.333, 2) + const aggregate = await caller.getFullDetail({ id: projectId }) + expect(aggregate.stats!.totalEvaluations).toBe(5) + }) }) describe('analytics.getProjectRankings per-round z-context (edition mode)', () => {