Manual assignments are the user's definitive word and Race assignments come from a hard Garmin fact (eventType.typeKey), not a retunable rule -- neither is ever touched by reclassify again, and Race can no longer be set by hand via the review queue. Adds an Activities page so this locked/unlocked status is visible per workout, since Review Queue only ever showed unresolved items.
116 lines
3.7 KiB
Go
116 lines
3.7 KiB
Go
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
|
|
}
|