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

## 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:
Matt
2026-02-19 12:59:35 +01:00
parent ee8b12e59c
commit baca483fcb
12 changed files with 1814 additions and 609 deletions

View File

@@ -268,9 +268,15 @@ export async function createBulkNotifications(params: {
})),
})
// Check email settings and send emails
for (const userId of userIds) {
await maybeSendEmail(userId, type, title, message, linkUrl, metadata)
// Check email settings once, then send emails only if enabled
const emailSetting = await prisma.notificationEmailSetting.findUnique({
where: { notificationType: type },
})
if (emailSetting?.sendEmail) {
for (const userId of userIds) {
await maybeSendEmailWithSetting(userId, type, title, message, emailSetting, linkUrl, metadata)
}
}
}
@@ -390,19 +396,36 @@ async function maybeSendEmail(
return
}
await maybeSendEmailWithSetting(userId, type, title, message, emailSetting, linkUrl, metadata)
} catch (error) {
// Log but don't fail the notification creation
console.error('[Notification] Failed to send email:', error)
}
}
/**
* Send email to a user using a pre-fetched email setting (skips the setting lookup)
*/
async function maybeSendEmailWithSetting(
userId: string,
type: string,
title: string,
message: string,
emailSetting: { sendEmail: boolean; emailSubject: string | null },
linkUrl?: string,
metadata?: Record<string, unknown>
): Promise<void> {
try {
// Check user's notification preference
const user = await prisma.user.findUnique({
where: { id: userId },
select: { email: true, name: true, notificationPreference: true },
})
if (!user || user.notificationPreference === 'NONE') {
if (!user || (user.notificationPreference !== 'EMAIL' && user.notificationPreference !== 'BOTH')) {
return
}
// Send styled email with full context
// The styled template will use metadata for rich content
// Subject can be overridden by admin settings
await sendStyledNotificationEmail(
user.email,
user.name || 'User',
@@ -416,7 +439,6 @@ async function maybeSendEmail(
emailSetting.emailSubject || undefined
)
} catch (error) {
// Log but don't fail the notification creation
console.error('[Notification] Failed to send email:', error)
}
}