feat: fix workout taxonomy to 7 types, add pace/HR-zone fields to the API
This commit is contained in:
@@ -12,13 +12,39 @@ import (
|
||||
"smartrun/backend/internal/store"
|
||||
)
|
||||
|
||||
// workoutKindResponse combines a workout kind's rule/metadata with its pace
|
||||
// range and expected HR zone (stored separately in workout_type_paces),
|
||||
// since the frontend always edits and displays them together.
|
||||
type workoutKindResponse struct {
|
||||
store.WorkoutKind
|
||||
PaceMinSecPerKm *float64 `json:"pace_min_sec_per_km"`
|
||||
PaceMaxSecPerKm *float64 `json:"pace_max_sec_per_km"`
|
||||
ExpectedHRZone *int `json:"expected_hr_zone"`
|
||||
}
|
||||
|
||||
func (s *Server) toWorkoutKindResponse(r *http.Request, k store.WorkoutKind) (workoutKindResponse, error) {
|
||||
pace, err := s.DB.GetWorkoutTypePace(r.Context(), k.ID)
|
||||
if err != nil {
|
||||
return workoutKindResponse{}, err
|
||||
}
|
||||
return workoutKindResponse{
|
||||
WorkoutKind: k,
|
||||
PaceMinSecPerKm: pace.PaceMinSecPerKm,
|
||||
PaceMaxSecPerKm: pace.PaceMaxSecPerKm,
|
||||
ExpectedHRZone: pace.ExpectedHRZone,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type workoutKindRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Color string `json:"color"`
|
||||
Rule json.RawMessage `json:"rule"`
|
||||
Priority int `json:"priority"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Color string `json:"color"`
|
||||
Rule json.RawMessage `json:"rule"`
|
||||
Priority int `json:"priority"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
PaceMinSecPerKm *float64 `json:"pace_min_sec_per_km"`
|
||||
PaceMaxSecPerKm *float64 `json:"pace_max_sec_per_km"`
|
||||
ExpectedHRZone *int `json:"expected_hr_zone"`
|
||||
}
|
||||
|
||||
func (req workoutKindRequest) validate() (classify.Node, error) {
|
||||
@@ -32,6 +58,9 @@ func (req workoutKindRequest) validate() (classify.Node, error) {
|
||||
if err := node.Validate(); err != nil {
|
||||
return node, errors.New("invalid rule: " + err.Error())
|
||||
}
|
||||
if req.ExpectedHRZone != nil && (*req.ExpectedHRZone < 1 || *req.ExpectedHRZone > 5) {
|
||||
return node, errors.New("expected_hr_zone must be between 1 and 5")
|
||||
}
|
||||
return node, nil
|
||||
}
|
||||
|
||||
@@ -42,7 +71,16 @@ func (s *Server) handleListWorkoutKinds(w http.ResponseWriter, r *http.Request)
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, kinds)
|
||||
resp := make([]workoutKindResponse, 0, len(kinds))
|
||||
for _, k := range kinds {
|
||||
wr, err := s.toWorkoutKindResponse(r, k)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
resp = append(resp, wr)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (s *Server) handleGetWorkoutKind(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -60,35 +98,12 @@ func (s *Server) handleGetWorkoutKind(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusNotFound, "workout kind not found")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, kind)
|
||||
}
|
||||
|
||||
func (s *Server) handleCreateWorkoutKind(w http.ResponseWriter, r *http.Request) {
|
||||
var req workoutKindRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
if _, err := req.validate(); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
isActive := true
|
||||
if req.IsActive != nil {
|
||||
isActive = *req.IsActive
|
||||
}
|
||||
|
||||
id, err := s.DB.CreateWorkoutKind(r.Context(), store.WorkoutKind{
|
||||
Name: req.Name, Description: req.Description, Color: req.Color,
|
||||
RuleJSON: string(req.Rule), Priority: req.Priority, IsActive: isActive,
|
||||
})
|
||||
resp, err := s.toWorkoutKindResponse(r, kind)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
kind, _, _ := s.DB.GetWorkoutKind(r.Context(), id)
|
||||
writeJSON(w, http.StatusCreated, kind)
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (s *Server) handleUpdateWorkoutKind(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -129,21 +144,23 @@ func (s *Server) handleUpdateWorkoutKind(w http.ResponseWriter, r *http.Request)
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
kind, _, _ := s.DB.GetWorkoutKind(r.Context(), id)
|
||||
writeJSON(w, http.StatusOK, kind)
|
||||
}
|
||||
|
||||
func (s *Server) handleDeleteWorkoutKind(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid workout kind id")
|
||||
return
|
||||
}
|
||||
if err := s.DB.SoftDeleteWorkoutKind(r.Context(), id); err != nil {
|
||||
if err := s.DB.UpdateWorkoutTypePace(r.Context(), store.WorkoutTypePace{
|
||||
WorkoutKindID: id,
|
||||
PaceMinSecPerKm: req.PaceMinSecPerKm,
|
||||
PaceMaxSecPerKm: req.PaceMaxSecPerKm,
|
||||
ExpectedHRZone: req.ExpectedHRZone,
|
||||
}); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
|
||||
kind, _, _ := s.DB.GetWorkoutKind(r.Context(), id)
|
||||
resp, err := s.toWorkoutKindResponse(r, kind)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// handleReclassifyKind re-runs the rule engine for every activity currently
|
||||
|
||||
Reference in New Issue
Block a user