Jury evaluation UX overhaul + admin review features
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m53s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m53s
- Fix project documents not displaying on jury project page (rewrote MultiWindowDocViewer to use file.listByProject) - Add working download/preview for project files via presigned URLs - Display project tags on jury project detail page - Add autosave for evaluation drafts (debounced 3s + save on unmount/beforeunload) - Support mixed criterion types: numeric scores, yes/no booleans, text responses, section headers - Replace inline criteria editor with rich EvaluationFormBuilder on admin round page - Remove COI dialog from evaluation page - Update AI summary service to handle boolean/text criteria (yes/no counts, text synthesis) - Update EvaluationSummaryCard to show boolean criteria bars and text responses - Add evaluation detail sheet on admin project page (click juror row to view full scores + feedback) - Add Recent Evaluations dashboard widget showing latest jury reviews Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1067,9 +1067,27 @@ export const evaluationRouter = router({
|
||||
id: z.string(),
|
||||
label: z.string().min(1).max(255),
|
||||
description: z.string().max(2000).optional(),
|
||||
type: z.enum(['numeric', 'text', 'boolean', 'section_header']).optional(),
|
||||
// Numeric fields
|
||||
weight: z.number().min(0).max(100).optional(),
|
||||
minScore: z.number().int().min(0).optional(),
|
||||
maxScore: z.number().int().min(1).optional(),
|
||||
scale: z.number().int().min(1).max(10).optional(),
|
||||
required: z.boolean().optional(),
|
||||
// Text fields
|
||||
maxLength: z.number().int().min(1).max(10000).optional(),
|
||||
placeholder: z.string().max(500).optional(),
|
||||
// Boolean fields
|
||||
trueLabel: z.string().max(100).optional(),
|
||||
falseLabel: z.string().max(100).optional(),
|
||||
// Conditional visibility
|
||||
condition: z.object({
|
||||
criterionId: z.string(),
|
||||
operator: z.enum(['equals', 'greaterThan', 'lessThan']),
|
||||
value: z.union([z.number(), z.string(), z.boolean()]),
|
||||
}).optional(),
|
||||
// Section grouping
|
||||
sectionId: z.string().optional(),
|
||||
})
|
||||
).min(1),
|
||||
})
|
||||
@@ -1088,18 +1106,46 @@ export const evaluationRouter = router({
|
||||
})
|
||||
const nextVersion = (latestForm?.version ?? 0) + 1
|
||||
|
||||
// Build criteriaJson with defaults
|
||||
const criteriaJson = criteria.map((c) => ({
|
||||
id: c.id,
|
||||
label: c.label,
|
||||
description: c.description || '',
|
||||
weight: c.weight ?? 1,
|
||||
scale: `${c.minScore ?? 1}-${c.maxScore ?? 10}`,
|
||||
required: true,
|
||||
}))
|
||||
// Build criteriaJson preserving all fields
|
||||
const criteriaJson = criteria.map((c) => {
|
||||
const type = c.type || 'numeric'
|
||||
const base = {
|
||||
id: c.id,
|
||||
label: c.label,
|
||||
description: c.description || '',
|
||||
type,
|
||||
required: c.required ?? (type !== 'section_header'),
|
||||
}
|
||||
|
||||
// Auto-generate scalesJson from criteria min/max ranges
|
||||
const scaleSet = new Set(criteriaJson.map((c) => c.scale))
|
||||
if (type === 'numeric') {
|
||||
const scaleVal = c.scale ?? 10
|
||||
return {
|
||||
...base,
|
||||
weight: c.weight ?? 1,
|
||||
scale: `${c.minScore ?? 1}-${c.maxScore ?? scaleVal}`,
|
||||
}
|
||||
}
|
||||
if (type === 'text') {
|
||||
return {
|
||||
...base,
|
||||
maxLength: c.maxLength ?? 1000,
|
||||
placeholder: c.placeholder || '',
|
||||
}
|
||||
}
|
||||
if (type === 'boolean') {
|
||||
return {
|
||||
...base,
|
||||
trueLabel: c.trueLabel || 'Yes',
|
||||
falseLabel: c.falseLabel || 'No',
|
||||
}
|
||||
}
|
||||
// section_header
|
||||
return base
|
||||
})
|
||||
|
||||
// Auto-generate scalesJson from numeric criteria
|
||||
const numericCriteria = criteriaJson.filter((c) => c.type === 'numeric')
|
||||
const scaleSet = new Set(numericCriteria.map((c) => (c as { scale: string }).scale))
|
||||
const scalesJson: Record<string, { min: number; max: number }> = {}
|
||||
for (const scale of scaleSet) {
|
||||
const [min, max] = scale.split('-').map(Number)
|
||||
|
||||
Reference in New Issue
Block a user