185 lines
4.9 KiB
Go
185 lines
4.9 KiB
Go
package lib
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
)
|
|
|
|
// TrackerAdd inserts a new prompt. Generates TrackerID if empty.
|
|
func TrackerAdd(p *Tracker) error {
|
|
if p.TrackerID == "" {
|
|
p.TrackerID = NewID()
|
|
}
|
|
now := time.Now().Unix()
|
|
if p.CreatedAt == 0 {
|
|
p.CreatedAt = now
|
|
}
|
|
p.UpdatedAt = now
|
|
if p.Active == false && p.Dismissed == false {
|
|
p.Active = true // default to active
|
|
}
|
|
return dbSave("trackers", p)
|
|
}
|
|
|
|
// TrackerModify updates an existing prompt
|
|
func TrackerModify(p *Tracker) error {
|
|
p.UpdatedAt = time.Now().Unix()
|
|
return dbSave("trackers", p)
|
|
}
|
|
|
|
// TrackerDelete removes a prompt
|
|
func TrackerDelete(trackerID string) error {
|
|
return dbDelete("trackers", "tracker_id", trackerID)
|
|
}
|
|
|
|
// TrackerGet retrieves a single tracker by ID
|
|
func TrackerGet(trackerID string) (*Tracker, error) {
|
|
p := &Tracker{}
|
|
return p, dbLoad("trackers", trackerID, p)
|
|
}
|
|
|
|
// TrackerQueryActive retrieves active trackers due for a dossier
|
|
func TrackerQueryActive(dossierID string) ([]*Tracker, error) {
|
|
now := time.Now().Unix()
|
|
var result []*Tracker
|
|
err := dbQuery(`SELECT * FROM trackers
|
|
WHERE dossier_id = ? AND active = 1 AND dismissed = 0
|
|
AND (expires_at = 0 OR expires_at > ?)
|
|
ORDER BY
|
|
CASE WHEN next_ask <= ? OR next_ask IS NULL OR input_type = 'freeform' THEN 0 ELSE 1 END,
|
|
next_ask, time_of_day`, []any{dossierID, now, now}, &result)
|
|
return result, err
|
|
}
|
|
|
|
// TrackerQueryAll retrieves all trackers for a dossier (including inactive)
|
|
func TrackerQueryAll(dossierID string) ([]*Tracker, error) {
|
|
var result []*Tracker
|
|
err := dbQuery(`SELECT * FROM trackers WHERE dossier_id = ? ORDER BY active DESC, time_of_day, created_at`,
|
|
[]any{dossierID}, &result)
|
|
return result, err
|
|
}
|
|
|
|
// TrackerRespond records a response and advances next_ask
|
|
func TrackerRespond(trackerID string, response, responseRaw string) error {
|
|
now := time.Now().Unix()
|
|
|
|
// Get current tracker to calculate next_ask
|
|
p, err := TrackerGet(trackerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
p.LastResponse = response
|
|
p.LastResponseRaw = responseRaw
|
|
p.LastResponseAt = now
|
|
p.NextAsk = calculateNextAsk(p.Frequency, p.TimeOfDay, now)
|
|
p.UpdatedAt = now
|
|
|
|
if err := dbSave("trackers", p); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create entry for certain tracker types
|
|
if err := trackerCreateEntry(p, response, now); err != nil {
|
|
// Log but don't fail the response
|
|
log.Printf("Failed to create entry for tracker %s: %v", trackerID, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// trackerCreateEntry creates an entry from a tracker response
|
|
// Uses the prompt's category/type directly - no hardcoded mappings
|
|
func trackerCreateEntry(p *Tracker, response string, timestamp int64) error {
|
|
// Skip freeform/note types for now
|
|
if p.InputType == "freeform" {
|
|
return nil
|
|
}
|
|
|
|
// Entry inherits category/type from prompt
|
|
e := &Entry{
|
|
DossierID: p.DossierID,
|
|
Category: CategoryFromString[p.Category], // Prompt still uses string, convert here
|
|
Type: p.Type,
|
|
Value: responseToValue(response),
|
|
Timestamp: timestamp,
|
|
Data: fmt.Sprintf(`{"response":%s,"source":"prompt","tracker_id":"%s"}`, response, p.TrackerID),
|
|
SearchKey: p.TrackerID, // Foreign key to link entry back to its prompt
|
|
}
|
|
return EntryAdd(e)
|
|
}
|
|
|
|
// responseToValue converts JSON response to a human-readable value string
|
|
func responseToValue(response string) string {
|
|
var resp map[string]interface{}
|
|
if err := json.Unmarshal([]byte(response), &resp); err != nil {
|
|
return response // fallback to raw
|
|
}
|
|
|
|
// Single value
|
|
if v, ok := resp["value"]; ok {
|
|
return fmt.Sprintf("%v", v)
|
|
}
|
|
|
|
// Blood pressure style: systolic/diastolic
|
|
if sys, ok := resp["systolic"]; ok {
|
|
if dia, ok := resp["diastolic"]; ok {
|
|
return fmt.Sprintf("%v/%v", sys, dia)
|
|
}
|
|
}
|
|
|
|
// Fallback: join all values
|
|
var parts []string
|
|
for _, v := range resp {
|
|
parts = append(parts, fmt.Sprintf("%v", v))
|
|
}
|
|
if len(parts) > 0 {
|
|
return fmt.Sprintf("%v", parts[0]) // just first for now
|
|
}
|
|
return response
|
|
}
|
|
|
|
// TrackerDismiss marks a tracker as dismissed
|
|
func TrackerDismiss(trackerID string) error {
|
|
p, err := TrackerGet(trackerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.Dismissed = true
|
|
p.UpdatedAt = time.Now().Unix()
|
|
return dbSave("trackers", p)
|
|
}
|
|
|
|
// TrackerSkip advances next_ask to tomorrow without recording a response
|
|
func TrackerSkip(trackerID string) error {
|
|
p, err := TrackerGet(trackerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
now := time.Now().Unix()
|
|
p.NextAsk = now + 24*60*60
|
|
p.UpdatedAt = now
|
|
return dbSave("trackers", p)
|
|
}
|
|
|
|
// calculateNextAsk determines when to ask again based on frequency
|
|
func calculateNextAsk(frequency, timeOfDay string, now int64) int64 {
|
|
switch frequency {
|
|
case "once":
|
|
return 0 // never ask again (will be filtered by expires_at or dismissed)
|
|
case "daily":
|
|
return now + 24*60*60
|
|
case "twice_daily":
|
|
return now + 12*60*60
|
|
case "weekly":
|
|
return now + 7*24*60*60
|
|
case "until_resolved":
|
|
return now + 24*60*60 // ask daily until dismissed
|
|
default:
|
|
// Handle "weekly:mon,wed,fri" or other patterns later
|
|
return now + 24*60*60
|
|
}
|
|
}
|