import { prisma } from '@/lib/prisma' import { sendStyledNotificationEmail } from '@/lib/email' const REMINDER_TYPES = [ { type: '3_DAYS', thresholdMs: 3 * 24 * 60 * 60 * 1000 }, { type: '24H', thresholdMs: 24 * 60 * 60 * 1000 }, { type: '1H', thresholdMs: 60 * 60 * 1000 }, ] as const type ReminderType = (typeof REMINDER_TYPES)[number]['type'] interface ReminderResult { sent: number errors: number } /** * Find active rounds with approaching voting deadlines and send reminders * to jurors who have incomplete assignments. */ export async function processEvaluationReminders(roundId?: string): Promise { const now = new Date() let totalSent = 0 let totalErrors = 0 // Find active rounds with voting end dates in the future const rounds = await prisma.round.findMany({ where: { status: 'ACTIVE', votingEndAt: { gt: now }, votingStartAt: { lte: now }, ...(roundId && { id: roundId }), }, select: { id: true, name: true, votingEndAt: true, program: { select: { name: true } }, }, }) for (const round of rounds) { if (!round.votingEndAt) continue const msUntilDeadline = round.votingEndAt.getTime() - now.getTime() // Determine which reminder types should fire for this round const applicableTypes = REMINDER_TYPES.filter( ({ thresholdMs }) => msUntilDeadline <= thresholdMs ) if (applicableTypes.length === 0) continue for (const { type } of applicableTypes) { const result = await sendRemindersForRound(round, type, now) totalSent += result.sent totalErrors += result.errors } } return { sent: totalSent, errors: totalErrors } } async function sendRemindersForRound( round: { id: string name: string votingEndAt: Date | null program: { name: string } }, type: ReminderType, now: Date ): Promise { let sent = 0 let errors = 0 if (!round.votingEndAt) return { sent, errors } // Find jurors with incomplete assignments for this round const incompleteAssignments = await prisma.assignment.findMany({ where: { roundId: round.id, isCompleted: false, }, select: { userId: true, }, }) // Get unique user IDs with incomplete work const userIds = [...new Set(incompleteAssignments.map((a) => a.userId))] if (userIds.length === 0) return { sent, errors } // Check which users already received this reminder type for this round const existingReminders = await prisma.reminderLog.findMany({ where: { roundId: round.id, type, userId: { in: userIds }, }, select: { userId: true }, }) const alreadySent = new Set(existingReminders.map((r) => r.userId)) const usersToNotify = userIds.filter((id) => !alreadySent.has(id)) if (usersToNotify.length === 0) return { sent, errors } // Get user details and their pending counts 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.votingEndAt.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZoneName: 'short', }) // Map to get pending count per user const pendingCounts = new Map() for (const a of incompleteAssignments) { pendingCounts.set(a.userId, (pendingCounts.get(a.userId) || 0) + 1) } // Select email template type based on reminder type const emailTemplateType = type === '1H' ? 'REMINDER_1H' : 'REMINDER_24H' for (const user of users) { const pendingCount = pendingCounts.get(user.id) || 0 if (pendingCount === 0) continue try { await sendStyledNotificationEmail( user.email, user.name || '', emailTemplateType, { name: user.name || undefined, title: `Evaluation Reminder - ${round.name}`, message: `You have ${pendingCount} pending evaluation${pendingCount !== 1 ? 's' : ''} for ${round.name}.`, linkUrl: `${baseUrl}/jury/assignments?round=${round.id}`, metadata: { pendingCount, roundName: round.name, deadline: deadlineStr, }, } ) // Log the sent reminder await prisma.reminderLog.create({ data: { roundId: round.id, userId: user.id, type, }, }) sent++ } catch (error) { console.error( `Failed to send ${type} reminder to ${user.email} for round ${round.name}:`, error ) errors++ } } return { sent, errors } }