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:
@@ -590,24 +590,25 @@ export const projectRouter = router({
|
||||
}
|
||||
}
|
||||
|
||||
await logAudit({
|
||||
prisma: tx,
|
||||
userId: ctx.user.id,
|
||||
action: 'CREATE',
|
||||
entityType: 'Project',
|
||||
entityId: created.id,
|
||||
detailsJson: {
|
||||
title: input.title,
|
||||
programId: resolvedProgramId,
|
||||
teamMembersCount: teamMembersInput?.length || 0,
|
||||
},
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
})
|
||||
|
||||
return { project: created, membersToInvite: inviteList }
|
||||
})
|
||||
|
||||
// Audit outside transaction so failures don't roll back the project creation
|
||||
await logAudit({
|
||||
prisma: ctx.prisma,
|
||||
userId: ctx.user.id,
|
||||
action: 'CREATE',
|
||||
entityType: 'Project',
|
||||
entityId: project.id,
|
||||
detailsJson: {
|
||||
title: input.title,
|
||||
programId: resolvedProgramId,
|
||||
teamMembersCount: teamMembersInput?.length || 0,
|
||||
},
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
})
|
||||
|
||||
// Send invite emails outside the transaction (never fail project creation)
|
||||
if (membersToInvite.length > 0) {
|
||||
const baseUrl = process.env.NEXTAUTH_URL || 'https://monaco-opc.com'
|
||||
@@ -782,26 +783,25 @@ export const projectRouter = router({
|
||||
delete: adminProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const project = await ctx.prisma.$transaction(async (tx) => {
|
||||
const target = await tx.project.findUniqueOrThrow({
|
||||
where: { id: input.id },
|
||||
select: { id: true, title: true },
|
||||
})
|
||||
const target = await ctx.prisma.project.findUniqueOrThrow({
|
||||
where: { id: input.id },
|
||||
select: { id: true, title: true },
|
||||
})
|
||||
|
||||
await logAudit({
|
||||
prisma: tx,
|
||||
userId: ctx.user.id,
|
||||
action: 'DELETE',
|
||||
entityType: 'Project',
|
||||
entityId: input.id,
|
||||
detailsJson: { title: target.title },
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
})
|
||||
const project = await ctx.prisma.project.delete({
|
||||
where: { id: input.id },
|
||||
})
|
||||
|
||||
return tx.project.delete({
|
||||
where: { id: input.id },
|
||||
})
|
||||
// Audit outside transaction so failures don't roll back the delete
|
||||
await logAudit({
|
||||
prisma: ctx.prisma,
|
||||
userId: ctx.user.id,
|
||||
action: 'DELETE',
|
||||
entityType: 'Project',
|
||||
entityId: input.id,
|
||||
detailsJson: { title: target.title },
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
})
|
||||
|
||||
return project
|
||||
@@ -829,24 +829,23 @@ export const projectRouter = router({
|
||||
})
|
||||
}
|
||||
|
||||
const result = await ctx.prisma.$transaction(async (tx) => {
|
||||
await logAudit({
|
||||
prisma: tx,
|
||||
userId: ctx.user.id,
|
||||
action: 'BULK_DELETE',
|
||||
entityType: 'Project',
|
||||
detailsJson: {
|
||||
count: projects.length,
|
||||
titles: projects.map((p) => p.title),
|
||||
ids: projects.map((p) => p.id),
|
||||
},
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
})
|
||||
const result = await ctx.prisma.project.deleteMany({
|
||||
where: { id: { in: projects.map((p) => p.id) } },
|
||||
})
|
||||
|
||||
return tx.project.deleteMany({
|
||||
where: { id: { in: projects.map((p) => p.id) } },
|
||||
})
|
||||
// Audit outside transaction so failures don't roll back the bulk delete
|
||||
await logAudit({
|
||||
prisma: ctx.prisma,
|
||||
userId: ctx.user.id,
|
||||
action: 'BULK_DELETE',
|
||||
entityType: 'Project',
|
||||
detailsJson: {
|
||||
count: projects.length,
|
||||
titles: projects.map((p) => p.title),
|
||||
ids: projects.map((p) => p.id),
|
||||
},
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
})
|
||||
|
||||
return { deleted: result.count }
|
||||
@@ -996,19 +995,20 @@ export const projectRouter = router({
|
||||
})
|
||||
}
|
||||
|
||||
await logAudit({
|
||||
prisma: tx,
|
||||
userId: ctx.user.id,
|
||||
action: 'BULK_UPDATE_STATUS',
|
||||
entityType: 'Project',
|
||||
detailsJson: { ids: matchingIds, status: input.status, count: result.count },
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// Audit outside transaction so failures don't roll back the bulk update
|
||||
await logAudit({
|
||||
prisma: ctx.prisma,
|
||||
userId: ctx.user.id,
|
||||
action: 'BULK_UPDATE_STATUS',
|
||||
entityType: 'Project',
|
||||
detailsJson: { ids: matchingIds, status: input.status, count: updated.count },
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
})
|
||||
|
||||
// Notify project teams based on status
|
||||
if (projects.length > 0) {
|
||||
const notificationConfig: Record<
|
||||
|
||||
Reference in New Issue
Block a user