package store import ( "context" "database/sql" "fmt" ) // WorkoutKind is a user-editable classification rule ("Easy", "Tempo", ...). // RuleJSON holds the condition tree evaluated by internal/classify. type WorkoutKind struct { ID int64 Name string Description string Color string RuleJSON string Priority int IsActive bool CreatedAt string UpdatedAt string } func scanWorkoutKind(row interface{ Scan(...any) error }) (WorkoutKind, error) { var k WorkoutKind err := row.Scan(&k.ID, &k.Name, &k.Description, &k.Color, &k.RuleJSON, &k.Priority, &k.IsActive, &k.CreatedAt, &k.UpdatedAt) return k, err } const workoutKindColumns = `id, name, description, color, rule_json, priority, is_active, created_at, updated_at` // CreateWorkoutKind inserts a new workout kind and returns its id. func (db *DB) CreateWorkoutKind(ctx context.Context, k WorkoutKind) (int64, error) { res, err := db.ExecContext(ctx, ` INSERT INTO workout_kinds (name, description, color, rule_json, priority, is_active) VALUES (?,?,?,?,?,?)`, k.Name, k.Description, k.Color, k.RuleJSON, k.Priority, k.IsActive) if err != nil { return 0, fmt.Errorf("create workout kind %q: %w", k.Name, err) } return res.LastInsertId() } // UpdateWorkoutKind updates an existing workout kind's editable fields. func (db *DB) UpdateWorkoutKind(ctx context.Context, k WorkoutKind) error { _, err := db.ExecContext(ctx, ` UPDATE workout_kinds SET name=?, description=?, color=?, rule_json=?, priority=?, is_active=?, updated_at=datetime('now') WHERE id=?`, k.Name, k.Description, k.Color, k.RuleJSON, k.Priority, k.IsActive, k.ID) if err != nil { return fmt.Errorf("update workout kind %d: %w", k.ID, err) } return nil } // GetWorkoutKind fetches one workout kind by id. func (db *DB) GetWorkoutKind(ctx context.Context, id int64) (WorkoutKind, bool, error) { row := db.QueryRowContext(ctx, `SELECT `+workoutKindColumns+` FROM workout_kinds WHERE id = ?`, id) k, err := scanWorkoutKind(row) if err == sql.ErrNoRows { return WorkoutKind{}, false, nil } if err != nil { return WorkoutKind{}, false, fmt.Errorf("get workout kind %d: %w", id, err) } return k, true, nil } // GetWorkoutKindByName fetches one workout kind by its unique name. func (db *DB) GetWorkoutKindByName(ctx context.Context, name string) (WorkoutKind, bool, error) { row := db.QueryRowContext(ctx, `SELECT `+workoutKindColumns+` FROM workout_kinds WHERE name = ?`, name) k, err := scanWorkoutKind(row) if err == sql.ErrNoRows { return WorkoutKind{}, false, nil } if err != nil { return WorkoutKind{}, false, fmt.Errorf("get workout kind %q: %w", name, err) } return k, true, nil } // ListWorkoutKinds returns workout kinds. If activeOnly, soft-deleted // (is_active=0) kinds are excluded. func (db *DB) ListWorkoutKinds(ctx context.Context, activeOnly bool) ([]WorkoutKind, error) { query := `SELECT ` + workoutKindColumns + ` FROM workout_kinds` if activeOnly { query += ` WHERE is_active = 1` } query += ` ORDER BY priority DESC, name` rows, err := db.QueryContext(ctx, query) if err != nil { return nil, fmt.Errorf("list workout kinds: %w", err) } defer rows.Close() kinds := []WorkoutKind{} for rows.Next() { k, err := scanWorkoutKind(rows) if err != nil { return nil, fmt.Errorf("scan workout kind row: %w", err) } kinds = append(kinds, k) } return kinds, rows.Err() } // SoftDeleteWorkoutKind sets is_active=0, keeping history (kind_assignments // referencing it) intact. func (db *DB) SoftDeleteWorkoutKind(ctx context.Context, id int64) error { _, err := db.ExecContext(ctx, `UPDATE workout_kinds SET is_active=0, updated_at=datetime('now') WHERE id=?`, id) if err != nil { return fmt.Errorf("soft delete workout kind %d: %w", id, err) } return nil }