Initial commit: smartrun MVP
Garmin run classification and progression tracker. Go backend (MCP client to mcp-garmin, SQLite store, deterministic rule engine, REST API) and React/TS frontend (Dashboard, Review Queue, Workout Kinds). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
102
backend/internal/store/workoutkinds.go
Normal file
102
backend/internal/store/workoutkinds.go
Normal file
@@ -0,0 +1,102 @@
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user