184 lines
4.8 KiB
Go
184 lines
4.8 KiB
Go
package lib
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
)
|
|
|
|
// PromptAdd inserts a new prompt. Generates PromptID if empty.
|
|
func PromptAdd(p *Prompt) error {
|
|
if p.PromptID == "" {
|
|
p.PromptID = 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("prompts", p)
|
|
}
|
|
|
|
// PromptModify updates an existing prompt
|
|
func PromptModify(p *Prompt) error {
|
|
p.UpdatedAt = time.Now().Unix()
|
|
return Save("prompts", p)
|
|
}
|
|
|
|
// PromptDelete removes a prompt
|
|
func PromptDelete(promptID string) error {
|
|
return Delete("prompts", "prompt_id", promptID)
|
|
}
|
|
|
|
// PromptGet retrieves a single prompt by ID
|
|
func PromptGet(promptID string) (*Prompt, error) {
|
|
p := &Prompt{}
|
|
return p, Load("prompts", promptID, p)
|
|
}
|
|
|
|
// PromptQueryActive retrieves active prompts due for a dossier
|
|
func PromptQueryActive(dossierID string) ([]*Prompt, error) {
|
|
now := time.Now().Unix()
|
|
var result []*Prompt
|
|
err := Query(`SELECT * FROM prompts
|
|
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
|
|
}
|
|
|
|
// PromptQueryAll retrieves all prompts for a dossier (including inactive)
|
|
func PromptQueryAll(dossierID string) ([]*Prompt, error) {
|
|
var result []*Prompt
|
|
err := Query(`SELECT * FROM prompts WHERE dossier_id = ? ORDER BY active DESC, time_of_day, created_at`,
|
|
[]any{dossierID}, &result)
|
|
return result, err
|
|
}
|
|
|
|
// PromptRespond records a response and advances next_ask
|
|
func PromptRespond(promptID string, response, responseRaw string) error {
|
|
now := time.Now().Unix()
|
|
|
|
// Get current prompt to calculate next_ask
|
|
p, err := PromptGet(promptID)
|
|
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("prompts", p); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create entry for certain prompt types
|
|
if err := promptCreateEntry(p, response, now); err != nil {
|
|
// Log but don't fail the response
|
|
log.Printf("Failed to create entry for prompt %s: %v", promptID, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// promptCreateEntry creates an entry from a prompt response
|
|
// Uses the prompt's category/type directly - no hardcoded mappings
|
|
func promptCreateEntry(p *Prompt, 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","prompt_id":"%s"}`, response, p.PromptID),
|
|
}
|
|
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
|
|
}
|
|
|
|
// PromptDismiss marks a prompt as dismissed
|
|
func PromptDismiss(promptID string) error {
|
|
p, err := PromptGet(promptID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.Dismissed = true
|
|
p.UpdatedAt = time.Now().Unix()
|
|
return Save("prompts", p)
|
|
}
|
|
|
|
// PromptSkip advances next_ask to tomorrow without recording a response
|
|
func PromptSkip(promptID string) error {
|
|
p, err := PromptGet(promptID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
now := time.Now().Unix()
|
|
p.NextAsk = now + 24*60*60
|
|
p.UpdatedAt = now
|
|
return Save("prompts", 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
|
|
}
|
|
}
|