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 } }