Consolidated round management, AI filtering enhancements, MinIO storage restructure
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
- Fix STAGE_ACTIVE bug in assignment router (now ROUND_ACTIVE)
- Add evaluation form CRUD (getForm + upsertForm endpoints)
- Add advanceProjects mutation for manual project advancement
- Rewrite round detail page: 7-tab consolidated interface
- Add filtering rules UI with full CRUD (field-based, document check, AI screening)
- Add pageCount field to ProjectFile for document page limit filtering
- Enhance AI filtering: per-file page limits, category/region-aware guidelines
- Restructure MinIO paths: {ProjectName}/{RoundName}/{timestamp}-{file}
- Update dashboard and pool page links from /admin/competitions to /admin/rounds
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1019,6 +1019,129 @@ export const evaluationRouter = router({
|
||||
return discussion
|
||||
}),
|
||||
|
||||
// =========================================================================
|
||||
// Evaluation Form CRUD (Admin)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Get active evaluation form for a round (admin view with full details)
|
||||
*/
|
||||
getForm: adminProcedure
|
||||
.input(z.object({ roundId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const form = await ctx.prisma.evaluationForm.findFirst({
|
||||
where: { roundId: input.roundId, isActive: true },
|
||||
})
|
||||
|
||||
if (!form) return null
|
||||
|
||||
return {
|
||||
id: form.id,
|
||||
roundId: form.roundId,
|
||||
version: form.version,
|
||||
isActive: form.isActive,
|
||||
criteriaJson: form.criteriaJson as Array<{
|
||||
id: string
|
||||
label: string
|
||||
description?: string
|
||||
weight?: number
|
||||
minScore?: number
|
||||
maxScore?: number
|
||||
}>,
|
||||
scalesJson: form.scalesJson as Record<string, unknown> | null,
|
||||
createdAt: form.createdAt,
|
||||
updatedAt: form.updatedAt,
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Create or update the evaluation form for a round.
|
||||
* Deactivates any existing active form and creates a new versioned one.
|
||||
*/
|
||||
upsertForm: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
roundId: z.string(),
|
||||
criteria: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
label: z.string().min(1).max(255),
|
||||
description: z.string().max(2000).optional(),
|
||||
weight: z.number().min(0).max(100).optional(),
|
||||
minScore: z.number().int().min(0).optional(),
|
||||
maxScore: z.number().int().min(1).optional(),
|
||||
})
|
||||
).min(1),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { roundId, criteria } = input
|
||||
|
||||
// Verify round exists
|
||||
await ctx.prisma.round.findUniqueOrThrow({ where: { id: roundId } })
|
||||
|
||||
// Get current max version for this round
|
||||
const latestForm = await ctx.prisma.evaluationForm.findFirst({
|
||||
where: { roundId },
|
||||
orderBy: { version: 'desc' },
|
||||
select: { version: true },
|
||||
})
|
||||
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,
|
||||
}))
|
||||
|
||||
// Auto-generate scalesJson from criteria min/max ranges
|
||||
const scaleSet = new Set(criteriaJson.map((c) => c.scale))
|
||||
const scalesJson: Record<string, { min: number; max: number }> = {}
|
||||
for (const scale of scaleSet) {
|
||||
const [min, max] = scale.split('-').map(Number)
|
||||
scalesJson[scale] = { min, max }
|
||||
}
|
||||
|
||||
// Transaction: deactivate old → create new
|
||||
const form = await ctx.prisma.$transaction(async (tx) => {
|
||||
await tx.evaluationForm.updateMany({
|
||||
where: { roundId, isActive: true },
|
||||
data: { isActive: false },
|
||||
})
|
||||
|
||||
return tx.evaluationForm.create({
|
||||
data: {
|
||||
roundId,
|
||||
version: nextVersion,
|
||||
criteriaJson,
|
||||
scalesJson,
|
||||
isActive: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
await logAudit({
|
||||
prisma: ctx.prisma,
|
||||
userId: ctx.user.id,
|
||||
action: 'UPSERT_EVALUATION_FORM',
|
||||
entityType: 'EvaluationForm',
|
||||
entityId: form.id,
|
||||
detailsJson: {
|
||||
roundId,
|
||||
version: nextVersion,
|
||||
criteriaCount: criteria.length,
|
||||
},
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
})
|
||||
|
||||
return form
|
||||
}),
|
||||
|
||||
// =========================================================================
|
||||
// Phase 4: Stage-scoped evaluation procedures
|
||||
// =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user