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
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:
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user