<h1 class="page-header">Med Compliance</h1> <div class="contianer"> <div class="row p-2"> <div class="col"> <div class="card border"> <div class="card-header"> <h3 class="card-title">Directions</h3> </div> <div class="card-body"> <style> .med-timeline { height: 2.5rem; flex-shrink: 3; } .med-title { flex-grow: 1; min-width: 40%; } </style> <div class="accordion" id="med-directions-accordion"> <div class="accordion-item d-none" id="med-direction-accordion-tpl" data-weight="0"> <h2 class="accordion-header trima-procedure-hidden"> <button class="accordion-button" type="button" data-bs-toggle="collapse" style="flex-wrap: wrap"> <span class="accordion-icon"> {{template "/partials/sidebar-trima-procedure-logos.tpl.html" "1.25rem" }} </span> <span class="med-title accordion-title"> <div class="fw-bold">$name</div> <div>900mg PO qHS</div> </span> <svg id="timeline-tpl" class="med-timeline d-none" viewbox="0 0 300 20" xmlns="http://www.w3.org/2000/svg"> <polygon class="arrow-flipped d-none" points="3,0 10,7 17,0" fill="" /> <rect class="bar" width="280" x="10" y="7" height="6" fill="#8A6BBE" /> <line class="line d-none" x1="10" y1="0" x2="10" y2="20" stroke="#373C38" stroke-width="1" /> <polygon class="arrow d-none" points="3,20 10,14 17,20" fill="#58B2DC" /> </svg> </button> </h2> <div class="accordion-collapse collapse show"> <div class="px-2 py-2 px-lg-5"> </div> <div class="p-2"> <form class="med-take-form px-3"> <div class="row g-3 align-items-center"> <div class="col-auto"> <label for="dosage" class="form-label">Dosage Taken: </label> </div> <div class="col-auto"> <input type="number" class="form-control" id="dosage"> </div> <div class="col-auto"> <label for="time" class="form-label">Time Override: </label> </div> <div class="col-auto"> <div class="input-group"> <span class="input-group-text"> <input type="checkbox" class="form-check-input" id="time-override"> </span> <input type="datetime-local" class="form-control" id="time"> </div> </div> <div class="col-auto"> <input type="submit" class="btn btn-primary mt-2" value="Submit"> </div> </div> </form> </div> <div class="p-2"> <h5>History</h5> <div class="p-2 table-responsive" style="height:20em;overflow-y:scroll;"> <table class="table table-striped compliance-log"> <thead> <tr> <th scope="col">Time</th> <th scope="col">Dose</th> <th scope="col">Offset</th> <th scope="col">Offset (7 day)</th> </tr> </thead> <tbody> <tr class="placeholder"> <th class="placeholder">Loading...</th> <td class="placeholder"></td> <td class="placeholder"></td> </tr> </tbody> </table> </div> </div> </div> </div> </div> </div> </div> </div> </div> <div class="row p-2"> <div class="col"> <div class="card border"> <div class="card-header"> <h3 class="card-title">Manage</h3> </div> <div class="card-body"> <form id="addMed" autocomplete="off"> <div class="mb-3"> <label id="med-shorthand-input" for="shorthand" class="form-label">Shorthand</label> <input type="text" class="form-control" id="shorthand" placeholder="Atorvastatin 10mg TAB 20mg PO qAM"> <label for="name" class="form-label">Name: </label> <input type="text" class="form-control" id="name" placeholder="Atorvastatin 10mg TAB"> <label for="dosage" class="form-label">Dosage: </label> <input type="number" class="form-control" id="dosage" placeholder="20"> <label for="dosage_unit" class="form-label">Dosage Unit: </label> <input type="text" class="form-control" id="dosage_unit" placeholder="mg"> <label for="dosage_route" class="form-label">Dosage Route: </label> <input type="text" class="form-control" id="dosage_route" placeholder="PO"> <label for="period_hours" class="form-label">Period (Hours): </label> <input type="number" class="form-control" id="period_hours" placeholder="24"> </div> <div class="mb-3"> <label for="flags" class="form-label">Flags:</label> <div class="form-check form-check-inline"> <input type="checkbox" class="form-check-input" id="flags-qam" name="qAM" value="qam"> <label for="flags-qam" class="form-check-label">qAM</label> </div> <div class="form-check form-check-inline"> <input type="checkbox" class="form-check-input" id="flags-qhs" name="qHS" value="qhs"> <label for="flags-qhs" class="form-check-label">qHS</label> </div> <div class="form-check form-check-inline"> <input type="checkbox" class="form-check-input" id="flags-prn" name="PRN" value="prn"> <label for="flags-prn" class="form-check-label">PRN</label> </div> <div class="form-check form-check-inline"> <input type="checkbox" class="form-check-input" id="flags-adlib" name="adlib" value="ad lib"> <label for="flags-adlib" class="form-check-label">ad lib</label> </div> </div> <div class="mb-3"> <label for="flags" class="form-label">Schedule:</label> <div class="form-check form-check-inline"> <input type="radio" class="form-check-input" id="schedule-default" name="schedule" value="default"> <label for="schedule-default" class="form-check-label">Default</label> </div> <div class="form-check form-check-inline"> <input type="radio" class="form-check-input" id="schedule-whole" name="schedule" value="whole"> <label for="schedule-whole" class="form-check-label">Whole Dose</label> </div> </div> <button type="submit" class="btn btn-primary">Submit</button> </form> <script> $("#addMed #shorthand").on('change', function () { let shorthand = $(this).val(); $.ajax({ url: "/api/health/meds/shorthand/parse?shorthand=" + encodeURIComponent(shorthand), success: function (data) { $("#addMed #name").val(data.name); $("#addMed #dosage").val(data.dosage); $("#addMed #dosage_unit").val(data.dosage_unit); $("#addMed #dosage_route").val(data.dosage_route); $("#addMed #period_hours").val(data.period_hours); $("#addMed #flags-qam").prop("checked", data.flags.includes("qam")); $("#addMed #flags-qhs").prop("checked", data.flags.includes("qhs")); $("#addMed #flags-prn").prop("checked", data.flags.includes("prn")); $("#addMed #flags-adlib").prop("checked", data.flags.includes("ad lib")); $("#addMed #schedule-default").prop("checked", data.schedule == "default"); $("#addMed #schedule-whole").prop("checked", data.schedule == "whole"); } }) }); $("#addMed").on("submit", function (e) { e.preventDefault(); let name = $("#addMed #name").val(); let dosage = $("#addMed #dosage").val(); let dosage_unit = $("#addMed #dosage_unit").val(); let dosage_route = $("#addMed #dosage_route").val(); let period_hours = $("#addMed #period_hours").val(); let flags = []; if ($("#addMed #flags-qam").prop("checked")) { flags.push("qam"); } if ($("#addMed #flags-qhs").prop("checked")) { flags.push("qhs"); } if ($("#addMed #flags-prn").prop("checked")) { flags.push("prn"); } if ($("#addMed #flags-adlib").prop("checked")) { flags.push("ad lib"); } let schedule = $("#addMed input[name=schedule]:checked").val(); $.ajax({ url: "/api/health/meds/directions", method: "POST", contentType: "application/json", data: JSON.stringify({ name: name, dosage: parseInt(dosage), dosage_unit: dosage_unit, dosage_route: dosage_route, period_hours: parseInt(period_hours), flags: flags, schedule: schedule }), success: function (data) { window.location.reload(); }, }) }) </script> </div> </div> </div> </div> </div> <script> (() => { "use strict"; const medKeyName = name => name.split(" ")[0].toLowerCase(); const dirAccEl = document.getElementById("med-directions-accordion") const dirAccTpl = document.getElementById("med-direction-accordion-tpl") let accByMeds = {}; const writeAccordion = async (medList, initial) => { await Promise.all( medList.map(med => new Promise((resolve, reject) => { { const prn = med.flags.includes("prn"); const adlib = med.flags.includes("ad lib"); let id = "med-direction-accordion-" + med.name let accEl = document.getElementById(id) if (!accEl) { accEl = dirAccTpl.cloneNode(true) accEl.id = id } const medTakeForm = accEl.querySelector(".med-take-form") medTakeForm.querySelector("input#dosage").value = med.dosage const medTimeline = (() => { let timeline = accEl.querySelector("#timeline") const medTimelineTpl = accEl.querySelector("#timeline-tpl") if (timeline) { timeline.parentElement.removeChild(timeline) } timeline = medTimelineTpl.cloneNode(true) medTimelineTpl.parentElement.append(timeline) timeline.id = "timeline" timeline.classList.remove("d-none") return timeline })() medTakeForm.onsubmit = e => { e.preventDefault(); let dosage = medTakeForm.querySelector("input#dosage").value let time = medTakeForm.querySelector("input#time").value if (confirm("Really Submit?")) $.ajax({ url: "/api/health/meds/compliance/log", method: "POST", contentType: "application/json", data: JSON.stringify({ med_keyname: medKeyName(med.name), actual: { time: dayjs(time || undefined).format(), dose: parseInt(dosage) }, }), success: function (data) { window.location.reload(); }, }) } accByMeds[medKeyName(med.name)] = accEl accEl.id = "med-direction-accordion-" + med.name let title = accEl.querySelector(".accordion-title") title.querySelector("div:first-child").innerText = med.name title.querySelector("div:last-child").innerText = med.shorthand const bodyId = accEl.querySelector(".accordion-collapse").id = "med-direction-body-" + medKeyName(med.name) accEl.querySelector(".accordion-button").setAttribute("data-bs-target", "#" + bodyId) accEl.querySelector(".accordion-button").setAttribute("aria-controls", bodyId) $.ajax({ url: "/api/health/meds/compliance/med/" + medKeyName(med.name) + "/project", type: "GET", dataType: "json", error: function (xhr, status, error) { reject(error) }, success: function (data) { let icon = accEl.querySelector(".accordion-icon") let important = false let available = false icon.setAttribute("class", "accordion-icon") if (data.dose_offset < -0.2 || dayjs().isAfter(dayjs(data.expected.time).add(1, "day"))) { accEl.setAttribute("data-weight", (prn || adlib) ? 5 : 10) icon.classList.add("trima-procedure-ineligible") } else if (data.dose_offset < 0 || adlib || (prn && data.dose_offset == 0)) { available = true accEl.setAttribute("data-weight", (prn || adlib) ? 15 : 20) icon.classList.add("trima-procedure-valid") } else { available = true accEl.setAttribute("data-weight", 50) important = !adlib icon.classList.add("trima-procedure-optimal") } if (initial) { accEl.querySelector(".accordion-collapse").classList[important ? "add" : "remove"]("show") accEl.querySelector(".accordion-button").classList[important ? "remove" : "add"]("collapsed") } medTakeForm.querySelector("input#dosage").value = data.expected.dose medTakeForm.querySelector("input#time").onkeyup = medTakeForm.querySelector("input#time").onchange = e => { const target = e.target if (target.validity.badInput || dayjs(target.value) > dayjs()) { target.classList.add("text-danger") target.classList.remove("text-success") medTakeForm.querySelector("input[type=submit]").disabled = true } else if (target.value == "") { target.classList.remove("text-danger") target.classList.remove("text-success") medTakeForm.querySelector("input[type=submit]").disabled = false } else if (dayjs(target.value).isValid()) { target.classList.remove("text-danger") target.classList.add("text-success") medTakeForm.querySelector("input[type=submit]").disabled = false } } medTakeForm.querySelector("input#time-override").onchange = e => { const target = e.target if (target.checked) { medTakeForm.querySelector("input#time").disabled = false medTakeForm.querySelector("input#time").value = dayjs().format("YYYY-MM-DDTHH:mm") } else { medTakeForm.querySelector("input#time").value = null medTakeForm.querySelector("input#time").disabled = true } medTakeForm.querySelector("input#time").dispatchEvent(new CustomEvent("change")) } $.ajax({ url: "/api/health/meds/compliance/med/" + medKeyName(med.name) + "/log", type: "GET", dataType: "json", error: function (xhr, status, error) { reject(error) }, success: function (logs) { const tbody = accEl.querySelector(".compliance-log tbody"); tbody.innerHTML = ""; let projectedTr = document.createElement("tr"); projectedTr.classList.add("table-primary"); projectedTr.innerHTML = `<th scope="row"></th><td></td><td></td><td></td>`; labelTimeElement(projectedTr.children[0], data.expected.time); projectedTr.children[1].innerText = `${data.expected.dose} ${med.dosage_unit} (${(prn || adlib) ? "available" : "scheduled"})`; tbody.appendChild(projectedTr); logs.forEach(log => { const tr = document.createElement("tr"); tr.innerHTML = `<th scope="row"></th><td></td><td></td><td></td>`; labelTimeElement(tr.children[0], log.actual.time) tr.children[1].innerText = `${log.actual.dose}/${log.expected.dose} ${med.dosage_unit}`; if (log.actual.dose !== log.expected.dose) { tr.children[1].classList.add("table-warning"); } else { tr.children[1].classList.add("table-success"); } tr.children[2].innerText = log.dose_offset.toFixed(2); if (Math.abs(log.dose_offset) > 1) { tr.children[2].classList.add("table-danger"); } else if (Math.abs(log.dose_offset) > 0.5) { tr.children[2].classList.add("table-warning"); } else { tr.children[2].classList.add("table-success"); } // compute 7 day offset const weekFrom = dayjs(log.actual.time).subtract(7, 'day') let offset = 0 logs.forEach(log => { if (dayjs(log.actual.time).isAfter(weekFrom)) { offset += log.dose_offset } }) tr.children[3].innerText = offset.toFixed(2); if (Math.abs(offset) > 1) { tr.children[3].classList.add("table-danger"); } else if (Math.abs(offset) > 0.5) { tr.children[3].classList.add("table-warning"); } else { tr.children[3].classList.add("table-success"); } tbody.appendChild(tr); }); // compute timeline const timelineStart = dayjs().subtract(med.period_hours * 3, 'hour') const timelineEnd = dayjs().add(med.period_hours, 'hour') let timelineDoses = [ { "type": "now", "time": dayjs(), "dose": 0 }, ] if (!(prn && available)) { timelineDoses.push({ "type": "projected", "time": data.expected.time, "dose": data.expected.dose }) } logs.forEach(log => { timelineDoses.push({ "type": "actual", "time": log.actual.time, "dose": log.actual.dose }) timelineDoses.push({ "type": "expected", "time": log.expected.time, "dose": log.expected.dose }) }) if (prn) $(medTimeline).find(".bar").attr("fill", "#B19693"); else if (adlib) $(medTimeline).find(".bar").attr("fill", "#F596AA"); timelineDoses = timelineDoses.map(dose => { dose.time = dayjs(dose.time) dose.timerel = dose.time.diff(timelineStart) / timelineEnd.diff(timelineStart) return dose }).filter(dose => { return dose.time.isAfter(timelineStart) && dose.time.isBefore(timelineEnd) }).forEach(dose => { console.log(dose) let baseClass = "" let fill = "" switch (dose.type) { case "projected": baseClass = "arrow-flipped" fill = "#ECB88A" break case "actual": baseClass = "arrow" fill = "#7BA23F" break case "expected": baseClass = "arrow-flipped" fill = "#58B2DC" break case "now": baseClass = "line" fill = "" break } let arrow = $("." + baseClass).clone() if (fill) arrow.attr("fill", fill) arrow.removeClass("d-none") const fullrange = 280 arrow.attr("transform", "translate(" + (dose.timerel * fullrange) + ", 0)") $(medTimeline).append(arrow) }) accEl.classList.remove("d-none") let inserted = false for (const node of dirAccEl.children) { if (node.classList.contains("accordion-item")) if (parseInt(node.getAttribute("data-weight")) < parseInt(accEl.getAttribute("data-weight"))) { dirAccEl.insertBefore(accEl, node) inserted = true break; } } if (!inserted) { dirAccEl.appendChild(accEl) } resolve() } }) } }) } })) ) } let updateTimer; document.addEventListener("sidebar-activate", e => { if (e.detail.page == "health-meds") { $.ajax({ url: "/api/health/meds/directions", type: "GET", dataType: "json", success: function (data) { writeAccordion(data, true).then(() => { updateTimer = setInterval(() => writeAccordion(data).catch(err => { throw err }), 300 * 1000) }) } }) } else { clearInterval(updateTimer); } }) })() </script>