feat: add juror progress dashboard with evaluation.getMyProgress query

- Add getMyProgress juryProcedure query to evaluationRouter: fetches all
  assignments for the current juror in a round, tallies completed/total,
  advance yes/no counts, per-project numeric scores and averages
- Create JurorProgressDashboard client component with progress bar, advance
  badge summary, and collapsible per-submission score table
- Wire dashboard into jury round page, gated by configJson.showJurorProgressDashboard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 15:15:08 +01:00
parent 2a61aa8e08
commit 79bd4dbae7
3 changed files with 221 additions and 0 deletions

View File

@@ -1588,4 +1588,100 @@ export const evaluationRouter = router({
orderBy: { updatedAt: 'desc' },
})
}),
/**
* Get the current juror's progress for a round
*/
getMyProgress: juryProcedure
.input(z.object({ roundId: z.string() }))
.query(async ({ ctx, input }) => {
const { roundId } = input
const userId = ctx.user.id
// Get all assignments for this juror in this round
const assignments = await ctx.prisma.assignment.findMany({
where: { roundId, userId },
include: {
project: { select: { id: true, title: true } },
evaluation: {
include: { form: { select: { criteriaJson: true } } },
},
},
})
const total = assignments.length
let completed = 0
let advanceYes = 0
let advanceNo = 0
const submissions: Array<{
projectId: string
projectName: string
submittedAt: Date | null
advanceDecision: boolean | null
criterionScores: Array<{ label: string; value: number }>
numericAverage: number | null
}> = []
for (const a of assignments) {
const ev = a.evaluation
if (!ev || ev.status !== 'SUBMITTED') continue
completed++
const criteria = (ev.form?.criteriaJson ?? []) as Array<{
id: string; label: string; type?: string; weight?: number
}>
const scores = (ev.criterionScoresJson ?? {}) as Record<string, unknown>
// Find the advance criterion
const advanceCriterion = criteria.find((c) => c.type === 'advance')
let advanceDecision: boolean | null = null
if (advanceCriterion) {
const val = scores[advanceCriterion.id]
if (typeof val === 'boolean') {
advanceDecision = val
if (val) advanceYes++
else advanceNo++
}
}
// Collect numeric criterion scores
const numericScores: Array<{ label: string; value: number }> = []
for (const c of criteria) {
if (c.type === 'numeric' || (!c.type && c.weight !== undefined)) {
const val = scores[c.id]
if (typeof val === 'number') {
numericScores.push({ label: c.label, value: val })
}
}
}
const numericAverage = numericScores.length > 0
? Math.round((numericScores.reduce((sum, s) => sum + s.value, 0) / numericScores.length) * 10) / 10
: null
submissions.push({
projectId: a.project.id,
projectName: a.project.title,
submittedAt: ev.submittedAt,
advanceDecision,
criterionScores: numericScores,
numericAverage,
})
}
// Sort by most recent first
submissions.sort((a, b) => {
if (!a.submittedAt) return 1
if (!b.submittedAt) return -1
return b.submittedAt.getTime() - a.submittedAt.getTime()
})
return {
total,
completed,
advanceCounts: { yes: advanceYes, no: advanceNo },
submissions,
}
}),
})