Comprehensive round system audit: fix 27 logic bugs, add manual project/assignment features, improve UI/UX
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m23s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m23s
## Critical Logic Fixes (Tier 1) - Fix requiredReviews config key mismatch (always defaulted to 3) - Fix double-email + stageName/roundName metadata mismatch in notifications - Fix snake_case config reads in peer review (peerReviewEnabled was always blocked) - Add server-side COI check to evaluation submit (was client-only) - Fix hard-coded feedbackText.min(10) — now uses config values - Fix binaryDecision corruption in non-binary scoring modes - Fix advanceProjects: add competition/sort-order/status validations, move autoPass into tx - Fix removeFromRound: now cleans up orphaned Assignment records - Fix 3-day reminder sending wrong email template (was using 24h template) ## High-Priority Logic Fixes (Tier 2) - Add project state transition whitelist (prevent invalid transitions like REJECTED→PASSED) - Scope AI assignment job to jury group members (was querying all JURY_MEMBERs) - Add COI awareness to AI assignment generation - Enforce requireAllCriteriaScored server-side - Fix expireIntentsForRound nested transaction (now uses caller's tx) - Implement notifyOnEntry for advancement path - Implement notifyOnAdvance (was dead config) - Fix checkRequirementsAndTransition for SubmissionFileRequirement model ## New Features (Tier 3) - Add Project to Round: dialog with "Create New" and "From Pool" tabs - Assignment "By Project" mode: select project → assign multiple jurors - Backend: project.createAndAssignToRound procedure ## UI/UX Improvements (Tier 4+5) - Add AlertDialog confirmation to header status dropdown - Replace native confirm() with AlertDialog in assignments table - Jury stats card now display-only with "Change" link - Assignments tab restructured into logical card groups - Inline-editable round name in header - Back button shows destination label - Readiness checklist: green check instead of strikethrough - Gate assignments tab when no jury group assigned - Relative time on window stats card - Toast feedback on date saves - Disable advance button when no target round - COI section shows placeholder when empty - Round position shown as "Round X of Y" - InlineMemberCap edit icon always visible - Status badge tooltip with description - Add REMINDER_3_DAYS email template - Fix maybeSendEmail to respect notification preferences - Optimize bulk notification email loop Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1249,4 +1249,97 @@ export const projectRouter = router({
|
||||
stats,
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Create a new project and assign it directly to a round.
|
||||
* Used for late-arriving projects that need to enter a specific round immediately.
|
||||
*/
|
||||
createAndAssignToRound: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
title: z.string().min(1).max(500),
|
||||
teamName: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
country: z.string().optional(),
|
||||
competitionCategory: z.enum(['STARTUP', 'BUSINESS_CONCEPT']).optional(),
|
||||
roundId: z.string(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { roundId, country, ...projectFields } = input
|
||||
|
||||
// Get the round to find competitionId, then competition to find programId
|
||||
const round = await ctx.prisma.round.findUniqueOrThrow({
|
||||
where: { id: roundId },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
competition: {
|
||||
select: {
|
||||
id: true,
|
||||
programId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Normalize country to ISO code if provided
|
||||
const normalizedCountry = country
|
||||
? normalizeCountryToCode(country)
|
||||
: undefined
|
||||
|
||||
const project = await ctx.prisma.$transaction(async (tx) => {
|
||||
// 1. Create the project
|
||||
const created = await tx.project.create({
|
||||
data: {
|
||||
programId: round.competition.programId,
|
||||
title: projectFields.title,
|
||||
teamName: projectFields.teamName,
|
||||
description: projectFields.description,
|
||||
country: normalizedCountry,
|
||||
competitionCategory: projectFields.competitionCategory,
|
||||
status: 'ASSIGNED',
|
||||
},
|
||||
})
|
||||
|
||||
// 2. Create ProjectRoundState entry
|
||||
await tx.projectRoundState.create({
|
||||
data: {
|
||||
projectId: created.id,
|
||||
roundId,
|
||||
state: 'PENDING',
|
||||
},
|
||||
})
|
||||
|
||||
// 3. Create ProjectStatusHistory entry
|
||||
await tx.projectStatusHistory.create({
|
||||
data: {
|
||||
projectId: created.id,
|
||||
status: 'ASSIGNED',
|
||||
changedBy: ctx.user.id,
|
||||
},
|
||||
})
|
||||
|
||||
return created
|
||||
})
|
||||
|
||||
// Audit outside transaction
|
||||
await logAudit({
|
||||
prisma: ctx.prisma,
|
||||
userId: ctx.user.id,
|
||||
action: 'CREATE_AND_ASSIGN',
|
||||
entityType: 'Project',
|
||||
entityId: project.id,
|
||||
detailsJson: {
|
||||
title: input.title,
|
||||
roundId,
|
||||
roundName: round.name,
|
||||
programId: round.competition.programId,
|
||||
},
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
})
|
||||
|
||||
return project
|
||||
}),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user