diff --git a/src/server/routers/analytics.ts b/src/server/routers/analytics.ts index 46fd950..1c8aa90 100644 --- a/src/server/routers/analytics.ts +++ b/src/server/routers/analytics.ts @@ -1368,7 +1368,7 @@ export const analyticsRouter = router({ * Read-only combined endpoint to avoid multiple round-trips. */ getProjectDetail: observerProcedure - .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({ @@ -1417,7 +1417,10 @@ export const analyticsRouter = 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 new file mode 100644 index 0000000..7e41684 --- /dev/null +++ b/tests/unit/juror-balance-round-scoping.test.ts @@ -0,0 +1,72 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { prisma, createCaller } from '../setup' +import { + createTestUser, createTestProgram, createTestCompetition, createTestRound, + createTestProject, createTestProjectRoundState, createTestAssignment, + createTestEvaluation, createTestEvaluationForm, cleanupTestData, uid, +} from '../helpers' +import { analyticsRouter } from '../../src/server/routers/analytics' + +describe('analytics.getProjectDetail round scoping', () => { + let programId: string + let admin: { id: string; email: string; role: 'SUPER_ADMIN' } + let projectId: string + let roundAId: string + let roundBId: string + const userIds: string[] = [] + + beforeAll(async () => { + const program = await createTestProgram({ name: `bal-scope-${uid()}` }) + programId = program.id + const competition = await createTestCompetition(programId) + const roundA = await createTestRound(competition.id, { name: 'Round A', sortOrder: 0, status: 'ROUND_CLOSED' }) + const roundB = await createTestRound(competition.id, { name: 'Round B', sortOrder: 1, status: 'ROUND_ACTIVE' }) + roundAId = roundA.id + roundBId = roundB.id + + const formA = await createTestEvaluationForm(roundA.id) + const formB = await createTestEvaluationForm(roundB.id) + + const project = await createTestProject(programId) + projectId = project.id + await createTestProjectRoundState(projectId, roundA.id, { state: 'PASSED' }) + await createTestProjectRoundState(projectId, roundB.id, { state: 'IN_PROGRESS' }) + + // 2 evaluations on Round A: 7.0, 8.0 (mean 7.5) + for (const score of [7, 8]) { + const juror = await createTestUser('JURY_MEMBER') + userIds.push(juror.id) + const a = await createTestAssignment(juror.id, projectId, roundA.id) + await createTestEvaluation(a.id, formA.id, { status: 'SUBMITTED', globalScore: score, submittedAt: new Date() }) + } + // 3 evaluations on Round B: 9.0, 8.0, 8.0 (mean 8.333…) + for (const score of [9, 8, 8]) { + const juror = await createTestUser('JURY_MEMBER') + userIds.push(juror.id) + const a = await createTestAssignment(juror.id, projectId, roundB.id) + await createTestEvaluation(a.id, formB.id, { status: 'SUBMITTED', globalScore: score, submittedAt: new Date() }) + } + + const adminUser = await createTestUser('SUPER_ADMIN') + userIds.push(adminUser.id) + admin = { id: adminUser.id, email: adminUser.email, role: 'SUPER_ADMIN' } + }) + + afterAll(async () => { + await cleanupTestData(programId, userIds) + }) + + it('returns only round-B stats when roundId=roundB is passed', async () => { + const caller = createCaller(analyticsRouter, admin) + const result = await caller.getProjectDetail({ id: projectId, roundId: roundBId }) + expect(result.stats).not.toBeNull() + expect(result.stats!.totalEvaluations).toBe(3) + expect(result.stats!.averageGlobalScore).toBeCloseTo(8.333, 2) + }) + + it('returns aggregated stats across all rounds when roundId is omitted', async () => { + const caller = createCaller(analyticsRouter, admin) + const result = await caller.getProjectDetail({ id: projectId }) + expect(result.stats!.totalEvaluations).toBe(5) + }) +})