136 lines
5.2 KiB
JavaScript
136 lines
5.2 KiB
JavaScript
import { Cohort } from '../models/Cohort.js';
|
|
import { OnboardedPatient } from '../models/OnboardedPatient.js';
|
|
import { addPatientToWhatsapp, removePatientFromWhatsapp, syncWhatsappMembership } from './whatsapp.js';
|
|
import { applyOnboardedPii, hashMobile, isMobileNumber } from './cryptoPii.js';
|
|
import { toYmd, weekdayOf } from './schedule.js';
|
|
import Boom from '@hapi/boom';
|
|
|
|
async function safeWhatsapp(label, fn) {
|
|
try {
|
|
await fn();
|
|
} catch (err) {
|
|
console.error(`[whatsapp] ${label} failed: ${err?.message || err}`);
|
|
}
|
|
}
|
|
|
|
export async function syncCohortPatients(cohort, previousPatientIds = []) {
|
|
const nextIds = (cohort.patients || []).map((id) => String(id));
|
|
const prevIds = (previousPatientIds || []).map((id) => String(id));
|
|
|
|
if (nextIds.length) {
|
|
await OnboardedPatient.updateMany(
|
|
{ _id: { $in: nextIds } },
|
|
{ $set: { assignedEducator: cohort.virtualPrt, cohort: cohort._id, matchingStatus: 'assigned' } },
|
|
);
|
|
}
|
|
|
|
const removed = prevIds.filter((id) => !nextIds.includes(id));
|
|
for (const id of removed) {
|
|
const other = await Cohort.findOne({ _id: { $ne: cohort._id }, patients: id });
|
|
if (other) {
|
|
await OnboardedPatient.updateOne(
|
|
{ _id: id },
|
|
{ $set: { assignedEducator: other.virtualPrt, cohort: other._id, matchingStatus: 'assigned' } },
|
|
);
|
|
} else {
|
|
await OnboardedPatient.updateOne(
|
|
{ _id: id },
|
|
{ $set: { assignedEducator: null, cohort: null, matchingStatus: 'unassigned' } },
|
|
);
|
|
}
|
|
}
|
|
|
|
if (cohort._id && cohort.virtualPrt) {
|
|
await OnboardedPatient.updateMany(
|
|
{ cohort: cohort._id },
|
|
{ $set: { assignedEducator: cohort.virtualPrt } },
|
|
);
|
|
}
|
|
await safeWhatsapp('membership sync', () => syncWhatsappMembership(cohort, previousPatientIds));
|
|
}
|
|
|
|
export async function addPatientToCohort(cohort, patientId) {
|
|
const id = String(patientId);
|
|
const current = (cohort.patients || []).map((item) => String(item._id || item));
|
|
if (!current.includes(id)) {
|
|
cohort.patients.push(patientId);
|
|
await cohort.save();
|
|
}
|
|
await OnboardedPatient.updateOne(
|
|
{ _id: patientId },
|
|
{ $set: { assignedEducator: cohort.virtualPrt, cohort: cohort._id, matchingStatus: 'assigned' } },
|
|
);
|
|
await safeWhatsapp('add patient', () => addPatientToWhatsapp(cohort, patientId));
|
|
}
|
|
|
|
export async function removePatientFromCohort(cohortId, patientId) {
|
|
if (!cohortId || !patientId) return;
|
|
await Cohort.updateOne({ _id: cohortId }, { $pull: { patients: patientId } });
|
|
await safeWhatsapp('remove patient', () => removePatientFromWhatsapp(cohortId, patientId));
|
|
}
|
|
|
|
export async function releaseCohortPatients(cohort) {
|
|
const previous = (cohort.patients || []).map((p) => String(p._id || p));
|
|
await syncCohortPatients({ ...cohort.toObject?.() || cohort, patients: [] }, previous);
|
|
}
|
|
|
|
// Cohort placement copied onto each patient so the record matches the cohort.
|
|
export function cohortPatientFields(cohort) {
|
|
const start = toYmd(cohort.startDate);
|
|
return {
|
|
matchingStatus: 'assigned',
|
|
assignedEducator: cohort.virtualPrt?._id || cohort.virtualPrt,
|
|
cohort: cohort._id,
|
|
slot: cohort.slot || '',
|
|
preferredWeekday: start ? weekdayOf(start) : null,
|
|
sessionDate: cohort.startDate || null,
|
|
sessionLanguage: cohort.language || '',
|
|
diseaseType: cohort.diseaseType || '',
|
|
};
|
|
}
|
|
|
|
export async function applyCohortToPatients(cohort) {
|
|
const ids = (cohort.patients || []).map((item) => item._id || item).filter(Boolean);
|
|
if (!ids.length || !cohort._id) return;
|
|
await OnboardedPatient.updateMany({ _id: { $in: ids } }, { $set: cohortPatientFields(cohort) });
|
|
}
|
|
|
|
// Creates confirmed patients for a manual cohort and returns their ids.
|
|
export async function createManualCohortPatients(cohort, rows, userId) {
|
|
const list = (Array.isArray(rows) ? rows : []).map((row) => ({
|
|
name: String(row?.name || '').trim(),
|
|
mobile: String(row?.mobile || '').trim(),
|
|
age: row?.age ?? '',
|
|
gender: String(row?.gender || '').trim(),
|
|
whatsappOptOut: Boolean(row?.whatsappOptOut),
|
|
})).filter((row) => row.name || row.mobile);
|
|
if (!list.length) return [];
|
|
if (list.some((row) => !row.name || !isMobileNumber(row.mobile))) throw Boom.badRequest('Each patient needs a name and a 10 digit mobile.');
|
|
const hashes = list.map((row) => hashMobile(row.mobile));
|
|
if (hashes.some((hash) => !hash)) throw Boom.badRequest('Each patient needs a mobile number.');
|
|
if (new Set(hashes).size !== hashes.length) throw Boom.badRequest('Each patient needs a different mobile.');
|
|
const taken = await OnboardedPatient.find({ mobileHash: { $in: hashes } }).select('_id');
|
|
if (taken.length) throw Boom.badRequest('A patient with that mobile is already in the system.');
|
|
const placement = cohortPatientFields(cohort);
|
|
const created = [];
|
|
try {
|
|
for (const row of list) {
|
|
const doc = await OnboardedPatient.create({
|
|
...applyOnboardedPii({}, row),
|
|
...placement,
|
|
paymentStatus: 'Paid',
|
|
onboardedBy: userId,
|
|
reviewStatus: 'confirmed',
|
|
reviewedBy: userId,
|
|
reviewedAt: new Date(),
|
|
whatsappOptOut: row.whatsappOptOut,
|
|
});
|
|
created.push(doc._id);
|
|
}
|
|
} catch (err) {
|
|
if (created.length) await OnboardedPatient.deleteMany({ _id: { $in: created } });
|
|
throw err;
|
|
}
|
|
return created;
|
|
}
|