Rounds overhaul: full CRUD submission windows, scheduling UI, analytics, design refresh
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m40s

- Fix special award FK crash: replace 4x raw auditLog.create with logAudit() helper
- Add updateSubmissionWindow + deleteSubmissionWindow mutations to round router
- Add per-round analytics (_count, juryGroup) to competition.getById
- Remove redundant acceptedCategories from intake config
- Rewrite submission window manager with full CRUD, all fields, date pickers
- Add round scheduling card (open/close dates) to round detail page
- Add project count, assignment count, jury group to round list cards
- Visual redesign: pipeline view, brand colors, progress bars, enhanced cards

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 07:07:09 +01:00
parent 2fb26d4734
commit f572336781
8 changed files with 1208 additions and 414 deletions

View File

@@ -293,6 +293,79 @@ export const roundRouter = router({
return window
}),
/**
* Update an existing submission window
*/
updateSubmissionWindow: adminProcedure
.input(
z.object({
id: z.string(),
name: z.string().min(1).max(255).optional(),
slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/).optional(),
roundNumber: z.number().int().min(1).optional(),
windowOpenAt: z.date().nullable().optional(),
windowCloseAt: z.date().nullable().optional(),
deadlinePolicy: z.enum(['HARD_DEADLINE', 'FLAG', 'GRACE']).optional(),
graceHours: z.number().int().min(0).nullable().optional(),
lockOnClose: z.boolean().optional(),
sortOrder: z.number().int().optional(),
})
)
.mutation(async ({ ctx, input }) => {
const { id, ...data } = input
const window = await ctx.prisma.$transaction(async (tx) => {
const updated = await tx.submissionWindow.update({
where: { id },
data,
})
await logAudit({
prisma: tx,
userId: ctx.user.id,
action: 'UPDATE',
entityType: 'SubmissionWindow',
entityId: id,
detailsJson: data,
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
})
return updated
})
return window
}),
/**
* Delete a submission window (only if no files uploaded)
*/
deleteSubmissionWindow: adminProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
// Check if window has uploaded files
const window = await ctx.prisma.submissionWindow.findUniqueOrThrow({
where: { id: input.id },
select: { id: true, name: true, _count: { select: { projectFiles: true } } },
})
if (window._count.projectFiles > 0) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: `Cannot delete window "${window.name}" — it has ${window._count.projectFiles} uploaded files. Remove files first.`,
})
}
await ctx.prisma.$transaction(async (tx) => {
await tx.submissionWindow.delete({ where: { id: input.id } })
await logAudit({
prisma: tx,
userId: ctx.user.id,
action: 'DELETE',
entityType: 'SubmissionWindow',
entityId: input.id,
detailsJson: { name: window.name },
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
})
})
return { success: true }
}),
/**
* Open a submission window
*/