fix health offset calculation
This commit is contained in:
parent
3284792aa7
commit
fb7b1dd969
8 changed files with 621 additions and 266 deletions
|
@ -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
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,31 +33,85 @@ type ComplianceDoseInfo struct {
|
|||
Dose int `json:"dose"`
|
||||
}
|
||||
|
||||
type f64OrNan float64
|
||||
|
||||
func (n *f64OrNan) UnmarshalJSON(b []byte) error {
|
||||
if string(b) == "null" {
|
||||
*n = f64OrNan(math.NaN())
|
||||
return nil
|
||||
}
|
||||
var f float64
|
||||
if err := json.Unmarshal(b, &f); err != nil {
|
||||
return err
|
||||
}
|
||||
*n = f64OrNan(f)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n f64OrNan) MarshalJSON() ([]byte, error) {
|
||||
if math.IsNaN(float64(n)) {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
return json.Marshal(float64(n))
|
||||
}
|
||||
|
||||
func doseOffset(dir Direction, this ComplianceLog, last ComplianceLog) float64 {
|
||||
if last.UUID == "" {
|
||||
return math.NaN()
|
||||
}
|
||||
|
||||
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 {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// ad lib ignore negative offsets
|
||||
if util.Contain(dir.Flags, DirectionFlagAdLib) {
|
||||
if offset < 0 {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
return offset
|
||||
}
|
||||
|
||||
type ComplianceLogList []ComplianceLog
|
||||
|
||||
func (c ComplianceLogList) ProjectNextDose(dir Direction) (nextDose ComplianceLog) {
|
||||
sort.Sort(c)
|
||||
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() {
|
||||
if c[ptr].MedKeyname == dir.KeyName() && c[ptr].Actual.Time.Before(this.Actual.Time) {
|
||||
if dir.OptSchedule == OptScheduleWholeDose {
|
||||
lastDose = c[ptr]
|
||||
break
|
||||
} else {
|
||||
return c[ptr]
|
||||
}
|
||||
|
||||
cumDosage += c[ptr].Actual.Dose
|
||||
if cumDosage < dir.Dosage {
|
||||
continue
|
||||
} else {
|
||||
if cumDosage > dir.Dosage {
|
||||
return lastDose
|
||||
} else if cumDosage == dir.Dosage {
|
||||
return c[ptr]
|
||||
}
|
||||
lastDose = c[ptr]
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
return lastDose
|
||||
}
|
||||
}
|
||||
if lastDose.UUID == "" /* not found */ {
|
||||
|
||||
func (c ComplianceLogList) ProjectNextDose(dir Direction) (nextDose ComplianceLog) {
|
||||
tmpUUID := uuid.New().String()
|
||||
|
||||
nextDose = ComplianceLog{
|
||||
UUID: tmpUUID,
|
||||
MedKeyname: dir.KeyName(),
|
||||
Expected: ComplianceDoseInfo{
|
||||
Time: time.Now(),
|
||||
|
@ -64,86 +123,34 @@ func (c ComplianceLogList) ProjectNextDose(dir Direction) (nextDose ComplianceLo
|
|||
},
|
||||
DoseOffset: 0,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: 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)
|
||||
}
|
||||
|
||||
lastDose := c.findEffectiveLastDose(dir, nextDose)
|
||||
if lastDose.UUID == "" /* not found */ {
|
||||
return
|
||||
}
|
||||
|
||||
func (c ComplianceLogList) ComputeDoseOffset(dir Direction, newLog *ComplianceLog) (float64, bool, error) {
|
||||
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)
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// for prn ignore positive offsets
|
||||
if util.Contain(dir.Flags, DirectionFlagPRN) {
|
||||
if offset > 0 {
|
||||
offset = 0
|
||||
c[i].DoseOffset = f64OrNan(doseOffset(dir, thisDose, lastDose))
|
||||
log.Printf("thisDose: %+v, \nlastDose: %+v\n-->offset: %f\n", thisDose, lastDose, c[i].DoseOffset)
|
||||
}
|
||||
}
|
||||
|
||||
// ad lib ignore negative offsets
|
||||
if util.Contain(dir.Flags, DirectionFlagAdLib) {
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
}
|
||||
|
||||
if math.Abs(offset) > 2 {
|
||||
// stop counting if three or more doses are missed
|
||||
return 0, false, nil
|
||||
}
|
||||
return offset, true, nil
|
||||
}
|
||||
|
||||
func (c ComplianceLogList) Len() int {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
33
internal/health/template.go
Normal file
33
internal/health/template.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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) }
|
||||
|
|
|
@ -65,7 +65,8 @@
|
|||
<input type="checkbox" class="form-check-input"
|
||||
id="time-override">
|
||||
</span>
|
||||
<input type="datetime-local" class="form-control" id="time">
|
||||
<input type="datetime-local" class="form-control" id="time"
|
||||
disabled>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
@ -78,10 +79,11 @@
|
|||
<div class="p-2">
|
||||
|
||||
<h5>History</h5>
|
||||
<div class="p-2 table-responsive" style="height:20em;overflow-y:scroll;">
|
||||
<div class="p-2 table-responsive" style="height:20em;overflow:scroll;">
|
||||
<table class="table table-striped compliance-log">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Action</th>
|
||||
<th scope="col">Time</th>
|
||||
<th scope="col">Dose</th>
|
||||
<th scope="col">Offset</th>
|
||||
|
@ -93,6 +95,8 @@
|
|||
<th class="placeholder">Loading...</th>
|
||||
<td class="placeholder"></td>
|
||||
<td class="placeholder"></td>
|
||||
<td class="placeholder"></td>
|
||||
<td class="placeholder"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -112,14 +116,27 @@
|
|||
<h3 class="card-title">Manage</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="accordion" id="medManageAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="addMedHeading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#addMedCollapse" aria-expanded="true"
|
||||
aria-controls="addMedCollapse">
|
||||
Edit Direction
|
||||
</button>
|
||||
</h2>
|
||||
<div id="addMedCollapse" class="accordion-collapse collapse"
|
||||
data-bs-parent="#medManageAccordion">
|
||||
<form id="addMed" autocomplete="off">
|
||||
<div class="mb-3">
|
||||
<label id="med-shorthand-input" for="shorthand" class="form-label">Shorthand</label>
|
||||
<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">
|
||||
<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>
|
||||
|
@ -133,33 +150,36 @@
|
|||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
|
@ -230,6 +250,103 @@
|
|||
})
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="medManageLog">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#medManageLogCollapse" aria-expanded="false"
|
||||
aria-controls="medManageLogCollapse">
|
||||
Manage Medication Log
|
||||
</button>
|
||||
</h2>
|
||||
<div id="medManageLogCollapse" class="accordion-collapse collapse"
|
||||
data-bs-parent="#medManageAccordion">
|
||||
<form id="editMedLog" autocomplete="off">
|
||||
<div class="mb-3">
|
||||
<label id="log-keyname-input" for="shorthand" class="form-label">Med Key
|
||||
name:</label>
|
||||
<input type="text" class="form-control" id="key-name"
|
||||
placeholder="atorvastatin">
|
||||
|
||||
<label for="uuid" class="form-label">Dose UUID:</label>
|
||||
|
||||
<input type="text" class="form-control" id="uuid" placeholder="UUID">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="expected-time" class="form-label">Expected Time:</label>
|
||||
<input type="datetime-local" class="form-control" id="expected-time"
|
||||
placeholder="2021-01-01T00:00:00">
|
||||
<label for="expected-dose" class="form-label">Expected Dose:</label>
|
||||
<input type="text" class="form-control" id="expected-dose" placeholder="10">
|
||||
</div>
|
||||
|
||||
<label for="dose-offset" class="form-label">Dose Offset:</label>
|
||||
<input type="text" class="form-control" id="dose-offset" placeholder="+0.1">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="actual-time" class="form-label">Actual Time:</label>
|
||||
<input type="datetime-local" class="form-control" id="actual-time"
|
||||
placeholder="2021-01-01T00:00:00">
|
||||
<label for="actual-dose" class="form-label">Actual Dose:</label>
|
||||
<input type="text" class="form-control" id="actual-dose" placeholder="10">
|
||||
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
|
||||
</form>
|
||||
<script>
|
||||
(() => {
|
||||
|
||||
window.loadMedLogForm = log => {
|
||||
const form = $("#editMedLog");
|
||||
form.find("#key-name").val(log.med_keyname);
|
||||
form.find("#uuid").val(log.uuid);
|
||||
form.find("#expected-time").val(dayjs(log.expected.time).format("YYYY-MM-DDTHH:mm:ss"));
|
||||
form.find("#expected-dose").val(log.expected.dose);
|
||||
form.find("#dose-offset").val(log.dose_offset);
|
||||
form.find("#actual-time").val(dayjs(log.actual.time).format("YYYY-MM-DDTHH:mm:ss"));
|
||||
form.find("#actual-dose").val(log.actual.dose);
|
||||
}
|
||||
document.getElementById("editMedLog").onsubmit = function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = $("#editMedLog");
|
||||
data = {
|
||||
med_keyname: form.find("#key-name").val(),
|
||||
uuid: form.find("#uuid").val(),
|
||||
expected: {
|
||||
time: form.find("#expected-time").valueAsDate,
|
||||
dose: parseInt(form.find("#expected-dose").val())
|
||||
},
|
||||
dose_offset: parseFloat(form.find("#dose-offset").val()),
|
||||
actual: {
|
||||
time: form.find("#actual-time")[0].valueAsDate,
|
||||
dose: parseInt(form.find("#actual-dose").val())
|
||||
}
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: "/api/health/meds/compliance/log",
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify(data),
|
||||
success: function (data) {
|
||||
window.location.reload();
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
})()
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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 = `<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"})`;
|
||||
projectedTr.innerHTML = `<th scope="row"></th><td></td><td></td><td></td><td></td>`;
|
||||
projectedTr.children[0].innerHTML = `<button class="btn btn-sm btn-warning" type="button" aria-expanded="false">Edit Directions</button>`;
|
||||
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 = `<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.innerHTML = `<th scope="row"></th><td></td><td></td><td></td><td></td>`;
|
||||
tr.children[0].innerHTML = `<button type="button" class="btn btn-sm btn-warning">Edit</button>`;
|
||||
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);
|
||||
}
|
||||
})
|
||||
})()
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue