reallycare-shared-meds-utils
Version:
Utils for handling meds on backend and frontend
257 lines (252 loc) • 13 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = global || self, factory(global.RCSharedMeds = {}));
}(this, function (exports) { 'use strict';
const sameId = function (a, b) {
if (!a || !b) {
return false;
}
return (a.toString() === b.toString());
};
const isRegular = function isRegular(item) {
return (!(item.when && (item.when.oneOff || item.when.parent)) && !item.status);
};
const format = function (d) {
return d.toString().split(' (')[0];
};
const arrangeSessions = function (hours, timezone, start, end, meds, responsibility, RRule, _) {
function addMedToSession(medRecord, addToSession) {
let sortPos = _.sortedIndexBy(addToSession.meds, medRecord, function (aMed) {
let actionCode = '0';
switch (aMed.action) {
case 'Assist':
actionCode = '1';
break;
case 'Prompt':
actionCode = '2';
break;
default:
break;
}
return actionCode + (aMed.blisterPack ? '1' : (aMed.isCream ? '3' : '2')) + (aMed.prn ? '1' : '0') + aMed.medDesc + '#' + aMed._id;
});
addToSession.meds.splice(sortPos, 0, medRecord);
}
function addResponsibilityToSession(responsibilityRecord, addToSession) {
addToSession.responsibility.push(responsibilityRecord);
}
function getMedsFromSession(aSession) {
return aSession.meds;
}
function getResponsibilityFromSession(aSession) {
return aSession.responsibility;
}
const sessions = {};
const baseRule = timezone ? {
timezone: timezone
} : {};
// Process some data (medications, or responsibility), looking only for recurring
// records and expanding these out across all sessions where they will fall
function expandRegularRecords(records, addFunc) {
for (let record of records) {
if (record.when && record.when.freq) {
const ruleOptions = _.extend({}, baseRule, record.when);
['dtstart', 'until'].forEach((key) => {
if (record.when[key]) {
ruleOptions[key] = new Date(record.when[key]);
}
});
// Fix minutes to 0 as timezones (such as Asia/Kolkata) with offsets including part hours were not working
if (ruleOptions.byhour) {
ruleOptions.byminute = [0];
ruleOptions.bysecond = [0];
}
const recordRule = new RRule(ruleOptions);
const instanceDates = recordRule.between(start, end, true);
for (let instanceDate of instanceDates)
addFunc(record, sessions[format(instanceDate)]);
}
}
}
// Process the data for a second pass, this time looking for instances (exceptions)
// and one-offs
function processInstancesAndOneOffs(records, addFunc, getSessionRecordsFunc) {
for (let record of records) {
if (record.when) {
// It's a regular. We've already processed these, so we don't need to add anything
// more to sessions for this.
if (record.when.freq) {
// However, we can reduce the payload by deleting redundant info, keeping the
// dtstart (so we know what the minimum until should be if we edit the
// recurrence later)
record.firstoccur = record.when.dtstart;
delete record.when;
}
// It's an instance of a regular. The regulars are already projected out in
// sessions as if there are never any exceptions. Now we have found an exception,
// we'll need to go back and rethink that...
else if (record.when.parent && record.when.recurId) {
const whenOccur = RRule.prototype.createDate(record.when.recurId, timezone);
// find the meds session relevant to this particular occurrence of the regular
const session = sessions[format(whenOccur)];
if (!session) {
/*
Came across this happening when testing, and switching timezones of organisation / browser etc
Not great practice, it turns out.
*/
throw new Error(`Unable to locate time for medication ${record._id} in slots. Time was ${format(whenOccur)}. Contact your support provider`);
}
// pull out the records for this session that we're actually interested in
// (be that meds or responsibility)
const sessionRecords = getSessionRecordsFunc(session);
// within these records, find the index of the regular record which this is
// an occurrence of
const slotPos = _.findIndex(sessionRecords, function (regRecord) {
if (sameId(regRecord._id, record.when.parent)) {
return true;
}
else if (regRecord.when && sameId(regRecord.when.parent, record.when.parent)) {
// This is to handle a hopefully historical condition. Would not expect to see if after July 2016
console.log('Unexpected: Medication has multiple status updates : ', JSON.stringify(regRecord, null, 2), JSON.stringify(record, null, 2));
return true;
}
else {
return false;
}
});
// if we have found that index (which I think we always should), then...
if (slotPos !== -1) {
// overwrite it if this instance is expected to occur...
if (record.when.dtstart) {
sessionRecords[slotPos] = record;
}
// ...or delete it if it has been cancelled
else {
sessionRecords.splice(slotPos, 1);
}
}
else if (whenOccur < new Date(2016, 10, 12)) ;
else if (whenOccur < start) {
// We should be filtering these out earlier - try and find out why we aren't
console.log(`Early med caught:\nWhenOccur: ${format(whenOccur)}\n\nSessions:\n${Object.keys(sessions).join('\n')}\nMed:\n${JSON.stringify(record, null, 2)}\n\nSlot:\n${JSON.stringify(session, null, 2)}`);
}
else if (whenOccur > end) ;
else {
throw new Error(`Unable to calculate slotPos in medications service getPersonSlotsInPeriod\nWhenOccur: ${format(whenOccur)}\n\nSessions:\n${Object.keys(sessions).join('\n')}\nMed:\n${JSON.stringify(record, null, 2)}\n\nSlot:\n${JSON.stringify(session, null, 2)}`);
}
}
// It's a one-off, so we just need to add this
else if (record.when.oneOff) {
let oneOffOccur = RRule.prototype.createDate(record.when.dtstart, timezone);
addFunc(record, sessions[format(oneOffOccur)]);
}
else {
throw new Error('Unexpected case in medications service getPersonSlotsInPeriod');
}
}
}
}
const allSessions = new RRule(_.extend({}, baseRule, {
freq: 3,
dtstart: new Date(2000, 0, 1),
byhour: hours,
byminute: [0],
bysecond: [0]
}));
_.each(allSessions.between(start, end, true), function (startTime) {
sessions[format(startTime)] = {
meds: [],
responsibility: []
};
});
// we're doing essentially the same thing with responsibility as we are with
// meds, hence the opportunity to abstract the two-pass logic used.
// however, as the result goes to a different place, we need callbacks for
// the reading and writing of meds and responsibility data from the
// (initially empty) IMedsSession objects created above
expandRegularRecords(meds, addMedToSession);
processInstancesAndOneOffs(meds, addMedToSession, getMedsFromSession);
expandRegularRecords(responsibility, addResponsibilityToSession);
processInstancesAndOneOffs(responsibility, addResponsibilityToSession, getResponsibilityFromSession);
return sessions;
};
const slotNumbers = function slotNumbers(meds, historical) {
const TABLETS = 0;
const PRN_MEDS = 1;
const HIST_MAND_MISSED = 2; // historical mandatory meds missed
const HIST_PRN_TAKEN = 3; // historical prn meds taken
const HIST_MAND_NO_RECORD = 4; // historical mandatory not recorded
const NEW_ALERTS = 5;
const HANDLED_ALERTS = 6;
const ASSIST = 7;
const PROMPT = 8;
const MISSED_ACTION = 9;
let numbers = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
function checkMissedAction(med, prev) {
if (historical && !med.status) {
prev[MISSED_ACTION] = 1;
}
}
if (meds && meds.length > 0) {
numbers = meds.reduce(function (prev, med) {
const units = 1; // Count meds, rather than units, now we are reporting mls etc
switch (med.action) {
case 'Assist':
prev[ASSIST] = 1;
checkMissedAction(med, prev);
break;
case 'Prompt':
prev[PROMPT] = 1;
checkMissedAction(med, prev);
break;
default:
if (historical && med.monitor && med.status === 'Taken') {
prev[HIST_PRN_TAKEN] = prev[HIST_PRN_TAKEN] + 1;
}
if (med.prn) {
prev[PRN_MEDS] = prev[PRN_MEDS] + units;
}
else {
prev[TABLETS] = prev[TABLETS] + units;
if (historical) {
let arrayOffset = null;
switch (med.status) {
case undefined:
arrayOffset = HIST_MAND_NO_RECORD;
break;
case 'Missed':
arrayOffset = HIST_MAND_MISSED;
break;
}
if (arrayOffset) {
prev[arrayOffset] = prev[arrayOffset] + units;
}
}
}
}
let arrayOffset = null;
switch (med.newAlert) {
case undefined:
break;
case true:
arrayOffset = NEW_ALERTS;
break;
case false:
arrayOffset = HANDLED_ALERTS;
break;
}
if (arrayOffset) {
prev[arrayOffset] = prev[arrayOffset] + units;
}
return prev;
}, numbers);
}
return numbers;
};
exports.arrangeSessions = arrangeSessions;
exports.format = format;
exports.isRegular = isRegular;
exports.slotNumbers = slotNumbers;
Object.defineProperty(exports, '__esModule', { value: true });
}));