Files
rideaware-api/internal/equipment/service.go

219 lines
5.5 KiB
Go

package equipment
import (
"errors"
"math"
)
type Service struct {
repo *Repository
}
func NewService() *Service {
return &Service{
repo: NewRepository(),
}
}
// Equipment methods
func (s *Service) CreateEquipment(userID uint, name, equipmentType, brand, model string, weight float64, notes string) (*Equipment, error) {
if name == "" || equipmentType == "" {
return nil, errors.New("name and type are required")
}
equipment := &Equipment{
UserID: userID,
Name: name,
Type: equipmentType,
Brand: brand,
Model: model,
Weight: weight,
Notes: notes,
Active: true,
}
if err := s.repo.CreateEquipment(equipment); err != nil {
return nil, err
}
return equipment, nil
}
func (s *Service) GetUserEquipment(userID uint) ([]Equipment, error) {
return s.repo.GetUserEquipment(userID)
}
func (s *Service) GetEquipmentByID(id, userID uint) (*Equipment, error) {
return s.repo.GetEquipmentByID(id, userID)
}
func (s *Service) UpdateEquipment(id, userID uint, updates map[string]interface{}) (*Equipment, error) {
equipment, err := s.repo.GetEquipmentByID(id, userID)
if err != nil {
return nil, err
}
if name, ok := updates["name"].(string); ok && name != "" {
equipment.Name = name
}
if brand, ok := updates["brand"].(string); ok {
equipment.Brand = brand
}
if model, ok := updates["model"].(string); ok {
equipment.Model = model
}
if weight, ok := updates["weight"].(float64); ok {
equipment.Weight = weight
}
if notes, ok := updates["notes"].(string); ok {
equipment.Notes = notes
}
if active, ok := updates["active"].(bool); ok {
equipment.Active = active
}
if v, ok := updates["service_interval_distance"].(float64); ok {
equipment.ServiceIntervalDistance = v
}
if v, ok := updates["service_interval_duration"].(float64); ok {
equipment.ServiceIntervalDuration = int(v)
}
if err := s.repo.UpdateEquipment(equipment); err != nil {
return nil, err
}
return equipment, nil
}
func (s *Service) DeleteEquipment(id, userID uint) error {
return s.repo.DeleteEquipment(id, userID)
}
// IncrementMileage adds distance (km) and duration (seconds) to equipment totals.
func (s *Service) IncrementMileage(equipmentID, userID uint, distance float64, duration int) error {
return s.repo.IncrementMileage(equipmentID, userID, distance, duration)
}
// RecordService resets the service counters and records the service date.
func (s *Service) RecordService(equipmentID, userID uint) (*Equipment, error) {
if err := s.repo.ResetServiceCounters(equipmentID, userID); err != nil {
return nil, err
}
return s.repo.GetEquipmentByID(equipmentID, userID)
}
// GetServiceStatus returns the service status for a piece of equipment.
func (s *Service) GetServiceStatus(equipmentID, userID uint) (*ServiceStatus, error) {
eq, err := s.repo.GetEquipmentByID(equipmentID, userID)
if err != nil {
return nil, err
}
status := eq.GetServiceStatus()
return &status, nil
}
// Training Zones calculation
func (s *Service) CalculateHRZones(maxHR, restingHR int) *HRZones {
if maxHR <= 0 {
return nil
}
// Karvonen formula (Heart Rate Reserve)
hrr := maxHR - restingHR
return &HRZones{
Zone1: TrainingZone{
ID: 1,
Name: "Recovery",
Min: restingHR + int(math.Round(float64(hrr)*0.50)),
Max: restingHR + int(math.Round(float64(hrr)*0.60)),
Color: "#4285F4", // Blue
},
Zone2: TrainingZone{
ID: 2,
Name: "Endurance",
Min: restingHR + int(math.Round(float64(hrr)*0.60)),
Max: restingHR + int(math.Round(float64(hrr)*0.70)),
Color: "#34A853", // Green
},
Zone3: TrainingZone{
ID: 3,
Name: "Tempo",
Min: restingHR + int(math.Round(float64(hrr)*0.70)),
Max: restingHR + int(math.Round(float64(hrr)*0.80)),
Color: "#FBBC04", // Yellow
},
Zone4: TrainingZone{
ID: 4,
Name: "Threshold",
Min: restingHR + int(math.Round(float64(hrr)*0.80)),
Max: restingHR + int(math.Round(float64(hrr)*0.90)),
Color: "#EA4335", // Red
},
Zone5: TrainingZone{
ID: 5,
Name: "VO2 Max",
Min: restingHR + int(math.Round(float64(hrr)*0.90)),
Max: maxHR,
Color: "#A61C00", // Dark Red
},
}
}
func (s *Service) CalculatePowerZones(ftp int) *PowerZones {
if ftp <= 0 {
return nil
}
return &PowerZones{
Zone1: TrainingZone{
ID: 1,
Name: "Active Recovery",
Min: 0,
Max: int(math.Round(float64(ftp) * 0.55)),
Color: "#4285F4", // Blue
},
Zone2: TrainingZone{
ID: 2,
Name: "Endurance",
Min: int(math.Round(float64(ftp) * 0.55)),
Max: int(math.Round(float64(ftp) * 0.75)),
Color: "#34A853", // Green
},
Zone3: TrainingZone{
ID: 3,
Name: "Sweet Spot",
Min: int(math.Round(float64(ftp) * 0.75)),
Max: int(math.Round(float64(ftp) * 0.90)),
Color: "#FBBC04", // Yellow
},
Zone4: TrainingZone{
ID: 4,
Name: "Threshold",
Min: int(math.Round(float64(ftp) * 0.90)),
Max: int(math.Round(float64(ftp) * 1.05)),
Color: "#EA4335", // Red
},
Zone5: TrainingZone{
ID: 5,
Name: "VO2 Max",
Min: int(math.Round(float64(ftp) * 1.05)),
Max: int(math.Round(float64(ftp) * 1.20)),
Color: "#A61C00", // Dark Red
},
Zone6: TrainingZone{
ID: 6,
Name: "Anaerobic",
Min: int(math.Round(float64(ftp) * 1.20)),
Max: int(math.Round(float64(ftp) * 1.50)),
Color: "#800080", // Purple
},
Zone7: TrainingZone{
ID: 7,
Name: "Neuromuscular",
Min: int(math.Round(float64(ftp) * 1.50)),
Max: int(math.Round(float64(ftp) * 2.00)),
Color: "#FF1744", // Bright Red
},
}
}