feat(mentor): add change-request procedures + admin email notification
- mentor.requestChange: applicants/admins open a PENDING MentorChangeRequest with a reason; one open request per (user, project) enforced - mentor.listChangeRequests: admin-only inbox listing - mentor.resolveChangeRequest: admin marks RESOLVED or DISMISSED with optional resolution note - sendMentorChangeRequestEmail: notifies all SUPER_ADMIN/PROGRAM_ADMIN users when a request is opened (try/catch — never throws) - Mentors are NOT notified of change requests, even after resolution (per design decision in PR8 plan) - Audit log entries for create + resolve; raw reason redacted from audit Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2826,6 +2826,103 @@ export async function sendMentorTeamAssignmentEmail(
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Mentor change requests (PR 8) — admin notification when an applicant or admin
|
||||
// opens a MentorChangeRequest. Mentors are NOT notified (per design decision).
|
||||
// =============================================================================
|
||||
|
||||
function getMentorChangeRequestTemplate(
|
||||
projectTitle: string,
|
||||
requesterName: string | null,
|
||||
reason: string,
|
||||
adminDashboardUrl: string,
|
||||
): EmailTemplate {
|
||||
const subject = `Mentor change request for "${projectTitle}"`
|
||||
const requesterLabel = requesterName || 'a team member'
|
||||
const text = [
|
||||
'Hi MOPC admins,',
|
||||
'',
|
||||
`A mentor change request has been opened by ${requesterLabel} for the project "${projectTitle}".`,
|
||||
'',
|
||||
'Reason:',
|
||||
`"${reason}"`,
|
||||
'',
|
||||
`Review the request: ${adminDashboardUrl}`,
|
||||
'',
|
||||
'The MOPC team',
|
||||
].join('\n')
|
||||
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body style="margin:0;padding:0;background:#f6f8fa;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;color:#0f172a;">
|
||||
<div style="max-width:560px;margin:32px auto;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,0.06);">
|
||||
<div style="background:#053d57;padding:24px 28px;color:#fefefe;">
|
||||
<h1 style="margin:0;font-size:20px;font-weight:600;">Mentor change request</h1>
|
||||
</div>
|
||||
<div style="padding:24px 28px;line-height:1.5;font-size:14px;">
|
||||
<p style="margin-top:0;">Hi MOPC admins,</p>
|
||||
<p>A mentor change request has been opened by <strong>${escapeHtml(requesterLabel)}</strong> for the project <strong>${escapeHtml(projectTitle)}</strong>.</p>
|
||||
<blockquote style="margin:16px 0;padding:12px 16px;background:#f1f5f9;border-left:3px solid #557f8c;border-radius:4px;color:#0f172a;font-style:italic;white-space:pre-wrap;">${escapeHtml(reason)}</blockquote>
|
||||
<p style="margin-top:24px;">
|
||||
<a href="${adminDashboardUrl}" style="display:inline-block;padding:10px 20px;background:#de0f1e;color:#fff;text-decoration:none;border-radius:6px;font-weight:600;">Review Request</a>
|
||||
</p>
|
||||
<p style="margin-top:24px;color:#64748b;font-size:12px;">
|
||||
Mentors are not notified of change requests; only admins see this.
|
||||
</p>
|
||||
</div>
|
||||
<div style="padding:16px 28px;background:#f1f5f9;color:#64748b;font-size:12px;text-align:center;">
|
||||
Monaco Ocean Protection Challenge
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`.trim()
|
||||
|
||||
return { subject, text, html }
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify all SUPER_ADMIN / PROGRAM_ADMIN users that a mentor change request
|
||||
* has been opened for a project. Sends one email per recipient.
|
||||
* Never throws — failures are caught and logged so the calling mutation
|
||||
* (mentor.requestChange) never fails because of email infrastructure issues.
|
||||
*/
|
||||
export async function sendMentorChangeRequestEmail(
|
||||
adminEmails: string[],
|
||||
projectTitle: string,
|
||||
requesterName: string | null,
|
||||
reason: string,
|
||||
adminDashboardUrl: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (adminEmails.length === 0) {
|
||||
console.warn('[sendMentorChangeRequestEmail] no admin recipients; skipping')
|
||||
return
|
||||
}
|
||||
const template = getMentorChangeRequestTemplate(
|
||||
projectTitle,
|
||||
requesterName,
|
||||
reason,
|
||||
adminDashboardUrl,
|
||||
)
|
||||
await Promise.all(
|
||||
adminEmails.map((email) =>
|
||||
sendEmail({
|
||||
to: email,
|
||||
subject: template.subject,
|
||||
text: template.text,
|
||||
html: template.html,
|
||||
}).catch((err) => {
|
||||
console.error('[sendMentorChangeRequestEmail] send failed', { email, err })
|
||||
}),
|
||||
),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('[sendMentorChangeRequestEmail] failed', { error })
|
||||
}
|
||||
}
|
||||
|
||||
function getFinalistConfirmationTemplate(
|
||||
name: string,
|
||||
projectTitle: string,
|
||||
|
||||
Reference in New Issue
Block a user