feat: extend equipment and workout models with service tracking
This commit is contained in:
@@ -136,6 +136,56 @@ func (h *Handler) DeleteEquipment(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// RecordService POST /api/protected/equipment/service?id=X
|
||||
func (h *Handler) RecordService(w http.ResponseWriter, r *http.Request) {
|
||||
claims := r.Context().Value(middleware.UserContextKey).(*config.CustomClaims)
|
||||
|
||||
idStr := r.URL.Query().Get("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "invalid equipment id"})
|
||||
return
|
||||
}
|
||||
|
||||
eq, err := h.service.RecordService(uint(id), claims.UserID)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(eq)
|
||||
}
|
||||
|
||||
// GetServiceStatus GET /api/protected/equipment/service-status?id=X
|
||||
func (h *Handler) GetServiceStatus(w http.ResponseWriter, r *http.Request) {
|
||||
claims := r.Context().Value(middleware.UserContextKey).(*config.CustomClaims)
|
||||
|
||||
idStr := r.URL.Query().Get("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "invalid equipment id"})
|
||||
return
|
||||
}
|
||||
|
||||
status, err := h.service.GetServiceStatus(uint(id), claims.UserID)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(status)
|
||||
}
|
||||
|
||||
// GetTrainingZones GET /api/zones
|
||||
func (h *Handler) GetTrainingZones(w http.ResponseWriter, r *http.Request) {
|
||||
claims := r.Context().Value(middleware.UserContextKey).(*config.CustomClaims)
|
||||
|
||||
@@ -3,17 +3,58 @@ package equipment
|
||||
import "time"
|
||||
|
||||
type Equipment struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"not null;index" json:"user_id"`
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Type string `gorm:"not null" json:"type"` // "bike", "shoes", "helmet", etc.
|
||||
Brand string `gorm:"default:''" json:"brand"`
|
||||
Model string `gorm:"default:''" json:"model"`
|
||||
Weight float64 `gorm:"default:0" json:"weight"` // grams
|
||||
Notes string `gorm:"default:''" json:"notes"`
|
||||
Active bool `gorm:"default:true" json:"active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"not null;index" json:"user_id"`
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Type string `gorm:"not null" json:"type"` // "bike", "shoes", "helmet", etc.
|
||||
Brand string `gorm:"default:''" json:"brand"`
|
||||
Model string `gorm:"default:''" json:"model"`
|
||||
Weight float64 `gorm:"default:0" json:"weight"` // grams
|
||||
Notes string `gorm:"default:''" json:"notes"`
|
||||
Active bool `gorm:"default:true" json:"active"`
|
||||
TotalDistance float64 `gorm:"default:0" json:"total_distance"` // km
|
||||
TotalDuration int `gorm:"default:0" json:"total_duration"` // seconds
|
||||
TotalRides int `gorm:"default:0" json:"total_rides"`
|
||||
ServiceIntervalDistance float64 `gorm:"default:0" json:"service_interval_distance"` // km, 0 = no reminder
|
||||
ServiceIntervalDuration int `gorm:"default:0" json:"service_interval_duration"` // hours, 0 = no reminder
|
||||
LastServiceDate *time.Time `json:"last_service_date"`
|
||||
DistanceSinceService float64 `gorm:"default:0" json:"distance_since_service"` // km since last service
|
||||
DurationSinceService int `gorm:"default:0" json:"duration_since_service"` // seconds since last service
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ServiceStatus indicates whether equipment needs servicing.
|
||||
type ServiceStatus struct {
|
||||
NeedsService bool `json:"needs_service"`
|
||||
DistanceDue bool `json:"distance_due"`
|
||||
DurationDue bool `json:"duration_due"`
|
||||
DistanceSinceService float64 `json:"distance_since_service"` // km
|
||||
DurationSinceService int `json:"duration_since_service"` // hours
|
||||
ServiceIntervalDist float64 `json:"service_interval_distance"`
|
||||
ServiceIntervalDur int `json:"service_interval_duration"`
|
||||
}
|
||||
|
||||
// GetServiceStatus checks if the equipment is due for service.
|
||||
func (e *Equipment) GetServiceStatus() ServiceStatus {
|
||||
status := ServiceStatus{
|
||||
DistanceSinceService: e.DistanceSinceService,
|
||||
DurationSinceService: e.DurationSinceService / 3600,
|
||||
ServiceIntervalDist: e.ServiceIntervalDistance,
|
||||
ServiceIntervalDur: e.ServiceIntervalDuration,
|
||||
}
|
||||
|
||||
if e.ServiceIntervalDistance > 0 && e.DistanceSinceService >= e.ServiceIntervalDistance {
|
||||
status.DistanceDue = true
|
||||
status.NeedsService = true
|
||||
}
|
||||
|
||||
if e.ServiceIntervalDuration > 0 && e.DurationSinceService >= e.ServiceIntervalDuration*3600 {
|
||||
status.DurationDue = true
|
||||
status.NeedsService = true
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
type TrainingZone struct {
|
||||
|
||||
@@ -3,6 +3,8 @@ package equipment
|
||||
import (
|
||||
"errors"
|
||||
"rideaware/pkg/database"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -53,4 +55,29 @@ func (r *Repository) UpdateEquipment(equipment *Equipment) error {
|
||||
func (r *Repository) DeleteEquipment(id, userID uint) error {
|
||||
return database.DB.Where("id = ? AND user_id = ?", id, userID).
|
||||
Delete(&Equipment{}).Error
|
||||
}
|
||||
|
||||
// IncrementMileage atomically adds distance and duration to equipment totals.
|
||||
func (r *Repository) IncrementMileage(id, userID uint, distance float64, duration int) error {
|
||||
return database.DB.Model(&Equipment{}).
|
||||
Where("id = ? AND user_id = ?", id, userID).
|
||||
Updates(map[string]interface{}{
|
||||
"total_distance": gorm.Expr("total_distance + ?", distance),
|
||||
"total_duration": gorm.Expr("total_duration + ?", duration),
|
||||
"total_rides": gorm.Expr("total_rides + 1"),
|
||||
"distance_since_service": gorm.Expr("distance_since_service + ?", distance),
|
||||
"duration_since_service": gorm.Expr("duration_since_service + ?", duration),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// ResetServiceCounters resets the service tracking counters after servicing.
|
||||
func (r *Repository) ResetServiceCounters(id, userID uint) error {
|
||||
now := time.Now()
|
||||
return database.DB.Model(&Equipment{}).
|
||||
Where("id = ? AND user_id = ?", id, userID).
|
||||
Updates(map[string]interface{}{
|
||||
"distance_since_service": 0,
|
||||
"duration_since_service": 0,
|
||||
"last_service_date": now,
|
||||
}).Error
|
||||
}
|
||||
@@ -71,6 +71,12 @@ func (s *Service) UpdateEquipment(id, userID uint, updates map[string]interface{
|
||||
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
|
||||
@@ -83,6 +89,29 @@ 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 {
|
||||
|
||||
Reference in New Issue
Block a user