Fix criteria validation using wrong form + fix reports page null crash
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m38s

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 <noreply@anthropic.com>
This commit is contained in:
Matt
2026-02-20 13:16:09 +01:00
parent bf02684736
commit 4519bc6080
2 changed files with 7 additions and 6 deletions

View File

@@ -77,7 +77,7 @@ function OverviewTab({ selectedValue }: { selectedValue: string | null }) {
const { data: programs, isLoading } = trpc.program.list.useQuery({ includeStages: true }) const { data: programs, isLoading } = trpc.program.list.useQuery({ includeStages: true })
const stages = programs?.flatMap(p => 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, ...s,
programName: `${p.year} Edition`, programName: `${p.year} Edition`,
})) }))
@@ -598,7 +598,7 @@ export default function ObserverReportsPage() {
const { data: programs, isLoading: stagesLoading } = trpc.program.list.useQuery({ includeStages: true }) const { data: programs, isLoading: stagesLoading } = trpc.program.list.useQuery({ includeStages: true })
const stages = programs?.flatMap(p => 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, ...s,
programId: p.id, programId: p.id,
programName: `${p.year} Edition`, programName: `${p.year} Edition`,

View File

@@ -146,6 +146,7 @@ export const evaluationRouter = router({
where: { id }, where: { id },
include: { include: {
assignment: true, assignment: true,
form: { select: { criteriaJson: true } },
}, },
}) })
@@ -231,11 +232,11 @@ export const evaluationRouter = router({
} }
// Fix 5: requireAllCriteriaScored validation // 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') { if (config.requireAllCriteriaScored && scoringMode === 'criteria') {
const evalForm = await ctx.prisma.evaluationForm.findFirst({ const evalForm = evaluation.form
where: { roundId: round.id, isActive: true },
select: { criteriaJson: true },
})
if (evalForm?.criteriaJson) { if (evalForm?.criteriaJson) {
const criteria = evalForm.criteriaJson as Array<{ id: string; label?: string; type?: string; required?: boolean }> const criteria = evalForm.criteriaJson as Array<{ id: string; label?: string; type?: string; required?: boolean }>
const scorableCriteria = criteria.filter( const scorableCriteria = criteria.filter(