Add COI/manual reassignment emails, confirmation dialog, and smart juror selection
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m14s
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m14s
- Add COI_REASSIGNED and MANUAL_REASSIGNED notification types with distinct email templates, icons, and priorities - COI declaration dialog now shows a confirmation step warning that the project will be reassigned before submitting - reassignAfterCOI now checks historical assignments (all rounds, audit logs) to never assign the same project to a juror twice, and prefers jurors with incomplete evaluations over those who have finished all their work - Admin transfer (transferAssignments) sends per-juror MANUAL_REASSIGNED notifications with actual project names instead of generic batch emails - docker-entrypoint syncs notification settings on every deploy via upsert Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
150
src/lib/email.ts
150
src/lib/email.ts
@@ -943,6 +943,140 @@ Together for a healthier ocean.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate "COI Reassignment" email template (for jury receiving a reassigned project)
|
||||
*/
|
||||
function getCOIReassignedTemplate(
|
||||
name: string,
|
||||
projectName: string,
|
||||
roundName: string,
|
||||
deadline?: string,
|
||||
assignmentsUrl?: string
|
||||
): EmailTemplate {
|
||||
const greeting = name ? `Hello ${name},` : 'Hello,'
|
||||
|
||||
const projectCard = `
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin: 20px 0;">
|
||||
<tr>
|
||||
<td style="background-color: #fef3c7; border-left: 4px solid #f59e0b; border-radius: 0 12px 12px 0; padding: 20px 24px;">
|
||||
<p style="color: ${BRAND.textMuted}; margin: 0 0 4px 0; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px;">Reassigned Project</p>
|
||||
<p style="color: ${BRAND.darkBlue}; margin: 0; font-size: 18px; font-weight: 700;">${projectName}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`
|
||||
|
||||
const deadlineBox = deadline ? `
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin: 20px 0;">
|
||||
<tr>
|
||||
<td style="background-color: #fef2f2; border-left: 4px solid ${BRAND.red}; border-radius: 0 8px 8px 0; padding: 16px 20px;">
|
||||
<p style="color: #991b1b; margin: 0 0 4px 0; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px;">Deadline</p>
|
||||
<p style="color: #7f1d1d; margin: 0; font-size: 16px; font-weight: 700;">${deadline}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
` : ''
|
||||
|
||||
const content = `
|
||||
${sectionTitle(greeting)}
|
||||
${paragraph(`A project has been <strong>reassigned to you</strong> for evaluation in <strong style="color: ${BRAND.darkBlue};">${roundName}</strong>, because the previously assigned juror declared a conflict of interest.`)}
|
||||
${projectCard}
|
||||
${deadlineBox}
|
||||
${paragraph('Please review the project materials and submit your evaluation before the deadline. This is an additional project on top of your existing assignments.')}
|
||||
${assignmentsUrl ? ctaButton(assignmentsUrl, 'View Assignment') : ''}
|
||||
`
|
||||
|
||||
return {
|
||||
subject: `Project Reassigned to You: "${projectName}" - ${roundName}`,
|
||||
html: getEmailWrapper(content),
|
||||
text: `
|
||||
${greeting}
|
||||
|
||||
A project has been reassigned to you for evaluation in ${roundName}, because the previously assigned juror declared a conflict of interest.
|
||||
|
||||
Project: ${projectName}
|
||||
${deadline ? `Deadline: ${deadline}` : ''}
|
||||
|
||||
Please review the project materials and submit your evaluation before the deadline. This is an additional project on top of your existing assignments.
|
||||
|
||||
${assignmentsUrl ? `View assignment: ${assignmentsUrl}` : ''}
|
||||
|
||||
---
|
||||
Monaco Ocean Protection Challenge
|
||||
Together for a healthier ocean.
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate "Manual Reassignment" email template (for jury)
|
||||
* Sent when an admin manually transfers a project assignment to a juror.
|
||||
*/
|
||||
function getManualReassignedTemplate(
|
||||
name: string,
|
||||
projectNames: string[],
|
||||
roundName: string,
|
||||
deadline?: string,
|
||||
assignmentsUrl?: string
|
||||
): EmailTemplate {
|
||||
const greeting = name ? `Hello ${name},` : 'Hello,'
|
||||
const count = projectNames.length
|
||||
const isSingle = count === 1
|
||||
|
||||
const projectList = projectNames.map((p) => `
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin: 8px 0;">
|
||||
<tr>
|
||||
<td style="background-color: #eff6ff; border-left: 4px solid ${BRAND.darkBlue}; border-radius: 0 8px 8px 0; padding: 14px 20px;">
|
||||
<p style="color: ${BRAND.darkBlue}; margin: 0; font-size: 16px; font-weight: 700;">${p}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
`).join('')
|
||||
|
||||
const deadlineBox = deadline ? `
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="margin: 20px 0;">
|
||||
<tr>
|
||||
<td style="background-color: #fef2f2; border-left: 4px solid ${BRAND.red}; border-radius: 0 8px 8px 0; padding: 16px 20px;">
|
||||
<p style="color: #991b1b; margin: 0 0 4px 0; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px;">Deadline</p>
|
||||
<p style="color: #7f1d1d; margin: 0; font-size: 16px; font-weight: 700;">${deadline}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
` : ''
|
||||
|
||||
const content = `
|
||||
${sectionTitle(greeting)}
|
||||
${paragraph(`An administrator has <strong>reassigned ${isSingle ? 'a project' : `${count} projects`}</strong> to you for evaluation in <strong style="color: ${BRAND.darkBlue};">${roundName}</strong>.`)}
|
||||
${projectList}
|
||||
${deadlineBox}
|
||||
${paragraph(`Please review the project material${isSingle ? '' : 's'} and submit your evaluation${isSingle ? '' : 's'} before the deadline.`)}
|
||||
${assignmentsUrl ? ctaButton(assignmentsUrl, 'View Assignments') : ''}
|
||||
`
|
||||
|
||||
const projectListText = projectNames.map((p) => ` - ${p}`).join('\n')
|
||||
|
||||
return {
|
||||
subject: `Project${isSingle ? '' : 's'} Reassigned to You - ${roundName}`,
|
||||
html: getEmailWrapper(content),
|
||||
text: `
|
||||
${greeting}
|
||||
|
||||
An administrator has reassigned ${isSingle ? 'a project' : `${count} projects`} to you for evaluation in ${roundName}.
|
||||
|
||||
${isSingle ? `Project: ${projectNames[0]}` : `Projects:\n${projectListText}`}
|
||||
${deadline ? `Deadline: ${deadline}` : ''}
|
||||
|
||||
Please review the project material${isSingle ? '' : 's'} and submit your evaluation${isSingle ? '' : 's'} before the deadline.
|
||||
|
||||
${assignmentsUrl ? `View assignments: ${assignmentsUrl}` : ''}
|
||||
|
||||
---
|
||||
Monaco Ocean Protection Challenge
|
||||
Together for a healthier ocean.
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate "Batch Assigned" email template (for jury)
|
||||
*/
|
||||
@@ -1527,6 +1661,22 @@ export const NOTIFICATION_EMAIL_TEMPLATES: Record<string, TemplateGenerator> = {
|
||||
ctx.metadata?.deadline as string | undefined,
|
||||
ctx.linkUrl
|
||||
),
|
||||
COI_REASSIGNED: (ctx) =>
|
||||
getCOIReassignedTemplate(
|
||||
ctx.name || '',
|
||||
(ctx.metadata?.projectName as string) || 'Project',
|
||||
(ctx.metadata?.roundName as string) || 'this round',
|
||||
ctx.metadata?.deadline as string | undefined,
|
||||
ctx.linkUrl
|
||||
),
|
||||
MANUAL_REASSIGNED: (ctx) =>
|
||||
getManualReassignedTemplate(
|
||||
ctx.name || '',
|
||||
(ctx.metadata?.projectNames as string[]) || [(ctx.metadata?.projectName as string) || 'Project'],
|
||||
(ctx.metadata?.roundName as string) || 'this round',
|
||||
ctx.metadata?.deadline as string | undefined,
|
||||
ctx.linkUrl
|
||||
),
|
||||
BATCH_ASSIGNED: (ctx) =>
|
||||
getBatchAssignedTemplate(
|
||||
ctx.name || '',
|
||||
|
||||
Reference in New Issue
Block a user