Round detail overhaul, file requirements, project management, audit log fix
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m32s
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:
@@ -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 }
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user