Fix jury reminders, add notify jurors button, fix checkbox borders, widen assignment modal
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m32s

- Send Reminders button now works: added sendManualReminders() that bypasses
  cron-specific window/deadline/dedup guards so admin can send immediately
- Added Notify Jurors button that sends direct BATCH_ASSIGNED emails to all
  jurors with assignments (not dependent on NotificationEmailSetting config)
- Fixed checkbox component: default border is now neutral grey (border-input),
  red border (border-primary) only applied when checked
- Widened Add Assignment dialog from max-w-2xl to max-w-3xl to prevent overflow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt
2026-02-19 12:15:51 +01:00
parent 51e18870b6
commit ee8b12e59c
5 changed files with 268 additions and 8 deletions

View File

@@ -14,9 +14,113 @@ interface ReminderResult {
errors: number
}
/**
* Manually send reminders to all jurors with incomplete assignments for a round.
* Bypasses window/deadline checks — the admin explicitly chose to send now.
* Uses 'MANUAL' type so it doesn't interfere with automated cron deduplication,
* but still deduplicates within manual sends (one manual reminder per juror per round).
*/
export async function sendManualReminders(roundId: string): Promise<ReminderResult> {
let sent = 0
let errors = 0
const round = await prisma.round.findUnique({
where: { id: roundId },
select: {
id: true,
name: true,
windowCloseAt: true,
competition: { select: { name: true } },
},
})
if (!round) return { sent, errors }
// Find jurors with incomplete assignments
const incompleteAssignments = await prisma.assignment.findMany({
where: { roundId, isCompleted: false },
select: { userId: true },
})
const userIds = [...new Set(incompleteAssignments.map((a) => a.userId))]
if (userIds.length === 0) return { sent, errors }
// Deduplicate: only one MANUAL reminder per juror per round
const existingManual = await prisma.reminderLog.findMany({
where: { roundId, type: 'MANUAL', userId: { in: userIds } },
select: { userId: true },
})
const alreadySent = new Set(existingManual.map((r) => r.userId))
const usersToNotify = userIds.filter((id) => !alreadySent.has(id))
if (usersToNotify.length === 0) return { sent, errors }
const users = await prisma.user.findMany({
where: { id: { in: usersToNotify } },
select: { id: true, name: true, email: true },
})
const baseUrl = process.env.NEXTAUTH_URL || 'https://monaco-opc.com'
const deadlineStr = round.windowCloseAt
? round.windowCloseAt.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'short',
})
: undefined
const pendingCounts = new Map<string, number>()
for (const a of incompleteAssignments) {
pendingCounts.set(a.userId, (pendingCounts.get(a.userId) || 0) + 1)
}
for (const user of users) {
const pendingCount = pendingCounts.get(user.id) || 0
if (pendingCount === 0) continue
try {
const deadlineNote = deadlineStr ? ` The deadline is ${deadlineStr}.` : ''
await sendStyledNotificationEmail(
user.email,
user.name || '',
'REMINDER_24H',
{
name: user.name || undefined,
title: `Evaluation Reminder - ${round.name}`,
message: `You have ${pendingCount} pending evaluation${pendingCount !== 1 ? 's' : ''} for ${round.name}.${deadlineNote}`,
linkUrl: `${baseUrl}/jury/rounds/${round.id}/assignments`,
metadata: {
pendingCount,
roundName: round.name,
...(deadlineStr && { deadline: deadlineStr }),
},
}
)
await prisma.reminderLog.create({
data: { roundId, userId: user.id, type: 'MANUAL' },
})
sent++
} catch (error) {
console.error(
`Failed to send manual reminder to ${user.email} for round ${round.name}:`,
error
)
errors++
}
}
return { sent, errors }
}
/**
* Find active stages with approaching deadlines and send reminders
* to jurors who have incomplete assignments.
* to jurors who have incomplete assignments. (Used by cron job)
*/
export async function processEvaluationReminders(roundId?: string): Promise<ReminderResult> {
const now = new Date()