Round detail overhaul, file requirements, project management, audit log fix
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m32s

- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
  (PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 07:49:39 +01:00
parent f572336781
commit 7f334ed095
18 changed files with 3387 additions and 968 deletions

View File

@@ -54,39 +54,35 @@ export const roundRouter = router({
? validateRoundConfig(input.roundType, input.configJson)
: defaultRoundConfig(input.roundType)
const round = await ctx.prisma.$transaction(async (tx) => {
const created = await tx.round.create({
data: {
competitionId: input.competitionId,
name: input.name,
slug: input.slug,
roundType: input.roundType,
sortOrder: input.sortOrder,
configJson: config as unknown as Prisma.InputJsonValue,
windowOpenAt: input.windowOpenAt ?? undefined,
windowCloseAt: input.windowCloseAt ?? undefined,
juryGroupId: input.juryGroupId ?? undefined,
submissionWindowId: input.submissionWindowId ?? undefined,
purposeKey: input.purposeKey ?? undefined,
},
})
const round = await ctx.prisma.round.create({
data: {
competitionId: input.competitionId,
name: input.name,
slug: input.slug,
roundType: input.roundType,
sortOrder: input.sortOrder,
configJson: config as unknown as Prisma.InputJsonValue,
windowOpenAt: input.windowOpenAt ?? undefined,
windowCloseAt: input.windowCloseAt ?? undefined,
juryGroupId: input.juryGroupId ?? undefined,
submissionWindowId: input.submissionWindowId ?? undefined,
purposeKey: input.purposeKey ?? undefined,
},
})
await logAudit({
prisma: tx,
userId: ctx.user.id,
action: 'CREATE',
entityType: 'Round',
entityId: created.id,
detailsJson: {
name: input.name,
roundType: input.roundType,
competitionId: input.competitionId,
},
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
})
return created
await logAudit({
prisma: ctx.prisma,
userId: ctx.user.id,
action: 'CREATE',
entityType: 'Round',
entityId: round.id,
detailsJson: {
name: input.name,
roundType: input.roundType,
competitionId: input.competitionId,
},
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
})
return round
@@ -145,42 +141,38 @@ export const roundRouter = router({
.mutation(async ({ ctx, input }) => {
const { id, configJson, ...data } = input
const round = await ctx.prisma.$transaction(async (tx) => {
const existing = await tx.round.findUniqueOrThrow({ where: { id } })
const existing = await ctx.prisma.round.findUniqueOrThrow({ where: { id } })
// If configJson provided, validate it against the round type
let validatedConfig: Prisma.InputJsonValue | undefined
if (configJson) {
const parsed = validateRoundConfig(existing.roundType, configJson)
validatedConfig = parsed as unknown as Prisma.InputJsonValue
}
// If configJson provided, validate it against the round type
let validatedConfig: Prisma.InputJsonValue | undefined
if (configJson) {
const parsed = validateRoundConfig(existing.roundType, configJson)
validatedConfig = parsed as unknown as Prisma.InputJsonValue
}
const updated = await tx.round.update({
where: { id },
data: {
...data,
...(validatedConfig !== undefined ? { configJson: validatedConfig } : {}),
const round = await ctx.prisma.round.update({
where: { id },
data: {
...data,
...(validatedConfig !== undefined ? { configJson: validatedConfig } : {}),
},
})
await logAudit({
prisma: ctx.prisma,
userId: ctx.user.id,
action: 'UPDATE',
entityType: 'Round',
entityId: id,
detailsJson: {
changes: input,
previous: {
name: existing.name,
status: existing.status,
},
})
await logAudit({
prisma: tx,
userId: ctx.user.id,
action: 'UPDATE',
entityType: 'Round',
entityId: id,
detailsJson: {
changes: input,
previous: {
name: existing.name,
status: existing.status,
},
},
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
})
return updated
},
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
})
return round
@@ -213,30 +205,26 @@ export const roundRouter = router({
delete: adminProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => {
const round = await ctx.prisma.$transaction(async (tx) => {
const existing = await tx.round.findUniqueOrThrow({ where: { id: input.id } })
const existing = await ctx.prisma.round.findUniqueOrThrow({ where: { id: input.id } })
await tx.round.delete({ where: { id: input.id } })
await ctx.prisma.round.delete({ where: { id: input.id } })
await logAudit({
prisma: tx,
userId: ctx.user.id,
action: 'DELETE',
entityType: 'Round',
entityId: input.id,
detailsJson: {
name: existing.name,
roundType: existing.roundType,
competitionId: existing.competitionId,
},
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
})
return existing
await logAudit({
prisma: ctx.prisma,
userId: ctx.user.id,
action: 'DELETE',
entityType: 'Round',
entityId: input.id,
detailsJson: {
name: existing.name,
roundType: existing.roundType,
competitionId: existing.competitionId,
},
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
})
return round
return existing
}),
// =========================================================================
@@ -261,33 +249,29 @@ export const roundRouter = router({
})
)
.mutation(async ({ ctx, input }) => {
const window = await ctx.prisma.$transaction(async (tx) => {
const created = await tx.submissionWindow.create({
data: {
competitionId: input.competitionId,
name: input.name,
slug: input.slug,
roundNumber: input.roundNumber,
windowOpenAt: input.windowOpenAt,
windowCloseAt: input.windowCloseAt,
deadlinePolicy: input.deadlinePolicy,
graceHours: input.graceHours,
lockOnClose: input.lockOnClose,
},
})
const window = await ctx.prisma.submissionWindow.create({
data: {
competitionId: input.competitionId,
name: input.name,
slug: input.slug,
roundNumber: input.roundNumber,
windowOpenAt: input.windowOpenAt,
windowCloseAt: input.windowCloseAt,
deadlinePolicy: input.deadlinePolicy,
graceHours: input.graceHours,
lockOnClose: input.lockOnClose,
},
})
await logAudit({
prisma: tx,
userId: ctx.user.id,
action: 'CREATE',
entityType: 'SubmissionWindow',
entityId: created.id,
detailsJson: { name: input.name, competitionId: input.competitionId },
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
})
return created
await logAudit({
prisma: ctx.prisma,
userId: ctx.user.id,
action: 'CREATE',
entityType: 'SubmissionWindow',
entityId: window.id,
detailsJson: { name: input.name, competitionId: input.competitionId },
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
})
return window
@@ -313,22 +297,19 @@ export const roundRouter = router({
)
.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
const window = await ctx.prisma.submissionWindow.update({
where: { id },
data,
})
await logAudit({
prisma: ctx.prisma,
userId: ctx.user.id,
action: 'UPDATE',
entityType: 'SubmissionWindow',
entityId: id,
detailsJson: data,
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
})
return window
}),
@@ -350,18 +331,16 @@ export const roundRouter = router({
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,
})
await ctx.prisma.submissionWindow.delete({ where: { id: input.id } })
await logAudit({
prisma: ctx.prisma,
userId: ctx.user.id,
action: 'DELETE',
entityType: 'SubmissionWindow',
entityId: input.id,
detailsJson: { name: window.name },
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
})
return { success: true }
}),