From 4519bc608039c2a7225c93540ef768cb3a609572 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 20 Feb 2026 13:16:09 +0100 Subject: [PATCH] Fix criteria validation using wrong form + fix reports page null crash 1. Evaluation submit: The requireAllCriteriaScored validation was querying findFirst({ roundId, isActive: true }) to get the form criteria, instead of using the evaluation's stored formId. If an admin ever re-saved the evaluation form (creating a new version with new criterion IDs), jurors who started evaluating before the re-save had scores keyed to old IDs that didn't match the new form. Now uses evaluation.form (the form assigned at start time). 2. Observer reports page: Two .map() calls on p.stages lacked null guards, causing "Cannot read properties of null (reading 'map')" crash. Added (p.stages || []) guards matching the pattern already used in CrossStageTab. Co-Authored-By: Claude Opus 4.6 --- src/app/(observer)/observer/reports/page.tsx | 4 ++-- src/server/routers/evaluation.ts | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/app/(observer)/observer/reports/page.tsx b/src/app/(observer)/observer/reports/page.tsx index 2ab0279..554b2b6 100644 --- a/src/app/(observer)/observer/reports/page.tsx +++ b/src/app/(observer)/observer/reports/page.tsx @@ -77,7 +77,7 @@ function OverviewTab({ selectedValue }: { selectedValue: string | null }) { const { data: programs, isLoading } = trpc.program.list.useQuery({ includeStages: true }) const stages = programs?.flatMap(p => - (p.stages as { id: string; name: string; status: string; roundType: string; windowCloseAt: Date | null; _count: { projects: number; assignments: number } }[]).map(s => ({ + ((p.stages || []) as { id: string; name: string; status: string; roundType: string; windowCloseAt: Date | null; _count: { projects: number; assignments: number } }[]).map(s => ({ ...s, programName: `${p.year} Edition`, })) @@ -598,7 +598,7 @@ export default function ObserverReportsPage() { const { data: programs, isLoading: stagesLoading } = trpc.program.list.useQuery({ includeStages: true }) const stages = programs?.flatMap(p => - (p.stages as { id: string; name: string; status: string; roundType: string; windowCloseAt: Date | null; _count: { projects: number; assignments: number } }[]).map(s => ({ + ((p.stages || []) as { id: string; name: string; status: string; roundType: string; windowCloseAt: Date | null; _count: { projects: number; assignments: number } }[]).map(s => ({ ...s, programId: p.id, programName: `${p.year} Edition`, diff --git a/src/server/routers/evaluation.ts b/src/server/routers/evaluation.ts index fc241b9..e4c1686 100644 --- a/src/server/routers/evaluation.ts +++ b/src/server/routers/evaluation.ts @@ -146,6 +146,7 @@ export const evaluationRouter = router({ where: { id }, include: { assignment: true, + form: { select: { criteriaJson: true } }, }, }) @@ -231,11 +232,11 @@ export const evaluationRouter = router({ } // Fix 5: requireAllCriteriaScored validation + // Use the form the juror was assigned (evaluation.form), NOT the current active form. + // If the admin re-saves the form, criterion IDs change — jurors who started before + // the re-save would have scores keyed to old IDs that don't match the new form. if (config.requireAllCriteriaScored && scoringMode === 'criteria') { - const evalForm = await ctx.prisma.evaluationForm.findFirst({ - where: { roundId: round.id, isActive: true }, - select: { criteriaJson: true }, - }) + const evalForm = evaluation.form if (evalForm?.criteriaJson) { const criteria = evalForm.criteriaJson as Array<{ id: string; label?: string; type?: string; required?: boolean }> const scorableCriteria = criteria.filter(