217 lines
7.1 KiB
JavaScript
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,
|
|
});
|
|
}
|