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, userID int64, k WorkoutKind) (int64, error) { res, err := db.ExecContext(ctx, ` INSERT INTO workout_kinds (user_id, name, description, color, rule_json, priority, is_active) VALUES (?,?,?,?,?,?,?)`, userID, k.Name, k.Description, k.Color, k.RuleJSON, k.Priority, k.IsActive) if err != nil { return 0, fmt.Errorf("create workout kind %q for user %d: %w", k.Name, userID, err) } return res.LastInsertId() } // UpdateWorkoutKind updates an existing workout kind's editable fields, // scoped so it can only ever affect a row owned by userID. func (db *DB) UpdateWorkoutKind(ctx context.Context, userID int64, 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=? AND user_id=?`, k.Name, k.Description, k.Color, k.RuleJSON, k.Priority, k.IsActive, k.ID, userID) if err != nil { return fmt.Errorf("update workout kind %d for user %d: %w", k.ID, userID, err) } return nil } // GetWorkoutKind fetches one workout kind by id, scoped to userID. func (db *DB) GetWorkoutKind(ctx context.Context, userID, id int64) (WorkoutKind, bool, error) { row := db.QueryRowContext(ctx, `SELECT `+workoutKindColumns+` FROM workout_kinds WHERE id = ? AND user_id = ?`, id, userID) k, err := scanWorkoutKind(row) if err == sql.ErrNoRows { return WorkoutKind{}, false, nil } if err != nil { return WorkoutKind{}, false, fmt.Errorf("get workout kind %d for user %d: %w", id, userID, err) } return k, true, nil } // GetWorkoutKindByName fetches one workout kind by name, scoped to userID // (the same name can exist for different users -- see UNIQUE(user_id, name)). func (db *DB) GetWorkoutKindByName(ctx context.Context, userID int64, name string) (WorkoutKind, bool, error) { row := db.QueryRowContext(ctx, `SELECT `+workoutKindColumns+` FROM workout_kinds WHERE name = ? AND user_id = ?`, name, userID) k, err := scanWorkoutKind(row) if err == sql.ErrNoRows { return WorkoutKind{}, false, nil } if err != nil { return WorkoutKind{}, false, fmt.Errorf("get workout kind %q for user %d: %w", name, userID, err) } return k, true, nil } // ListWorkoutKinds returns workout kinds for userID. If activeOnly, soft-deleted // (is_active=0) kinds are excluded. func (db *DB) ListWorkoutKinds(ctx context.Context, userID int64, activeOnly bool) ([]WorkoutKind, error) { query := `SELECT ` + workoutKindColumns + ` FROM workout_kinds WHERE user_id = ?` if activeOnly { query += ` AND is_active = 1` } query += ` ORDER BY priority DESC, name` rows, err := db.QueryContext(ctx, query, userID) if err != nil { return nil, fmt.Errorf("list workout kinds for user %d: %w", userID, 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. Scoped to userID. func (db *DB) SoftDeleteWorkoutKind(ctx context.Context, userID, id int64) error { _, err := db.ExecContext(ctx, `UPDATE workout_kinds SET is_active=0, updated_at=datetime('now') WHERE id=? AND user_id=?`, id, userID) if err != nil { return fmt.Errorf("soft delete workout kind %d for user %d: %w", id, userID, err) } return nil }