From fb7b1dd96951a756dbe15702875163a229e3948d Mon Sep 17 00:00:00 2001 From: eternal-flame-AD Date: Fri, 18 Nov 2022 23:37:47 -0600 Subject: [PATCH] fix health offset calculation --- internal/health/api.go | 107 ++++- internal/health/api_compliance.go | 56 +++ internal/health/compliance.go | 197 +++++----- internal/health/db.go | 56 ++- internal/health/template.go | 33 ++ internal/servetpl/middleware.go | 2 +- webroot/includes/page-health-meds.tpl.html | 430 ++++++++++++++------- webroot/lib/lib.js | 6 +- 8 files changed, 621 insertions(+), 266 deletions(-) create mode 100644 internal/health/template.go diff --git a/internal/health/api.go b/internal/health/api.go index 727e496..df8a3ee 100644 --- a/internal/health/api.go +++ b/internal/health/api.go @@ -1,15 +1,19 @@ package health import ( + "log" "sync" + "time" "github.com/eternal-flame-AD/yoake/internal/auth" "github.com/eternal-flame-AD/yoake/internal/comm" + "github.com/eternal-flame-AD/yoake/internal/comm/model" "github.com/eternal-flame-AD/yoake/internal/db" + "github.com/eternal-flame-AD/yoake/internal/util" "github.com/labstack/echo/v4" ) -func Register(g *echo.Group, db db.DB, comm *comm.Communicator) { +func Register(g *echo.Group, database db.DB, comm *comm.Communicator) { megsG := g.Group("/meds") { shortHands := megsG.Group("/shorthand") @@ -23,22 +27,109 @@ func Register(g *echo.Group, db db.DB, comm *comm.Communicator) { writeMutex := new(sync.Mutex) directions := megsG.Group("/directions", auth.RequireMiddleware(auth.RoleAdmin)) { - directions.GET("", RESTMedGetDirections(db)) - directions.POST("", RESTMedPostDirections(db, writeMutex)) - directions.DELETE("/:name", RESTMedDeleteDirections(db, writeMutex)) + directions.GET("", RESTMedGetDirections(database)) + directions.POST("", RESTMedPostDirections(database, writeMutex)) + directions.DELETE("/:name", RESTMedDeleteDirections(database, writeMutex)) } compliance := megsG.Group("/compliance", auth.RequireMiddleware(auth.RoleAdmin)) { complianceByMed := compliance.Group("/med/:med") { - complianceByMed.GET("/log", RESTComplianceLogGet(db)) - complianceByMed.GET("/project", RESTComplianceLogProjectMed(db)) + complianceByMed.GET("/log", RESTComplianceLogGet(database)) + complianceByMed.GET("/project", RESTComplianceLogProjectMed(database)) + } - compliance.GET("/log", RESTComplianceLogGet(db)) + compliance.GET("/log", RESTComplianceLogGet(database)) - compliance.POST("/log", RESTComplianceLogPost(db, writeMutex)) + compliance.POST("/log", RESTComplianceLogPost(database, writeMutex)) + + compliance.POST("/recalc", RESTRecalcMedComplianceLog(database, writeMutex)) } } + + go func() { + ticker := time.NewTicker(5 * time.Minute) + notified := make(map[string]time.Time) + for { + func() { + txn := database.NewTransaction(false) + defer txn.Discard() + + existingNotified := make(map[string]time.Time) + err := db.GetJSON(txn, []byte("health_meds_compliance_notified_meds"), &existingNotified) + + if err != nil && !db.IsNotFound(err) { + log.Println("Error getting notified meds: ", err) + return + } + for k, v := range existingNotified { + o := notified[k] + if o.Before(v) { + notified[k] = v + } + } + txn.Discard() + txn = database.NewTransaction(true) + err = db.SetJSON(txn, []byte("health_meds_compliance_notified_meds"), notified) + if err != nil { + log.Println("Error setting notified meds: ", err) + return + } else if err := txn.Commit(); err != nil { + log.Println("Error committing notified meds: ", err) + return + } + }() + + meds, err := DBMedListGet(database) + if err != nil { + log.Println("Error getting med list:", err) + continue + } + + logs, err := DBMedComplianceLogGet(database, util.DateRangeAround(time.Now(), 1)) + if err != nil { + log.Println("Error getting med compliance log:", err) + continue + } + + var notifications []CommCtx + + hasNew := false + for _, med := range meds { + nextDose := logs.ProjectNextDose(med) + if nextDose.Expected.Time.Before(time.Now()) { + if lastNotified, ok := notified[med.KeyName()]; !ok || + lastNotified.Before(nextDose.Expected.Time) || + lastNotified.Add(4*time.Hour).Before(time.Now()) { + { + if !util.Contain(med.Flags, DirectionFlagPRN) { + hasNew = true + } + } + notifications = append(notifications, CommCtx{ + Med: med, + Dose: nextDose, + }) + } + } + } + if hasNew { + if err := comm.SendGenericMessage("gotify", model.GenericMessage{ + Subject: "Medications Due", + Body: commTemplate, + MIME: "text/markdown+html/template", + Context: notifications, + }, true); err != nil { + log.Println("Error sending med compliance notification:", err) + } + for _, n := range notifications { + notified[n.Med.KeyName()] = time.Now() + } + } + + <-ticker.C + } + }() } diff --git a/internal/health/api_compliance.go b/internal/health/api_compliance.go index 1e4388c..c6719fe 100644 --- a/internal/health/api_compliance.go +++ b/internal/health/api_compliance.go @@ -11,6 +11,8 @@ import ( "github.com/labstack/echo/v4" ) +const yearAbsZero = 2000 + func RESTComplianceLogGet(database db.DB) func(c echo.Context) error { return func(c echo.Context) error { filterKeyname := c.Param("med") @@ -124,3 +126,57 @@ func RESTComplianceLogProjectMed(db db.DB) func(c echo.Context) error { return c.JSON(200, complianceLog.ProjectNextDose(*dir)) } } + +func RESTRecalcMedComplianceLog(db db.DB, writeMutex *sync.Mutex) func(c echo.Context) error { + return func(c echo.Context) error { + meds, err := DBMedListGet(db) + if err != nil { + return echoerror.NewHttp(500, err) + } + + from := time.Date(yearAbsZero, 1, 1, 0, 0, 0, 0, time.UTC) + to := time.Now() + if fromStr := c.QueryParam("from"); fromStr != "" { + from, err = time.Parse("2006-01", fromStr) + if err != nil { + return echoerror.NewHttp(400, err) + } + } + if toStr := c.QueryParam("to"); toStr != "" { + to, err = time.Parse("2006-01", toStr) + if err != nil { + return echoerror.NewHttp(400, err) + } + } + + writeMutex.Lock() + defer writeMutex.Unlock() + for year := from.Year(); year <= to.Year(); year++ { + for month := 1; month <= 12; month++ { + if year == from.Year() && month < int(from.Month()) { + continue + } + if year == to.Year() && month > int(to.Month()) { + continue + } + + log, err := DBMedComplianceLogGet(db, util.DateRangeAround(time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC), 1)) + if err != nil { + return echoerror.NewHttp(500, err) + } + if len(log) == 0 { + continue + } + + for _, dir := range meds { + log.UpdateDoseOffset(dir) + } + if err := DBMedComplianceLogAppend(db, log); err != nil { + return echoerror.NewHttp(500, err) + } + } + } + + return c.NoContent(204) + } +} diff --git a/internal/health/compliance.go b/internal/health/compliance.go index 1dc0be2..deb1d9b 100644 --- a/internal/health/compliance.go +++ b/internal/health/compliance.go @@ -1,11 +1,14 @@ package health import ( + "encoding/json" + "log" "math" "sort" "time" "github.com/eternal-flame-AD/yoake/internal/util" + "github.com/google/uuid" ) type ComplianceLog struct { @@ -17,7 +20,9 @@ type ComplianceLog struct { // 0 = closest to expected time +1 = closest to next expected dose // get a cumsum of this to get a compliance stat - DoseOffset float64 `json:"dose_offset"` + DoseOffset f64OrNan `json:"dose_offset"` + + EffectiveLastDose *ComplianceLog `json:"effective_last_dose,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -28,122 +33,124 @@ type ComplianceDoseInfo struct { Dose int `json:"dose"` } -type ComplianceLogList []ComplianceLog +type f64OrNan float64 -func (c ComplianceLogList) ProjectNextDose(dir Direction) (nextDose ComplianceLog) { - sort.Sort(c) - - var lastDose ComplianceLog - var cumDosage int - for ptr := 0; ptr < len(c); ptr++ { - if c[ptr].MedKeyname == dir.KeyName() { - if dir.OptSchedule == OptScheduleWholeDose { - lastDose = c[ptr] - break - } else { - cumDosage += c[ptr].Actual.Dose - if cumDosage < dir.Dosage { - continue - } else { - lastDose = c[ptr] - break - } - } - } +func (n *f64OrNan) UnmarshalJSON(b []byte) error { + if string(b) == "null" { + *n = f64OrNan(math.NaN()) + return nil } - if lastDose.UUID == "" /* not found */ { - nextDose = ComplianceLog{ - MedKeyname: dir.KeyName(), - Expected: ComplianceDoseInfo{ - Time: time.Now(), - Dose: dir.Dosage, - }, - Actual: ComplianceDoseInfo{ - Time: time.Now(), - Dose: dir.Dosage, - }, - DoseOffset: 0, - CreatedAt: time.Now(), - } - } else { - nextDose = ComplianceLog{ - MedKeyname: dir.KeyName(), - Expected: ComplianceDoseInfo{ - Time: lastDose.Actual.Time.Add(time.Duration(dir.PeriodHours) * time.Hour), - Dose: dir.Dosage, - }, - Actual: ComplianceDoseInfo{ - Time: time.Now(), - Dose: dir.Dosage, - }, - CreatedAt: time.Now(), - } - nextDose.DoseOffset, _, _ = c.ComputeDoseOffset(dir, &nextDose) + var f float64 + if err := json.Unmarshal(b, &f); err != nil { + return err } - return + *n = f64OrNan(f) + return nil } -func (c ComplianceLogList) ComputeDoseOffset(dir Direction, newLog *ComplianceLog) (float64, bool, error) { - sort.Sort(c) +func (n f64OrNan) MarshalJSON() ([]byte, error) { + if math.IsNaN(float64(n)) { + return []byte("null"), nil + } + return json.Marshal(float64(n)) +} - var lastTwoDoses []ComplianceLog - if newLog != nil { - lastTwoDoses = []ComplianceLog{*newLog} - } - for ptr := 0; len(lastTwoDoses) < 2 && ptr < len(c); ptr++ { - if c[ptr].MedKeyname == dir.KeyName() { - if len(lastTwoDoses) == 0 || lastTwoDoses[0].Actual.Time.After(c[ptr].Actual.Time) { - lastTwoDoses = append(lastTwoDoses, c[ptr]) - } - } - } - if newLog != nil { - if newLog.Expected.Dose == 0 && dir.KeyName() == newLog.MedKeyname { - newLog.Expected.Dose = dir.Dosage - } - if newLog.Expected.Time.IsZero() { - if len(lastTwoDoses) == 2 { - newLog.Expected.Time = lastTwoDoses[1].Actual.Time.Add(time.Duration(dir.PeriodHours) * time.Hour) - } else { - newLog.Expected.Time = newLog.Actual.Time - } - } - lastTwoDoses[0] = *newLog - } - if len(lastTwoDoses) < 2 { - return 0, false, nil +func doseOffset(dir Direction, this ComplianceLog, last ComplianceLog) float64 { + if last.UUID == "" { + return math.NaN() } - // now we have: - // *exp ~actual - // * ~ ~ * - // offset = (new_actual - last_expected) / diff(new_expected, last_actual) - 1 - - if lastTwoDoses[0].Actual.Time.IsZero() { - lastTwoDoses[0].Actual.Time = time.Now() - } - offset := float64(lastTwoDoses[0].Actual.Time.Sub(lastTwoDoses[1].Expected.Time))/ - float64(lastTwoDoses[0].Expected.Time.Sub(lastTwoDoses[1].Actual.Time)) - 1 + offset := float64(this.Actual.Time.Sub(last.Actual.Time))/ + float64(time.Duration(dir.PeriodHours)*time.Hour) - 1 // for prn ignore positive offsets if util.Contain(dir.Flags, DirectionFlagPRN) { if offset > 0 { - offset = 0 + return 0 } } // ad lib ignore negative offsets if util.Contain(dir.Flags, DirectionFlagAdLib) { if offset < 0 { - offset = 0 + return 0 } } - if math.Abs(offset) > 2 { - // stop counting if three or more doses are missed - return 0, false, nil + return offset +} + +type ComplianceLogList []ComplianceLog + +func (c ComplianceLogList) findEffectiveLastDose(dir Direction, this ComplianceLog) ComplianceLog { + // for ad lib directions, this finds the last dose + // for default scheduling, this find the earliest dose that does not cumulatively exceed a whole dose + + var lastDose ComplianceLog + var cumDosage int + for ptr := 0; ptr < len(c); ptr++ { + if c[ptr].MedKeyname == dir.KeyName() && c[ptr].Actual.Time.Before(this.Actual.Time) { + if dir.OptSchedule == OptScheduleWholeDose { + return c[ptr] + } + + cumDosage += c[ptr].Actual.Dose + if cumDosage > dir.Dosage { + return lastDose + } else if cumDosage == dir.Dosage { + return c[ptr] + } + lastDose = c[ptr] + + } + } + return lastDose +} + +func (c ComplianceLogList) ProjectNextDose(dir Direction) (nextDose ComplianceLog) { + tmpUUID := uuid.New().String() + + nextDose = ComplianceLog{ + UUID: tmpUUID, + MedKeyname: dir.KeyName(), + Expected: ComplianceDoseInfo{ + Time: time.Now(), + Dose: dir.Dosage, + }, + Actual: ComplianceDoseInfo{ + Time: time.Now(), + Dose: dir.Dosage, + }, + DoseOffset: 0, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + lastDose := c.findEffectiveLastDose(dir, nextDose) + if lastDose.UUID == "" /* not found */ { + return + } + + nextDose.EffectiveLastDose = &lastDose + nextDose.Expected.Time = lastDose.Actual.Time.Add(time.Duration(dir.PeriodHours) * time.Hour) + nextDose.DoseOffset = f64OrNan(doseOffset(dir, nextDose, lastDose)) + return +} + +func (c ComplianceLogList) UpdateDoseOffset(dir Direction) { + sort.Sort(c) + + for i := range c { + if c[i].MedKeyname == dir.KeyName() { + lastDose, thisDose := c.findEffectiveLastDose(dir, c[i]), c[i] + if lastDose.UUID == "" /* not found */ { + return + } + + c[i].DoseOffset = f64OrNan(doseOffset(dir, thisDose, lastDose)) + log.Printf("thisDose: %+v, \nlastDose: %+v\n-->offset: %f\n", thisDose, lastDose, c[i].DoseOffset) + } } - return offset, true, nil } func (c ComplianceLogList) Len() int { diff --git a/internal/health/db.go b/internal/health/db.go index 298a0d5..0b6e9b1 100644 --- a/internal/health/db.go +++ b/internal/health/db.go @@ -68,6 +68,45 @@ func DBMedComplianceLogGet(database db.DB, dates util.DateRange) (ComplianceLogL return res, nil } +func DBMedComplianceLogAppend(database db.DB, pending ComplianceLogList) error { + txn := database.NewTransaction(true) + defer txn.Discard() + + for len(pending) > 0 { + index := pending[0].Actual.Time.UTC().Format("2006-01") + var origLogs ComplianceLogList + if err := db.GetJSON(txn, []byte(dbMedComplianceLogPrefix+index), &origLogs); db.IsNotFound(err) { + origLogs = []ComplianceLog{} + } else if err != nil { + return err + } + + for i := len(pending) - 1; i >= 0; i-- { + if pending[i].Actual.Time.UTC().Format("2006-01") != index { + continue + } + origLogs = append(origLogs, pending[i]) + pending = append(pending[:i], pending[i+1:]...) + uuidMap := make(map[string]int) + for j := len(origLogs) - 1; j >= 0; j-- { + if _, ok := uuidMap[origLogs[j].UUID]; ok { + origLogs = append(origLogs[:j], origLogs[j+1:]...) + } else { + uuidMap[origLogs[j].UUID] = j + } + } + } + + sort.Sort(origLogs) + + if err := db.SetJSON(txn, []byte(dbMedComplianceLogPrefix+index), origLogs); err != nil { + return err + } + } + + return txn.Commit() +} + func DBMedComplianceLogSetOne(database db.DB, dir Direction, log *ComplianceLog) error { index := log.Actual.Time.UTC().Format("2006-01") @@ -111,6 +150,7 @@ func DBMedComplianceLogSetOne(database db.DB, dir Direction, log *ComplianceLog) log.UpdatedAt = time.Now() existingLogs[foundIdx] = *log } + sort.Sort(existingLogs) if err := db.SetJSON(txn, []byte(dbMedComplianceLogPrefix+index), existingLogs); err != nil { return err } @@ -131,6 +171,11 @@ func DBMedComplianceLogSetOne(database db.DB, dir Direction, log *ComplianceLog) if log.UUID == "" { log.UUID = uuid.New().String() log.CreatedAt = time.Now() + if log.Expected.Dose == 0 { + nextDose := existingLogs.ProjectNextDose(dir) + log.Expected.Time = nextDose.Expected.Time + log.Expected.Dose = nextDose.Expected.Dose + } } log.UpdatedAt = time.Now() @@ -142,17 +187,10 @@ func DBMedComplianceLogSetOne(database db.DB, dir Direction, log *ComplianceLog) } else { logs = append(logs, *log) } - for i := len(logs) - 1; i >= 0; i-- { - offset, upd, err := logs.ComputeDoseOffset(dir, &logs[i]) - if err != nil { - return err - } - if upd { - logs[i].DoseOffset = offset - } - } + logs.UpdateDoseOffset(dir) sort.Sort(ComplianceLogList(logs)) + if err := db.SetJSON(txn, []byte(dbMedComplianceLogPrefix+index), logs); err != nil { return err } diff --git a/internal/health/template.go b/internal/health/template.go new file mode 100644 index 0000000..72892ae --- /dev/null +++ b/internal/health/template.go @@ -0,0 +1,33 @@ +package health + +const commTemplate = ` +The following doses are due: + +{{ range . }} +--- +### {{ .Med.Name }} _{{ .Med.DirectionShorthand }}_ + +{{ $isPRN := false -}} +{{- range .Med.Flags -}} +{{- if eq . "prn" -}} +{{- $isPRN = true -}} +{{end}}{{- end -}} + +{{ if not $isPRN -}} +Expected at: {{ .Dose.Expected.Time }} + +{{if .Dose.EffectiveLastDose -}} +Last Taken at: {{ .Dose.EffectiveLastDose.Actual.Time }} + +{{ end -}} +Offset: {{ .Dose.DoseOffset }} +{{ else -}} +avail as PRN +{{- end }} +{{ end }} +` + +type CommCtx struct { + Med Direction + Dose ComplianceLog +} diff --git a/internal/servetpl/middleware.go b/internal/servetpl/middleware.go index b9ee9c4..4aa4650 100644 --- a/internal/servetpl/middleware.go +++ b/internal/servetpl/middleware.go @@ -133,7 +133,7 @@ func ServeTemplateDir(dir string) echo.MiddlewareFunc { tplPath := filepath.Join(dir, tplName) if _, err := os.Stat(tplPath); err == nil { - log.Printf("dispatch template: %s(%s) ext=%s", tplName, tplPath, ext) + //log.Printf("dispatch template: %s(%s) ext=%s", tplName, tplPath, ext) // template file is still there, execute if ext == ".html" { return func(wr io.Writer, data any) error { return templates.ExecuteTemplate(wr, tplName, data) } diff --git a/webroot/includes/page-health-meds.tpl.html b/webroot/includes/page-health-meds.tpl.html index 63dc6df..947b5aa 100644 --- a/webroot/includes/page-health-meds.tpl.html +++ b/webroot/includes/page-health-meds.tpl.html @@ -65,7 +65,8 @@ - + @@ -78,10 +79,11 @@
History
-
+
+ @@ -93,6 +95,8 @@ + +
Action Time Dose OffsetLoading...
@@ -112,123 +116,236 @@

Manage

-
-
- - +
+
+

+ +

+
+ +
+ + - - - - - - - - - - -
-
- + + + + + + + + + + +
+
+ -
- - -
-
- - -
-
- - -
-
- - -
-
-
- -
- - -
-
- - +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ + +
+
+ + +
+
+ + + + +
- +
+

+ +

+
+
+
+ + - - +
+
+
- }) - }); - $("#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(); - }, - }) - }) -
@@ -261,7 +378,7 @@ } const medTakeForm = accEl.querySelector(".med-take-form") medTakeForm.querySelector("input#dosage").value = med.dosage - const medTimeline = (() => { + let medTimeline = (() => { let timeline = accEl.querySelector("#timeline") const medTimelineTpl = accEl.querySelector("#timeline-tpl") if (timeline) { @@ -312,22 +429,25 @@ }, success: function (data) { let icon = accEl.querySelector(".accordion-icon") - let important = false - let available = false + let important = dayjs().isAfter(dayjs(data.expected.time)) && !prn; + let available = data.dose_offset > -0.2 || adlib; icon.setAttribute("class", "accordion-icon") - if (data.dose_offset < -0.2 || dayjs().isAfter(dayjs(data.expected.time).add(1, "day"))) { + + + if (important) { + available = true + accEl.setAttribute("data-weight", 50) + important = !adlib + icon.classList.add("trima-procedure-optimal") + } else 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") @@ -375,47 +495,57 @@ tbody.innerHTML = ""; let projectedTr = document.createElement("tr"); projectedTr.classList.add("table-primary"); - projectedTr.innerHTML = ``; - labelTimeElement(projectedTr.children[0], data.expected.time); - projectedTr.children[1].innerText = `${data.expected.dose} ${med.dosage_unit} (${(prn || adlib) ? "available" : "scheduled"})`; + projectedTr.innerHTML = ``; + projectedTr.children[0].innerHTML = ``; + projectedTr.querySelector("button").onclick = e => { + document.querySelector("#addMed #name").value = med.name + " " + med.shorthand; + document.querySelector("#addMed #name").dispatchEvent(new CustomEvent("change")); + } + labelTimeElement(projectedTr.children[1], data.expected.time, "YY-MM-DD HH:mm"); + projectedTr.children[2].innerText = `${data.expected.dose} ${med.dosage_unit} (${(prn || adlib) ? "available" : "scheduled"})`; + projectedTr.children[3].innerText = data.dose_offset; tbody.appendChild(projectedTr); logs.forEach(log => { const tr = document.createElement("tr"); - tr.innerHTML = ``; - 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.innerHTML = ``; + tr.children[0].innerHTML = ``; + tr.querySelector("button").onclick = e => { + window.loadMedLogForm(log) } - 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) { + labelTimeElement(tr.children[1], log.actual.time, "YY-MM-DD HH:mm") + tr.children[2].innerText = `${log.actual.dose}/${log.expected.dose} ${med.dosage_unit}`; + if (log.actual.dose !== log.expected.dose) { 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].innerText = log.dose_offset?.toFixed(2); + if (Math.abs(log.dose_offset) > 0.5) { tr.children[3].classList.add("table-danger"); - } else if (Math.abs(offset) > 0.5) { + } else if (Math.abs(log.dose_offset) > 0.2) { tr.children[3].classList.add("table-warning"); } else { tr.children[3].classList.add("table-success"); } + + // compute 7 day offset + const weekFrom = dayjs(log.actual.time).subtract(7, 'day') + let offset = log.dose_offset + logs.forEach(logI => { + if (dayjs(logI.actual.time).isAfter(weekFrom) && dayjs(logI.actual.time).isBefore(log.actual.time)) { + offset += logI.dose_offset + } + }) + + tr.children[4].innerText = offset.toFixed(2); + if (Math.abs(offset) > 1) { + tr.children[4].classList.add("table-danger"); + } else if (Math.abs(offset) > 0.5) { + tr.children[4].classList.add("table-warning"); + } else { + tr.children[4].classList.add("table-success"); + } tbody.appendChild(tr); }); @@ -468,7 +598,7 @@ fill = "" break } - let arrow = $("." + baseClass).clone() + let arrow = $(".d-none." + baseClass).first().clone() if (fill) arrow.attr("fill", fill) arrow.removeClass("d-none") @@ -513,14 +643,14 @@ dataType: "json", success: function (data) { writeAccordion(data, true).then(() => { - updateTimer = setInterval(() => writeAccordion(data).catch(err => { + updateTimer = setTimeout(() => writeAccordion(data).catch(err => { throw err }), 300 * 1000) }) } }) } else { - clearInterval(updateTimer); + clearTimeout(updateTimer); } }) })() diff --git a/webroot/lib/lib.js b/webroot/lib/lib.js index 4641ff0..d2ac642 100644 --- a/webroot/lib/lib.js +++ b/webroot/lib/lib.js @@ -3,11 +3,11 @@ function doNow(fn) { return fn; } -function labelTimeElement(tag, time) { +function labelTimeElement(tag, time, format) { time = dayjs(time); - if (tag.innerText == "") - tag.innerText = time.fromNow(); + if (tag.innerText == "" || format) + tag.innerText = format ? time.format(format) : time.fromNow(); tag.setAttribute("data-bs-toggle", "tooltip"); tag.setAttribute("data-bs-title", time.format("L LT")); new bootstrap.Tooltip(tag);