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:
@@ -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,
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user