UNPKG

reallycare-shared-meds-utils

Version:

Utils for handling meds on backend and frontend

257 lines (252 loc) 13 kB
(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 }); }));