UNPKG

nightscout

Version:

Nightscout acts as a web-based CGM (Continuous Glucose Monitor) to allow multiple caregivers to remotely view a patients glucose data in realtime.

567 lines (460 loc) 20.6 kB
'use strict'; var moment = require('moment-timezone'); var _ = require('lodash'); var parse_duration = require('parse-duration'); // https://www.npmjs.com/package/parse-duration var times = require('../times'); var consts = require('../constants'); var Storages = require('js-storage'); function init (client, $) { var careportal = {}; var translate = client.translate; var storage = Storages.localStorage; var units = client.settings.units; var eventTime = $('#eventTimeValue'); var eventDate = $('#eventDateValue'); function setDateAndTime (time) { time = time || moment(); eventTime.val(time.format('HH:mm')); eventDate.val(time.format('YYYY-MM-DD')); } function mergeDateAndTime () { return client.utils.mergeInputTime(eventTime.val(), eventDate.val()); } function updateTime (ele, time) { ele.attr('oldminutes', time.minutes()); ele.attr('oldhours', time.hours()); } function maybePrevent (event) { if (event) { event.preventDefault(); } } var inputMatrix = {}; var submitHooks = {}; function refreshEventTypes() { careportal.allEventTypes = client.plugins.getAllEventTypes(client.sbx); careportal.events = _.map(careportal.allEventTypes, function each (event) { return _.pick(event, ['val', 'name']); }); inputMatrix = {}; submitHooks = {}; _.forEach(careportal.allEventTypes, function each (event) { inputMatrix[event.val] = _.pick(event, ['otp','remoteCarbs', 'remoteAbsorption', 'remoteBolus', 'bg', 'insulin', 'carbs', 'protein', 'fat', 'prebolus', 'duration', 'percent', 'absolute', 'profile', 'split', 'sensor', 'reasons', 'targets']); submitHooks[event.val] = event.submitHook; }); } refreshEventTypes(); careportal.filterInputs = function filterInputs (event) { var eventType = $('#eventType').val(); function displayType (enabled) { if (enabled) { return ''; } else { return 'none'; } } function resetIfHidden (visible, id) { if (!visible) { $(id).val(''); } } // validate the eventType input - should never hit this but bail if we do if (!Object.prototype.hasOwnProperty.call(inputMatrix, eventType)) { maybePrevent(event); return; } /* eslint-disable security/detect-object-injection */ // verified false positive by check above var reasons = inputMatrix[eventType]['reasons']; $('#reasonLabel').css('display', displayType(reasons && reasons.length > 0)); $('#targets').css('display', displayType(inputMatrix[eventType]['targets'])); $('#otpLabel').css('display', displayType(inputMatrix[eventType]['otp'])); $('#remoteCarbsLabel').css('display', displayType(inputMatrix[eventType]['remoteCarbs'])); $('#remoteAbsorptionLabel').css('display', displayType(inputMatrix[eventType]['remoteAbsorption'])); $('#remoteBolusLabel').css('display', displayType(inputMatrix[eventType]['remoteBolus'])); $('#bg').css('display', displayType(inputMatrix[eventType]['bg'])); $('#insulinGivenLabel').css('display', displayType(inputMatrix[eventType]['insulin'])); $('#carbsGivenLabel').css('display', displayType(inputMatrix[eventType]['carbs'])); $('#proteinGivenLabel').css('display', displayType(inputMatrix[eventType]['protein'])); $('#fatGivenLabel').css('display', displayType(inputMatrix[eventType]['fat'])); $('#sensorInfo').css('display', displayType(inputMatrix[eventType]['sensor'])); $('#durationLabel').css('display', displayType(inputMatrix[eventType]['duration'])); $('#percentLabel').css('display', displayType(inputMatrix[eventType]['percent'] && $('#absolute').val() === '')); $('#absoluteLabel').css('display', displayType(inputMatrix[eventType]['absolute'] && $('#percent').val() === '')); $('#profileLabel').css('display', displayType(inputMatrix[eventType]['profile'])); $('#preBolusLabel').css('display', displayType(inputMatrix[eventType]['prebolus'])); $('#insulinSplitLabel').css('display', displayType(inputMatrix[eventType]['split'])); $('#reason').empty(); _.each(reasons, function eachReason (reason) { $('#reason').append('<option value="' + reason.name + '">' + translate(reason.displayName || reason.name) + '</option>'); }); careportal.reasonable(); resetIfHidden(inputMatrix[eventType]['otp'], '#otp'); resetIfHidden(inputMatrix[eventType]['remoteCarbs'], '#remoteCarbs'); resetIfHidden(inputMatrix[eventType]['remoteAbsorption'], '#remoteAbsorption'); resetIfHidden(inputMatrix[eventType]['remoteBolus'], '#remoteBolus'); resetIfHidden(inputMatrix[eventType]['insulin'], '#insulinGiven'); resetIfHidden(inputMatrix[eventType]['carbs'], '#carbsGiven'); resetIfHidden(inputMatrix[eventType]['protein'], '#proteinGiven'); resetIfHidden(inputMatrix[eventType]['fat'], '#fatGiven'); resetIfHidden(inputMatrix[eventType]['sensor'], '#sensorCode'); resetIfHidden(inputMatrix[eventType]['sensor'], '#transmitterId'); resetIfHidden(inputMatrix[eventType]['duration'], '#duration'); resetIfHidden(inputMatrix[eventType]['absolute'], '#absolute'); resetIfHidden(inputMatrix[eventType]['percent'], '#percent'); resetIfHidden(inputMatrix[eventType]['prebolus'], '#preBolus'); resetIfHidden(inputMatrix[eventType]['split'], '#insulinSplitNow'); resetIfHidden(inputMatrix[eventType]['split'], '#insulinSplitExt'); /* eslint-enable security/detect-object-injection */ // verified false positive maybePrevent(event); }; careportal.reasonable = function reasonable () { var eventType = $('#eventType').val(); var reasons = []; // validate the eventType input before getting the reasons list if (Object.prototype.hasOwnProperty.call(inputMatrix, eventType)) { /* eslint-disable-next-line security/detect-object-injection */ // verified false positive reasons = inputMatrix[eventType]['reasons']; } var selected = $('#reason').val(); var reason = _.find(reasons, function matches (r) { return r.name === selected; }); if (reason && reason.targetTop) { $('#targetTop').val(reason.targetTop); } else { $('#targetTop').val(''); } if (reason && reason.targetBottom) { $('#targetBottom').val(reason.targetBottom); } else { $('#targetBottom').val(''); } if (reason) { if (reason.duration) { $('#duration').val(reason.duration); } else { $('#duration').val(''); } } }; careportal.prepareEvents = function prepareEvents () { $('#eventType').empty(); _.each(careportal.events, function eachEvent (event) { $('#eventType').append('<option value="' + event.val + '">' + translate(event.name) + '</option>'); }); $('#eventType').change(careportal.filterInputs); $('#reason').change(careportal.reasonable); $('#percent').on('input', careportal.filterInputs); $('#absolute').on('input', careportal.filterInputs); $('#insulinSplitNow').on('input', careportal.adjustSplit); $('#insulinSplitExt').on('input', careportal.adjustSplit); careportal.filterInputs(); careportal.adjustSplit(); }; careportal.adjustSplit = function adjustSplit (event) { if ($(this).attr('id') === 'insulinSplitNow') { var nowval = parseInt($('#insulinSplitNow').val()) || 0; $('#insulinSplitExt').val(100 - nowval); $('#insulinSplitNow').val(nowval); } else { var extval = parseInt($('#insulinSplitExt').val()) || 0; $('#insulinSplitNow').val(100 - extval); $('#insulinSplitExt').val(extval); } maybePrevent(event); }; careportal.resolveEventName = function resolveEventName (value) { _.each(careportal.events, function eachEvent (e) { if (e.val === value) { value = e.name; } }); return value; }; careportal.prepare = function prepare () { refreshEventTypes(); $('#profile').empty(); client.profilefunctions.listBasalProfiles().forEach(function(p) { $('#profile').append('<option val="' + p + '">' + p + '</option>'); }); careportal.prepareEvents(); $('#eventType').val('<none>'); $('#glucoseValue').val('').attr('placeholder', translate('Value in') + ' ' + client.settings.units); $('#meter').prop('checked', true); $('#otp').val(''); $('#remoteCarbs').val(''); $('#remoteAbsorption').val(''); $('#remoteBolus').val(''); $('#carbsGiven').val(''); $('#proteinGiven').val(''); $('#fatGiven').val(''); $('#sensorCode').val(''); $('#transmitterId').val(''); $('#insulinGiven').val(''); $('#duration').val(''); $('#percent').val(''); $('#absolute').val(''); $('#profile').val(client.profilefunctions.activeProfileToTime()); $('#preBolus').val(0); $('#notes').val(''); $('#enteredBy').val(client.authorized ? client.authorized.sub : storage.get('enteredBy') || ''); $('#nowtime').prop('checked', true); setDateAndTime(); }; function gatherData () { var eventType = $('#eventType').val(); var selectedReason = $('#reason').val(); var data = { enteredBy: $('#enteredBy').val() , eventType: eventType , otp: $('#otp').val() , remoteCarbs: $('#remoteCarbs').val() , remoteAbsorption: $('#remoteAbsorption').val() , remoteBolus: $('#remoteBolus').val() , glucose: $('#glucoseValue').val().replace(',', '.') , reason: selectedReason , targetTop: $('#targetTop').val().replace(',', '.') , targetBottom: $('#targetBottom').val().replace(',', '.') , glucoseType: $('#treatment-form').find('input[name=glucoseType]:checked').val() , carbs: $('#carbsGiven').val() , protein: $('#proteinGiven').val() , fat: $('#fatGiven').val() , sensorCode: $('#sensorCode').val() , transmitterId: $('#transmitterId').val() , insulin: $('#insulinGiven').val() , duration: times.msecs(parse_duration($('#duration').val())).mins < 1 ? $('#duration').val() : times.msecs(parse_duration($('#duration').val())).mins , percent: $('#percent').val() , profile: $('#profile').val() , preBolus: $('#preBolus').val() , notes: $('#notes').val() , units: client.settings.units }; data.preBolus = parseInt(data.preBolus); if (isNaN(data.preBolus)) { delete data.preBolus; } var reasons = []; // validate the eventType input before getting the reasons list if (Object.prototype.hasOwnProperty.call(inputMatrix, eventType)) { /* eslint-disable-next-line security/detect-object-injection */ // verified false positive reasons = inputMatrix[eventType]['reasons']; } var reason = _.find(reasons, function matches (r) { return r.name === selectedReason; }); if (reason) { data.reasonDisplay = reason.displayName; } if (units == "mmol") { data.targetTop = data.targetTop * consts.MMOL_TO_MGDL; data.targetBottom = data.targetBottom * consts.MMOL_TO_MGDL; } //special handling for absolute to support temp to 0 var absolute = $('#absolute').val(); if ('' !== absolute && !isNaN(absolute)) { data.absolute = Number(absolute); } if ($('#othertime').is(':checked')) { data.eventTime = mergeDateAndTime().toDate(); } data.created_at = data.eventTime ? data.eventTime.toISOString() : new Date().toISOString(); if (!inputMatrix[data.eventType].profile) { delete data.profile; } if (data.eventType.indexOf('Temp Basal') > -1) { data.eventType = 'Temp Basal'; } if (data.eventType.indexOf('Temporary Target Cancel') > -1) { data.duration = 0; data.eventType = 'Temporary Target'; data.targetBottom = ""; data.targetTop = ""; } if (data.eventType.indexOf('Combo Bolus') > -1) { data.splitNow = parseInt($('#insulinSplitNow').val()) || 0; data.splitExt = parseInt($('#insulinSplitExt').val()) || 0; } let d = {}; Object.keys(data).forEach(function(key) { /* eslint-disable security/detect-object-injection */ // verified false positive if (data[key] !== "" && data[key] !== null) { d[key] = data[key] } /* eslint-enable security/detect-object-injection */ // verified false positive }); return d; } careportal.save = function save (event) { var data = gatherData(); confirmPost(data); maybePrevent(event); }; function validateData (data) { let allOk = true; let messages = []; console.log('Validating careportal entry: ', data.eventType); if (data.duration !== 0 && data.eventType == 'Temporary Target') { if (isNaN(data.targetTop) || isNaN(data.targetBottom) || !data.targetBottom || !data.targetTop) { console.log('Bottom or Top target missing'); allOk = false; messages.push("Please enter a valid value for both top and bottom target to save a Temporary Target"); } else { let targetTop = parseInt(data.targetTop); let targetBottom = parseInt(data.targetBottom); let minTarget = 4 * consts.MMOL_TO_MGDL; let maxTarget = 18 * consts.MMOL_TO_MGDL; if (units == "mmol") { targetTop = Math.round(targetTop / consts.MMOL_TO_MGDL * 10) / 10; targetBottom = Math.round(targetBottom / consts.MMOL_TO_MGDL * 10) / 10; minTarget = Math.round(minTarget / consts.MMOL_TO_MGDL * 10) / 10; maxTarget = Math.round(maxTarget / consts.MMOL_TO_MGDL * 10) / 10; } if (targetTop > maxTarget) { allOk = false; messages.push("Temporary target high is too high"); } if (targetBottom < minTarget) { allOk = false; messages.push("Temporary target low is too low"); } if (targetTop < targetBottom || targetBottom > targetTop) { allOk = false; messages.push("The low target must be lower than the high target and high target must be higher than the low target."); } } } // TODO: add check for remote (Bolus, Carbs, Absorption) return { allOk , messages }; } function buildConfirmText (data) { var text = [ translate('Please verify that the data entered is correct') + ': ' , translate('Event Type') + ': ' + translate(careportal.resolveEventName(data.eventType)) ]; function pushIf (check, valueText) { if (check) { text.push(valueText); } } if (data.duration === 0 && data.eventType === 'Temporary Target') { text[text.length - 1] += ' ' + translate('Cancel'); } pushIf(data.remoteCarbs, translate('Remote Carbs') + ': ' + data.remoteCarbs); pushIf(data.remoteAbsorption, translate('Remote Absorption') + ': ' + data.remoteAbsorption); pushIf(data.remoteBolus, translate('Remote Bolus') + ': ' + data.remoteBolus); pushIf(data.otp, translate('One Time Pascode') + ': ' + data.otp); pushIf(data.glucose, translate('Blood Glucose') + ': ' + data.glucose); pushIf(data.glucose, translate('Measurement Method') + ': ' + translate(data.glucoseType)); pushIf(data.reason, translate('Reason') + ': ' + data.reason); var targetTop = data.targetTop; var targetBottom = data.targetBottom; if (units == "mmol") { targetTop = Math.round(data.targetTop / consts.MMOL_TO_MGDL * 10) / 10; targetBottom = Math.round(data.targetBottom / consts.MMOL_TO_MGDL * 10) / 10; } pushIf(data.targetTop, translate('Target Top') + ': ' + targetTop); pushIf(data.targetBottom, translate('Target Bottom') + ': ' + targetBottom); pushIf(data.carbs, translate('Carbs Given') + ': ' + data.carbs); pushIf(data.protein, translate('Protein Given') + ': ' + data.protein); pushIf(data.fat, translate('Fat Given') + ': ' + data.fat); pushIf(data.sensorCode, translate('Sensor Code') + ': ' + data.sensorCode); pushIf(data.transmitterId, translate('Transmitter ID') + ': ' + data.transmitterId); pushIf(data.insulin, translate('Insulin Given') + ': ' + data.insulin); pushIf(data.eventType === 'Combo Bolus', translate('Combo Bolus') + ': ' + data.splitNow + '% : ' + data.splitExt + '%'); pushIf(data.duration, translate('Duration') + ': ' + data.duration + ' ' + translate('mins')); pushIf(data.percent, translate('Percent') + ': ' + data.percent); pushIf('absolute' in data, translate('Basal value') + ': ' + data.absolute); pushIf(data.profile, translate('Profile') + ': ' + data.profile); pushIf(data.preBolus, translate('Carb Time') + ': ' + data.preBolus + ' ' + translate('mins')); pushIf(data.notes, translate('Notes') + ': ' + data.notes); pushIf(data.enteredBy, translate('Entered By') + ': ' + data.enteredBy); text.push(translate('Event Time') + ': ' + (data.eventTime ? data.eventTime.toLocaleString() : new Date().toLocaleString())); return text.join('\n'); } function confirmPost (data) { const validation = validateData(data); if (!validation.allOk) { let messages = ""; validation.messages.forEach(function(m) { messages += translate(m) + "\n"; }); window.alert(messages); } else { if (window.confirm(buildConfirmText(data))) { var submitHook = submitHooks[data.eventType]; if (submitHook) { submitHook(client, data, function (error) { if (error) { console.log("submit error = ", error); alert(translate('Error') + ': ' + error); } else { client.browserUtils.closeDrawer('#treatmentDrawer'); } }); } else { postTreatment(data); } } } } function postTreatment (data) { if (data.eventType === 'Combo Bolus') { data.enteredinsulin = data.insulin; data.insulin = data.enteredinsulin * data.splitNow / 100; data.relative = data.enteredinsulin * data.splitExt / 100 / data.duration * 60; } $.ajax({ method: 'POST' , url: '/api/v1/treatments/' , headers: client.headers() , data: data }).done(function treatmentSaved (response) { console.info('treatment saved', response); }).fail(function treatmentSaveFail (response) { console.info('treatment saved', response); alert(translate('Entering record failed') + '. ' + translate('Status') + ': ' + response.status); }); storage.set('enteredBy', data.enteredBy); client.browserUtils.closeDrawer('#treatmentDrawer'); } careportal.dateTimeFocus = function dateTimeFocus (event) { $('#othertime').prop('checked', true); updateTime($(this), mergeDateAndTime()); maybePrevent(event); }; careportal.dateTimeChange = function dateTimeChange (event) { $('#othertime').prop('checked', true); var ele = $(this); var merged = mergeDateAndTime(); if (ele.attr('oldminutes') === '59' && merged.minutes() === 0) { merged.add(1, 'hours'); } if (ele.attr('oldminutes') === '0' && merged.minutes() === 59) { merged.add(-1, 'hours'); } setDateAndTime(merged); updateTime(ele, merged); maybePrevent(event); }; careportal.eventTimeTypeChange = function eventTimeTypeChange (event) { if ($('#othertime').is(':checked')) { eventTime.focus(); } else { setDateAndTime(); } maybePrevent(event); }; careportal.toggleDrawer = function toggleDrawer (event) { client.browserUtils.toggleDrawer('#treatmentDrawer', careportal.prepare); maybePrevent(event); }; $('#treatmentDrawerToggle').click(careportal.toggleDrawer); $('#treatmentDrawer').find('button').click(careportal.save); $('#eventTime').find('input:radio').change(careportal.eventTimeTypeChange); $('.eventinput').focus(careportal.dateTimeFocus).change(careportal.dateTimeChange); return careportal; } module.exports = init;