feat: extend equipment and workout models with service tracking
This commit is contained in:
126
internal/activity/service.go
Normal file
126
internal/activity/service.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package activity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"rideaware/internal/workout"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
workoutRepo *workout.Repository
|
||||
}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{
|
||||
workoutRepo: workout.NewRepository(),
|
||||
}
|
||||
}
|
||||
|
||||
// ImportActivity parses an activity file and creates or updates a workout with the metrics.
|
||||
func (s *Service) ImportActivity(userID uint, fileData []byte, filename string, opts ImportOptions) (*workout.Workout, error) {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
|
||||
var parsed *ParsedActivity
|
||||
var err error
|
||||
|
||||
switch ext {
|
||||
case ".fit":
|
||||
parsed, err = ParseFIT(fileData)
|
||||
case ".tcx":
|
||||
parsed, err = ParseTCX(fileData)
|
||||
case ".gpx":
|
||||
parsed, err = ParseGPX(fileData)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported file type: %s (supported: .fit, .tcx, .gpx)", ext)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse %s file: %w", ext, err)
|
||||
}
|
||||
|
||||
// If updating an existing workout, apply metrics to it
|
||||
if opts.WorkoutID > 0 {
|
||||
return s.updateExistingWorkout(userID, opts.WorkoutID, parsed, opts)
|
||||
}
|
||||
|
||||
// Create a new workout from parsed data
|
||||
return s.createNewWorkout(userID, parsed, ext, opts)
|
||||
}
|
||||
|
||||
func (s *Service) updateExistingWorkout(userID uint, workoutID uint, parsed *ParsedActivity, opts ImportOptions) (*workout.Workout, error) {
|
||||
w, err := s.workoutRepo.GetWorkoutByID(workoutID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w.Status = "completed"
|
||||
w.Duration = parsed.Duration
|
||||
w.Distance = parsed.Distance
|
||||
w.ElevGain = parsed.ElevGain
|
||||
w.AvgPower = parsed.AvgPower
|
||||
w.MaxPower = parsed.MaxPower
|
||||
w.AvgHR = parsed.AvgHR
|
||||
w.MaxHR = parsed.MaxHR
|
||||
w.CaloriesBurned = parsed.CaloriesBurned
|
||||
|
||||
if opts.EquipmentID != nil {
|
||||
w.EquipmentID = opts.EquipmentID
|
||||
}
|
||||
|
||||
if err := s.workoutRepo.UpdateWorkout(w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func (s *Service) createNewWorkout(userID uint, parsed *ParsedActivity, fileExt string, opts ImportOptions) (*workout.Workout, error) {
|
||||
title := opts.Title
|
||||
if title == "" && parsed.Title != "" {
|
||||
title = parsed.Title
|
||||
}
|
||||
if title == "" {
|
||||
title = "Imported Ride"
|
||||
}
|
||||
|
||||
scheduledDate := parsed.StartTime
|
||||
if scheduledDate.IsZero() {
|
||||
scheduledDate = time.Now()
|
||||
}
|
||||
|
||||
w := &workout.Workout{
|
||||
UserID: userID,
|
||||
Title: title,
|
||||
Type: "ride",
|
||||
Status: "completed",
|
||||
ScheduledDate: scheduledDate,
|
||||
Duration: parsed.Duration,
|
||||
Distance: parsed.Distance,
|
||||
ElevGain: parsed.ElevGain,
|
||||
AvgPower: parsed.AvgPower,
|
||||
MaxPower: parsed.MaxPower,
|
||||
AvgHR: parsed.AvgHR,
|
||||
MaxHR: parsed.MaxHR,
|
||||
CaloriesBurned: parsed.CaloriesBurned,
|
||||
FileType: strings.TrimPrefix(fileExt, "."),
|
||||
EquipmentID: opts.EquipmentID,
|
||||
Notes: opts.Notes,
|
||||
}
|
||||
|
||||
if err := s.workoutRepo.CreateWorkout(w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// ImportOptions holds optional parameters for activity import.
|
||||
type ImportOptions struct {
|
||||
WorkoutID uint // If set, update existing workout instead of creating new
|
||||
Title string // Custom title override
|
||||
EquipmentID *uint // Equipment to associate
|
||||
Notes string // Optional notes
|
||||
}
|
||||
Reference in New Issue
Block a user