diff --git a/prisma/seed-notification-settings.ts b/prisma/seed-notification-settings.ts index 0b738b6..ec52456 100644 --- a/prisma/seed-notification-settings.ts +++ b/prisma/seed-notification-settings.ts @@ -271,6 +271,20 @@ const NOTIFICATION_EMAIL_SETTINGS = [ description: 'Email to the attendee when their visa application status changes', sendEmail: true, }, + { + notificationType: 'GRAND_FINAL_DOCS_REMINDER', + category: 'logistics', + label: 'Final Documents Reminder', + description: 'Reminder to finalist teams to upload their Grand Final documents before the deadline', + sendEmail: true, + }, + { + notificationType: 'GRAND_FINAL_DOCS_SUBMITTED', + category: 'logistics', + label: 'Final Documents Submitted', + description: 'Notifies the team mentor when a finalist uploads a Grand Final document', + sendEmail: false, + }, // Admin notifications (in-app only by default) { diff --git a/src/lib/email.ts b/src/lib/email.ts index 2c9b5a1..fb47a8f 100644 --- a/src/lib/email.ts +++ b/src/lib/email.ts @@ -2238,6 +2238,42 @@ function getFinalistReminderTemplate( } } +/** + * Reminder email sent to a finalist team to upload their Grand Final documents. + */ +function getGrandFinalDocsReminderTemplate( + name: string, + projectTitle: string, + deadline: Date | null, + uploadUrl: string, + missing: string[], +): EmailTemplate { + const greeting = name ? `Hi ${name},` : 'Hi there,' + const formattedDeadline = deadline + ? deadline.toLocaleString('en-GB', { timeZone: 'Europe/Paris', dateStyle: 'full', timeStyle: 'short' }) + : null + const missingLine = missing.length + ? `Still needed: ${escapeHtml(missing.join(', '))}.` + : 'Please make sure all required documents are uploaded.' + const content = ` + ${sectionTitle('Grand Final — Final Documents')} + ${paragraph(greeting)} + ${paragraph(`Please upload the final documents for ${escapeHtml(projectTitle)} ahead of the Monaco Ocean Protection Challenge Grand Finale.`)} + ${paragraph(missingLine)} + ${formattedDeadline ? infoBox(`Deadline: ${escapeHtml(formattedDeadline)} (Paris time).`, 'warning') : ''} + ${ctaButton(uploadUrl, 'Upload documents')} + ${paragraph('If you have any questions, please reach out to the MOPC team.')} + ` + const text = [ + greeting, '', + `Please upload the final documents for "${projectTitle}" ahead of the Grand Finale.`, + missing.length ? `Still needed: ${missing.join(', ')}.` : 'Please make sure all required documents are uploaded.', + formattedDeadline ? `Deadline: ${formattedDeadline} (Paris time).` : '', + `Upload documents: ${uploadUrl}`, '', 'The MOPC team', + ].join('\n') + return { subject: 'Action needed: upload your Grand Final documents', html: getEmailWrapper(content), text } +} + /** * Notification email sent to a team when their confirmed finalist slot is withdrawn by an admin. */ @@ -2685,6 +2721,14 @@ export const NOTIFICATION_EMAIL_TEMPLATES: Record = { new Date((ctx.metadata?.deadline as string) || Date.now()), ctx.linkUrl || '', ), + GRAND_FINAL_DOCS_REMINDER: (ctx) => + getGrandFinalDocsReminderTemplate( + ctx.name || '', + (ctx.metadata?.projectTitle as string) || 'Your project', + ctx.metadata?.deadline ? new Date(ctx.metadata.deadline as string) : null, + ctx.linkUrl || '', + (ctx.metadata?.missing as string[]) || [], + ), FINALIST_WITHDRAWN: (ctx) => getFinalistWithdrawnTemplate( ctx.name || '', diff --git a/src/server/services/in-app-notification.ts b/src/server/services/in-app-notification.ts index fe59d55..58a8121 100644 --- a/src/server/services/in-app-notification.ts +++ b/src/server/services/in-app-notification.ts @@ -105,6 +105,8 @@ export const NotificationTypes = { FINALIST_WITHDRAWN: 'FINALIST_WITHDRAWN', TRAVEL_CONFIRMED: 'TRAVEL_CONFIRMED', VISA_STATUS_UPDATE: 'VISA_STATUS_UPDATE', + GRAND_FINAL_DOCS_REMINDER: 'GRAND_FINAL_DOCS_REMINDER', + GRAND_FINAL_DOCS_SUBMITTED: 'GRAND_FINAL_DOCS_SUBMITTED', } as const export type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes]