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
|
package health
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/eternal-flame-AD/yoake/internal/auth"
|
"github.com/eternal-flame-AD/yoake/internal/auth"
|
||||||
"github.com/eternal-flame-AD/yoake/internal/comm"
|
"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/db"
|
||||||
|
"github.com/eternal-flame-AD/yoake/internal/util"
|
||||||
"github.com/labstack/echo/v4"
|
"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")
|
megsG := g.Group("/meds")
|
||||||
{
|
{
|
||||||
shortHands := megsG.Group("/shorthand")
|
shortHands := megsG.Group("/shorthand")
|
||||||
|
@ -23,22 +27,109 @@ func Register(g *echo.Group, db db.DB, comm *comm.Communicator) {
|
||||||
writeMutex := new(sync.Mutex)
|
writeMutex := new(sync.Mutex)
|
||||||
directions := megsG.Group("/directions", auth.RequireMiddleware(auth.RoleAdmin))
|
directions := megsG.Group("/directions", auth.RequireMiddleware(auth.RoleAdmin))
|
||||||
{
|
{
|
||||||
directions.GET("", RESTMedGetDirections(db))
|
directions.GET("", RESTMedGetDirections(database))
|
||||||
directions.POST("", RESTMedPostDirections(db, writeMutex))
|
directions.POST("", RESTMedPostDirections(database, writeMutex))
|
||||||
directions.DELETE("/:name", RESTMedDeleteDirections(db, writeMutex))
|
directions.DELETE("/:name", RESTMedDeleteDirections(database, writeMutex))
|
||||||
}
|
}
|
||||||
|
|
||||||
compliance := megsG.Group("/compliance", auth.RequireMiddleware(auth.RoleAdmin))
|
compliance := megsG.Group("/compliance", auth.RequireMiddleware(auth.RoleAdmin))
|
||||||
{
|
{
|
||||||
complianceByMed := compliance.Group("/med/:med")
|
complianceByMed := compliance.Group("/med/:med")
|
||||||
{
|
{
|
||||||
complianceByMed.GET("/log", RESTComplianceLogGet(db))
|
complianceByMed.GET("/log", RESTComplianceLogGet(database))
|
||||||
complianceByMed.GET("/project", RESTComplianceLogProjectMed(db))
|
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"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const yearAbsZero = 2000
|
||||||
|
|
||||||
func RESTComplianceLogGet(database db.DB) func(c echo.Context) error {
|
func RESTComplianceLogGet(database db.DB) func(c echo.Context) error {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
filterKeyname := c.Param("med")
|
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))
|
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
|
package health
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/eternal-flame-AD/yoake/internal/util"
|
"github.com/eternal-flame-AD/yoake/internal/util"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ComplianceLog struct {
|
type ComplianceLog struct {
|
||||||
|
@ -17,7 +20,9 @@ type ComplianceLog struct {
|
||||||
|
|
||||||
// 0 = closest to expected time +1 = closest to next expected dose
|
// 0 = closest to expected time +1 = closest to next expected dose
|
||||||
// get a cumsum of this to get a compliance stat
|
// 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"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
@ -28,122 +33,124 @@ type ComplianceDoseInfo struct {
|
||||||
Dose int `json:"dose"`
|
Dose int `json:"dose"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ComplianceLogList []ComplianceLog
|
type f64OrNan float64
|
||||||
|
|
||||||
func (c ComplianceLogList) ProjectNextDose(dir Direction) (nextDose ComplianceLog) {
|
func (n *f64OrNan) UnmarshalJSON(b []byte) error {
|
||||||
sort.Sort(c)
|
if string(b) == "null" {
|
||||||
|
*n = f64OrNan(math.NaN())
|
||||||
var lastDose ComplianceLog
|
return nil
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if lastDose.UUID == "" /* not found */ {
|
var f float64
|
||||||
nextDose = ComplianceLog{
|
if err := json.Unmarshal(b, &f); err != nil {
|
||||||
MedKeyname: dir.KeyName(),
|
return err
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
return
|
*n = f64OrNan(f)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c ComplianceLogList) ComputeDoseOffset(dir Direction, newLog *ComplianceLog) (float64, bool, error) {
|
func (n f64OrNan) MarshalJSON() ([]byte, error) {
|
||||||
sort.Sort(c)
|
if math.IsNaN(float64(n)) {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
return json.Marshal(float64(n))
|
||||||
|
}
|
||||||
|
|
||||||
var lastTwoDoses []ComplianceLog
|
func doseOffset(dir Direction, this ComplianceLog, last ComplianceLog) float64 {
|
||||||
if newLog != nil {
|
if last.UUID == "" {
|
||||||
lastTwoDoses = []ComplianceLog{*newLog}
|
return math.NaN()
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// now we have:
|
offset := float64(this.Actual.Time.Sub(last.Actual.Time))/
|
||||||
// *exp ~actual
|
float64(time.Duration(dir.PeriodHours)*time.Hour) - 1
|
||||||
// * ~ ~ *
|
|
||||||
// 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
|
// for prn ignore positive offsets
|
||||||
if util.Contain(dir.Flags, DirectionFlagPRN) {
|
if util.Contain(dir.Flags, DirectionFlagPRN) {
|
||||||
if offset > 0 {
|
if offset > 0 {
|
||||||
offset = 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ad lib ignore negative offsets
|
// ad lib ignore negative offsets
|
||||||
if util.Contain(dir.Flags, DirectionFlagAdLib) {
|
if util.Contain(dir.Flags, DirectionFlagAdLib) {
|
||||||
if offset < 0 {
|
if offset < 0 {
|
||||||
offset = 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if math.Abs(offset) > 2 {
|
return offset
|
||||||
// stop counting if three or more doses are missed
|
}
|
||||||
return 0, false, nil
|
|
||||||
|
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 {
|
func (c ComplianceLogList) Len() int {
|
||||||
|
|
|
@ -68,6 +68,45 @@ func DBMedComplianceLogGet(database db.DB, dates util.DateRange) (ComplianceLogL
|
||||||
return res, nil
|
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 {
|
func DBMedComplianceLogSetOne(database db.DB, dir Direction, log *ComplianceLog) error {
|
||||||
|
|
||||||
index := log.Actual.Time.UTC().Format("2006-01")
|
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()
|
log.UpdatedAt = time.Now()
|
||||||
existingLogs[foundIdx] = *log
|
existingLogs[foundIdx] = *log
|
||||||
}
|
}
|
||||||
|
sort.Sort(existingLogs)
|
||||||
if err := db.SetJSON(txn, []byte(dbMedComplianceLogPrefix+index), existingLogs); err != nil {
|
if err := db.SetJSON(txn, []byte(dbMedComplianceLogPrefix+index), existingLogs); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -131,6 +171,11 @@ func DBMedComplianceLogSetOne(database db.DB, dir Direction, log *ComplianceLog)
|
||||||
if log.UUID == "" {
|
if log.UUID == "" {
|
||||||
log.UUID = uuid.New().String()
|
log.UUID = uuid.New().String()
|
||||||
log.CreatedAt = time.Now()
|
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()
|
log.UpdatedAt = time.Now()
|
||||||
|
@ -142,17 +187,10 @@ func DBMedComplianceLogSetOne(database db.DB, dir Direction, log *ComplianceLog)
|
||||||
} else {
|
} else {
|
||||||
logs = append(logs, *log)
|
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))
|
sort.Sort(ComplianceLogList(logs))
|
||||||
|
|
||||||
if err := db.SetJSON(txn, []byte(dbMedComplianceLogPrefix+index), logs); err != nil {
|
if err := db.SetJSON(txn, []byte(dbMedComplianceLogPrefix+index), logs); err != nil {
|
||||||
return err
|
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)
|
tplPath := filepath.Join(dir, tplName)
|
||||||
if _, err := os.Stat(tplPath); err == nil {
|
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
|
// template file is still there, execute
|
||||||
if ext == ".html" {
|
if ext == ".html" {
|
||||||
return func(wr io.Writer, data any) error { return templates.ExecuteTemplate(wr, tplName, data) }
|
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"
|
<input type="checkbox" class="form-check-input"
|
||||||
id="time-override">
|
id="time-override">
|
||||||
</span>
|
</span>
|
||||||
<input type="datetime-local" class="form-control" id="time">
|
<input type="datetime-local" class="form-control" id="time"
|
||||||
|
disabled>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -78,10 +79,11 @@
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
|
|
||||||
<h5>History</h5>
|
<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">
|
<table class="table table-striped compliance-log">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th scope="col">Action</th>
|
||||||
<th scope="col">Time</th>
|
<th scope="col">Time</th>
|
||||||
<th scope="col">Dose</th>
|
<th scope="col">Dose</th>
|
||||||
<th scope="col">Offset</th>
|
<th scope="col">Offset</th>
|
||||||
|
@ -93,6 +95,8 @@
|
||||||
<th class="placeholder">Loading...</th>
|
<th class="placeholder">Loading...</th>
|
||||||
<td class="placeholder"></td>
|
<td class="placeholder"></td>
|
||||||
<td class="placeholder"></td>
|
<td class="placeholder"></td>
|
||||||
|
<td class="placeholder"></td>
|
||||||
|
<td class="placeholder"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -112,123 +116,236 @@
|
||||||
<h3 class="card-title">Manage</h3>
|
<h3 class="card-title">Manage</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="addMed" autocomplete="off">
|
<div class="accordion" id="medManageAccordion">
|
||||||
<div class="mb-3">
|
<div class="accordion-item">
|
||||||
<label id="med-shorthand-input" for="shorthand" class="form-label">Shorthand</label>
|
<h2 class="accordion-header" id="addMedHeading">
|
||||||
<input type="text" class="form-control" id="shorthand"
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||||
placeholder="Atorvastatin 10mg TAB 20mg PO qAM">
|
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>
|
||||||
|
<input type="text" class="form-control" id="shorthand"
|
||||||
|
placeholder="Atorvastatin 10mg TAB 20mg PO qAM">
|
||||||
|
|
||||||
<label for="name" class="form-label">Name: </label>
|
<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"
|
||||||
<label for="dosage" class="form-label">Dosage: </label>
|
placeholder="Atorvastatin 10mg TAB">
|
||||||
<input type="number" class="form-control" id="dosage" placeholder="20">
|
<label for="dosage" class="form-label">Dosage: </label>
|
||||||
<label for="dosage_unit" class="form-label">Dosage Unit: </label>
|
<input type="number" class="form-control" id="dosage" placeholder="20">
|
||||||
<input type="text" class="form-control" id="dosage_unit" placeholder="mg">
|
<label for="dosage_unit" class="form-label">Dosage Unit: </label>
|
||||||
<label for="dosage_route" class="form-label">Dosage Route: </label>
|
<input type="text" class="form-control" id="dosage_unit" placeholder="mg">
|
||||||
<input type="text" class="form-control" id="dosage_route" placeholder="PO">
|
<label for="dosage_route" class="form-label">Dosage Route: </label>
|
||||||
<label for="period_hours" class="form-label">Period (Hours): </label>
|
<input type="text" class="form-control" id="dosage_route" placeholder="PO">
|
||||||
<input type="number" class="form-control" id="period_hours" placeholder="24">
|
<label for="period_hours" class="form-label">Period (Hours): </label>
|
||||||
</div>
|
<input type="number" class="form-control" id="period_hours" placeholder="24">
|
||||||
<div class="mb-3">
|
</div>
|
||||||
<label for="flags" class="form-label">Flags:</label>
|
<div class="mb-3">
|
||||||
|
<label for="flags" class="form-label">Flags:</label>
|
||||||
|
|
||||||
<div class="form-check form-check-inline">
|
<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"
|
||||||
<label for="flags-qam" class="form-check-label">qAM</label>
|
value="qam">
|
||||||
</div>
|
<label for="flags-qam" class="form-check-label">qAM</label>
|
||||||
<div class="form-check form-check-inline">
|
</div>
|
||||||
<input type="checkbox" class="form-check-input" id="flags-qhs" name="qHS" value="qhs">
|
<div class="form-check form-check-inline">
|
||||||
<label for="flags-qhs" class="form-check-label">qHS</label>
|
<input type="checkbox" class="form-check-input" id="flags-qhs" name="qHS"
|
||||||
</div>
|
value="qhs">
|
||||||
<div class="form-check form-check-inline">
|
<label for="flags-qhs" class="form-check-label">qHS</label>
|
||||||
<input type="checkbox" class="form-check-input" id="flags-prn" name="PRN" value="prn">
|
</div>
|
||||||
<label for="flags-prn" class="form-check-label">PRN</label>
|
<div class="form-check form-check-inline">
|
||||||
</div>
|
<input type="checkbox" class="form-check-input" id="flags-prn" name="PRN"
|
||||||
<div class="form-check form-check-inline">
|
value="prn">
|
||||||
<input type="checkbox" class="form-check-input" id="flags-adlib" name="adlib"
|
<label for="flags-prn" class="form-check-label">PRN</label>
|
||||||
value="ad lib">
|
</div>
|
||||||
<label for="flags-adlib" class="form-check-label">ad lib</label>
|
<div class="form-check form-check-inline">
|
||||||
</div>
|
<input type="checkbox" class="form-check-input" id="flags-adlib"
|
||||||
</div>
|
name="adlib" value="ad lib">
|
||||||
<div class="mb-3">
|
<label for="flags-adlib" class="form-check-label">ad lib</label>
|
||||||
<label for="flags" class="form-label">Schedule:</label>
|
</div>
|
||||||
<div class="form-check form-check-inline">
|
</div>
|
||||||
<input type="radio" class="form-check-input" id="schedule-default" name="schedule"
|
<div class="mb-3">
|
||||||
value="default">
|
<label for="flags" class="form-label">Schedule:</label>
|
||||||
<label for="schedule-default" class="form-check-label">Default</label>
|
<div class="form-check form-check-inline">
|
||||||
</div>
|
<input type="radio" class="form-check-input" id="schedule-default"
|
||||||
<div class="form-check form-check-inline">
|
name="schedule" value="default">
|
||||||
<input type="radio" class="form-check-input" id="schedule-whole" name="schedule"
|
<label for="schedule-default" class="form-check-label">Default</label>
|
||||||
value="whole">
|
</div>
|
||||||
<label for="schedule-whole" class="form-check-label">Whole Dose</label>
|
<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>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">Submit</button>
|
<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">
|
||||||
|
|
||||||
</form>
|
<label for="uuid" class="form-label">Dose UUID:</label>
|
||||||
<script>
|
|
||||||
$("#addMed #shorthand").on('change', function () {
|
<input type="text" class="form-control" id="uuid" placeholder="UUID">
|
||||||
let shorthand = $(this).val();
|
</div>
|
||||||
$.ajax({
|
|
||||||
url: "/api/health/meds/shorthand/parse?shorthand=" + encodeURIComponent(shorthand),
|
<div class="mb-3">
|
||||||
success: function (data) {
|
<label for="expected-time" class="form-label">Expected Time:</label>
|
||||||
$("#addMed #name").val(data.name);
|
<input type="datetime-local" class="form-control" id="expected-time"
|
||||||
$("#addMed #dosage").val(data.dosage);
|
placeholder="2021-01-01T00:00:00">
|
||||||
$("#addMed #dosage_unit").val(data.dosage_unit);
|
<label for="expected-dose" class="form-label">Expected Dose:</label>
|
||||||
$("#addMed #dosage_route").val(data.dosage_route);
|
<input type="text" class="form-control" id="expected-dose" placeholder="10">
|
||||||
$("#addMed #period_hours").val(data.period_hours);
|
</div>
|
||||||
$("#addMed #flags-qam").prop("checked", data.flags.includes("qam"));
|
|
||||||
$("#addMed #flags-qhs").prop("checked", data.flags.includes("qhs"));
|
<label for="dose-offset" class="form-label">Dose Offset:</label>
|
||||||
$("#addMed #flags-prn").prop("checked", data.flags.includes("prn"));
|
<input type="text" class="form-control" id="dose-offset" placeholder="+0.1">
|
||||||
$("#addMed #flags-adlib").prop("checked", data.flags.includes("ad lib"));
|
|
||||||
$("#addMed #schedule-default").prop("checked", data.schedule == "default");
|
<div class="mb-3">
|
||||||
$("#addMed #schedule-whole").prop("checked", data.schedule == "whole");
|
<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>
|
||||||
|
|
||||||
})
|
|
||||||
});
|
|
||||||
$("#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>
|
||||||
|
@ -261,7 +378,7 @@
|
||||||
}
|
}
|
||||||
const medTakeForm = accEl.querySelector(".med-take-form")
|
const medTakeForm = accEl.querySelector(".med-take-form")
|
||||||
medTakeForm.querySelector("input#dosage").value = med.dosage
|
medTakeForm.querySelector("input#dosage").value = med.dosage
|
||||||
const medTimeline = (() => {
|
let medTimeline = (() => {
|
||||||
let timeline = accEl.querySelector("#timeline")
|
let timeline = accEl.querySelector("#timeline")
|
||||||
const medTimelineTpl = accEl.querySelector("#timeline-tpl")
|
const medTimelineTpl = accEl.querySelector("#timeline-tpl")
|
||||||
if (timeline) {
|
if (timeline) {
|
||||||
|
@ -312,22 +429,25 @@
|
||||||
},
|
},
|
||||||
success: function (data) {
|
success: function (data) {
|
||||||
let icon = accEl.querySelector(".accordion-icon")
|
let icon = accEl.querySelector(".accordion-icon")
|
||||||
let important = false
|
let important = dayjs().isAfter(dayjs(data.expected.time)) && !prn;
|
||||||
let available = false
|
let available = data.dose_offset > -0.2 || adlib;
|
||||||
icon.setAttribute("class", "accordion-icon")
|
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)
|
accEl.setAttribute("data-weight", (prn || adlib) ? 5 : 10)
|
||||||
icon.classList.add("trima-procedure-ineligible")
|
icon.classList.add("trima-procedure-ineligible")
|
||||||
} else if (data.dose_offset < 0 || adlib || (prn && data.dose_offset == 0)) {
|
} else if (data.dose_offset < 0 || adlib || (prn && data.dose_offset == 0)) {
|
||||||
available = true
|
available = true
|
||||||
accEl.setAttribute("data-weight", (prn || adlib) ? 15 : 20)
|
accEl.setAttribute("data-weight", (prn || adlib) ? 15 : 20)
|
||||||
icon.classList.add("trima-procedure-valid")
|
icon.classList.add("trima-procedure-valid")
|
||||||
} else {
|
|
||||||
available = true
|
|
||||||
accEl.setAttribute("data-weight", 50)
|
|
||||||
important = !adlib
|
|
||||||
icon.classList.add("trima-procedure-optimal")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (initial) {
|
if (initial) {
|
||||||
accEl.querySelector(".accordion-collapse").classList[important ? "add" : "remove"]("show")
|
accEl.querySelector(".accordion-collapse").classList[important ? "add" : "remove"]("show")
|
||||||
accEl.querySelector(".accordion-button").classList[important ? "remove" : "add"]("collapsed")
|
accEl.querySelector(".accordion-button").classList[important ? "remove" : "add"]("collapsed")
|
||||||
|
@ -375,47 +495,57 @@
|
||||||
tbody.innerHTML = "";
|
tbody.innerHTML = "";
|
||||||
let projectedTr = document.createElement("tr");
|
let projectedTr = document.createElement("tr");
|
||||||
projectedTr.classList.add("table-primary");
|
projectedTr.classList.add("table-primary");
|
||||||
projectedTr.innerHTML = `<th scope="row"></th><td></td><td></td><td></td>`;
|
projectedTr.innerHTML = `<th scope="row"></th><td></td><td></td><td></td><td></td>`;
|
||||||
labelTimeElement(projectedTr.children[0], data.expected.time);
|
projectedTr.children[0].innerHTML = `<button class="btn btn-sm btn-warning" type="button" aria-expanded="false">Edit Directions</button>`;
|
||||||
projectedTr.children[1].innerText = `${data.expected.dose} ${med.dosage_unit} (${(prn || adlib) ? "available" : "scheduled"})`;
|
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);
|
tbody.appendChild(projectedTr);
|
||||||
|
|
||||||
logs.forEach(log => {
|
logs.forEach(log => {
|
||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
tr.innerHTML = `<th scope="row"></th><td></td><td></td><td></td>`;
|
tr.innerHTML = `<th scope="row"></th><td></td><td></td><td></td><td></td>`;
|
||||||
labelTimeElement(tr.children[0], log.actual.time)
|
tr.children[0].innerHTML = `<button type="button" class="btn btn-sm btn-warning">Edit</button>`;
|
||||||
tr.children[1].innerText = `${log.actual.dose}/${log.expected.dose} ${med.dosage_unit}`;
|
tr.querySelector("button").onclick = e => {
|
||||||
if (log.actual.dose !== log.expected.dose) {
|
window.loadMedLogForm(log)
|
||||||
tr.children[1].classList.add("table-warning");
|
|
||||||
} else {
|
|
||||||
tr.children[1].classList.add("table-success");
|
|
||||||
}
|
}
|
||||||
tr.children[2].innerText = log.dose_offset.toFixed(2);
|
labelTimeElement(tr.children[1], log.actual.time, "YY-MM-DD HH:mm")
|
||||||
if (Math.abs(log.dose_offset) > 1) {
|
tr.children[2].innerText = `${log.actual.dose}/${log.expected.dose} ${med.dosage_unit}`;
|
||||||
tr.children[2].classList.add("table-danger");
|
if (log.actual.dose !== log.expected.dose) {
|
||||||
} else if (Math.abs(log.dose_offset) > 0.5) {
|
|
||||||
tr.children[2].classList.add("table-warning");
|
tr.children[2].classList.add("table-warning");
|
||||||
} else {
|
} else {
|
||||||
tr.children[2].classList.add("table-success");
|
tr.children[2].classList.add("table-success");
|
||||||
}
|
}
|
||||||
|
tr.children[3].innerText = log.dose_offset?.toFixed(2);
|
||||||
// compute 7 day offset
|
if (Math.abs(log.dose_offset) > 0.5) {
|
||||||
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");
|
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");
|
tr.children[3].classList.add("table-warning");
|
||||||
} else {
|
} else {
|
||||||
tr.children[3].classList.add("table-success");
|
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);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -468,7 +598,7 @@
|
||||||
fill = ""
|
fill = ""
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
let arrow = $("." + baseClass).clone()
|
let arrow = $(".d-none." + baseClass).first().clone()
|
||||||
if (fill)
|
if (fill)
|
||||||
arrow.attr("fill", fill)
|
arrow.attr("fill", fill)
|
||||||
arrow.removeClass("d-none")
|
arrow.removeClass("d-none")
|
||||||
|
@ -513,14 +643,14 @@
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
success: function (data) {
|
success: function (data) {
|
||||||
writeAccordion(data, true).then(() => {
|
writeAccordion(data, true).then(() => {
|
||||||
updateTimer = setInterval(() => writeAccordion(data).catch(err => {
|
updateTimer = setTimeout(() => writeAccordion(data).catch(err => {
|
||||||
throw err
|
throw err
|
||||||
}), 300 * 1000)
|
}), 300 * 1000)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
clearInterval(updateTimer);
|
clearTimeout(updateTimer);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})()
|
})()
|
||||||
|
|
|
@ -3,11 +3,11 @@ function doNow(fn) {
|
||||||
return fn;
|
return fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
function labelTimeElement(tag, time) {
|
function labelTimeElement(tag, time, format) {
|
||||||
time = dayjs(time);
|
time = dayjs(time);
|
||||||
|
|
||||||
if (tag.innerText == "")
|
if (tag.innerText == "" || format)
|
||||||
tag.innerText = time.fromNow();
|
tag.innerText = format ? time.format(format) : time.fromNow();
|
||||||
tag.setAttribute("data-bs-toggle", "tooltip");
|
tag.setAttribute("data-bs-toggle", "tooltip");
|
||||||
tag.setAttribute("data-bs-title", time.format("L LT"));
|
tag.setAttribute("data-bs-title", time.format("L LT"));
|
||||||
new bootstrap.Tooltip(tag);
|
new bootstrap.Tooltip(tag);
|
||||||
|
|
Loading…
Reference in a new issue