VirtualPR/backend/src/services/notify.js

217 lines
7.1 KiB
JavaScript

import { NotificationLog } from '../models/NotificationLog.js';
import { brandedEmail, sendMail } from './mail.js';
export async function notify({ kind, to, subject, text, html }) {
const recipient = String(to || '').trim();
if (!recipient) return { ok: false, skipped: true };
try {
await sendMail({ to: recipient, subject, text, html });
await NotificationLog.create({ kind, to: recipient, subject, status: 'sent' });
return { ok: true };
} catch (err) {
const error = err?.message || 'Could not send email';
await NotificationLog.create({ kind, to: recipient, subject, status: 'failed', error });
console.error(`[notify] ${kind} failed for ${recipient}: ${error}`);
return { ok: false, error };
}
}
export function personName(user) {
return [user?.firstName, user?.lastName].filter(Boolean).join(' ');
}
export function staffGreeting(user) {
const name = personName(user);
return name ? `Dear ${name},` : 'Hello,';
}
function prtLabel(user) {
const name = personName(user);
return name || 'your Virtual PRT';
}
function cohortDetails(cohort) {
return [
{ label: 'Cohort', value: cohort?.name || '' },
{ label: 'Start date', value: String(cohort?.startDate || '').slice(0, 10) },
{ label: 'Slot', value: cohort?.slot || '' },
{ label: 'Language', value: cohort?.language || '' },
{ label: 'Virtual PRT', value: prtLabel(cohort?.virtualPrt) },
];
}
export async function notifyPaymentReview({ toUser, patientName, status, reason }) {
if (!toUser?.email) return;
const approved = status === 'confirmed';
const subject = approved
? `Patient confirmed: ${patientName}`
: `Patient not confirmed: ${patientName}`;
const { text, html } = brandedEmail({
greeting: staffGreeting(toUser),
intro: approved
? `${patientName} has been confirmed and can now be matched to a cohort.`
: `${patientName} was not confirmed.`,
details: [
{ label: 'Patient', value: patientName },
{ label: 'Decision', value: approved ? 'Confirmed' : 'Rejected' },
{ label: 'Reason', value: !approved ? reason : '' },
],
note: approved
? 'No further action is needed unless you were asked to update the patient record.'
: 'Please review the reason and update the patient record if a correction is needed.',
});
await notify({
kind: approved ? 'payment_confirmed' : 'payment_rejected',
to: toUser.email,
subject,
text,
html,
});
}
export async function notifyCohortConfirmed({ prt, fieldNotices = [], cohort }) {
const subject = `Cohort confirmed: ${cohort.name}`;
if (prt?.email) {
const { text, html } = brandedEmail({
greeting: staffGreeting(prt),
intro: `You have been assigned ${cohort.name}. The cohort is confirmed and its sessions are on your calendar.`,
details: cohortDetails(cohort),
note: 'Please review the start date and slot, and connect Google Calendar on your profile if Meet links are not yet created.',
});
await notify({
kind: 'cohort_confirmed',
to: prt.email,
subject,
text,
html,
});
}
for (const notice of fieldNotices) {
const field = notice.user || notice;
if (!field?.email) continue;
const names = (notice.names || []).filter(Boolean);
const { text, html } = brandedEmail({
greeting: staffGreeting(field),
intro: names.length
? `Patients you registered have been placed in ${cohort.name}.`
: `Patients you registered were assigned to ${cohort.name}.`,
details: [
...cohortDetails(cohort),
{ label: 'Your patients', value: names.join(', ') },
],
});
await notify({
kind: 'cohort_confirmed_field',
to: field.email,
subject,
text,
html,
});
}
}
export async function notifySessionReminder({ prt, session, cohort }) {
if (!prt?.email) return;
const subject = `Session reminder: ${cohort?.name || 'Cohort'} at ${session.slot || ''}`;
const { text, html } = brandedEmail({
greeting: staffGreeting(prt),
intro: 'Your cohort session starts in about 2 hours.',
details: [
{ label: 'Cohort', value: cohort?.name || '' },
{ label: 'Date', value: session.date || '' },
{ label: 'Time', value: session.slot || '' },
],
note: session.meetUrl ? 'Use the button below to join the meeting.' : 'A Meet link is not on this session yet.',
action: session.meetUrl ? { href: session.meetUrl, label: 'Join Meet' } : null,
});
await notify({
kind: 'session_reminder',
to: prt.email,
subject,
text,
html,
});
}
export async function notifySessionPushed({ prt, admins = [], cohort, fromDate, toDate, reason }) {
const subject = `Session moved: ${cohort?.name || 'Cohort'}`;
const content = {
intro: `The ${fromDate} session for ${cohort?.name || 'a cohort'} was moved.`,
details: [
{ label: 'Cohort', value: cohort?.name || '' },
{ label: 'Original date', value: fromDate },
{ label: 'New date', value: toDate },
{ label: 'Reason', value: reason || 'Rescheduled' },
],
note: 'The replacement session keeps the same slot. Please update anyone who was expecting the original date.',
};
if (prt?.email) {
const { text, html } = brandedEmail({ greeting: staffGreeting(prt), ...content });
await notify({
kind: 'session_pushed',
to: prt.email,
subject,
text,
html,
});
}
for (const admin of admins) {
if (!admin?.email) continue;
const { text, html } = brandedEmail({ greeting: staffGreeting(admin), ...content });
await notify({
kind: 'session_cancelled_admin',
to: admin.email,
subject,
text,
html,
});
}
}
export async function notifyLeaveDecision({ prt, status, startDate, endDate }) {
if (!prt?.email) return;
const approved = status === 'approved';
const subject = `Leave ${approved ? 'approved' : status}: ${startDate} to ${endDate}`;
const { text, html } = brandedEmail({
greeting: staffGreeting(prt),
intro: approved
? 'Your leave request has been approved.'
: `Your leave request was ${status}.`,
details: [
{ label: 'From', value: startDate },
{ label: 'To', value: endDate },
{ label: 'Decision', value: approved ? 'Approved' : status },
],
note: approved
? 'Sessions in this period will be covered or moved according to the leave decision.'
: 'Your sessions in this period stay on the original dates.',
});
await notify({
kind: `leave_${status}`,
to: prt.email,
subject,
text,
html,
});
}
export async function notifyReassignment({ prt, cohort, reason }) {
if (!prt?.email) return;
const { text, html } = brandedEmail({
greeting: staffGreeting(prt),
intro: `You have been assigned to ${cohort?.name || 'a cohort'}.`,
details: [
...cohortDetails(cohort),
{ label: 'Reason', value: reason || 'The previous Virtual PRT is no longer assigned.' },
],
note: 'Please check the cohort schedule and make sure your Google Calendar is connected.',
});
await notify({
kind: 'reassignment',
to: prt.email,
subject: `Cohort assigned: ${cohort?.name || 'Cohort'}`,
text,
html,
});
}