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 Save("trackers", p) } // TrackerModify updates an existing prompt func TrackerModify(p *Tracker) error { p.UpdatedAt = time.Now().Unix() return Save("trackers", p) } // TrackerDelete removes a prompt func TrackerDelete(trackerID string) error { return Delete("trackers", "tracker_id", trackerID) } // TrackerGet retrieves a single tracker by ID func TrackerGet(trackerID string) (*Tracker, error) { p := &Tracker{} return p, Load("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 := Query(`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 := Query(`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 := Save("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 Save("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 Save("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 } }