lots of stuff, don't truly remember
This commit is contained in:
@@ -110,6 +110,73 @@ func (h *Handler) GetMonthlyStats(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(stats)
|
||||
}
|
||||
|
||||
// GetTrainingLoad GET /api/protected/stats/training-load?days=180
|
||||
func (h *Handler) GetTrainingLoad(w http.ResponseWriter, r *http.Request) {
|
||||
claims := r.Context().Value(middleware.UserContextKey).(*config.CustomClaims)
|
||||
if claims == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
days := 180
|
||||
if dStr := r.URL.Query().Get("days"); dStr != "" {
|
||||
if parsed, err := strconv.Atoi(dStr); err == nil {
|
||||
days = parsed
|
||||
}
|
||||
}
|
||||
|
||||
data, ftp, err := h.service.GetTrainingLoad(claims.UserID, days)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "failed to fetch training load"})
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"ftp": ftp,
|
||||
"daily_tss": data,
|
||||
})
|
||||
}
|
||||
|
||||
// GetPowerHistory GET /api/protected/stats/power-history?days=365
|
||||
func (h *Handler) GetPowerHistory(w http.ResponseWriter, r *http.Request) {
|
||||
claims := r.Context().Value(middleware.UserContextKey).(*config.CustomClaims)
|
||||
if claims == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
days := 365
|
||||
if dStr := r.URL.Query().Get("days"); dStr != "" {
|
||||
if parsed, err := strconv.Atoi(dStr); err == nil {
|
||||
days = parsed
|
||||
}
|
||||
}
|
||||
|
||||
data, err := h.service.GetPowerHistory(claims.UserID, days)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "failed to fetch power history"})
|
||||
return
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
data = []PowerPoint{}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
// GetPersonalBests GET /api/protected/stats/personal-bests
|
||||
func (h *Handler) GetPersonalBests(w http.ResponseWriter, r *http.Request) {
|
||||
claims := r.Context().Value(middleware.UserContextKey).(*config.CustomClaims)
|
||||
|
||||
@@ -43,6 +43,21 @@ type PeriodStats struct {
|
||||
AvgHR float64 `json:"avg_hr"`
|
||||
}
|
||||
|
||||
// DailyTSS holds the computed TSS for a single day.
|
||||
type DailyTSS struct {
|
||||
Date string `json:"date"`
|
||||
TSS float64 `json:"tss"`
|
||||
}
|
||||
|
||||
// PowerPoint holds power data for a single completed workout.
|
||||
type PowerPoint struct {
|
||||
Date string `json:"date"`
|
||||
AvgPower int `json:"avg_power"`
|
||||
MaxPower int `json:"max_power"`
|
||||
Duration int `json:"duration"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
// GetSummary returns overall ride statistics for completed workouts.
|
||||
func (r *Repository) GetSummary(userID uint) (*Summary, error) {
|
||||
var summary Summary
|
||||
@@ -120,6 +135,65 @@ func (r *Repository) GetMonthlyStats(userID uint, months int) ([]PeriodStats, er
|
||||
return stats, err
|
||||
}
|
||||
|
||||
// GetDailyTSS returns daily TSS values for the last N days.
|
||||
// TSS = (duration_seconds * (avg_power / FTP)^2 / 3600) * 100
|
||||
// Simplified: (duration * avg_power^2) / (FTP^2 * 36)
|
||||
func (r *Repository) GetDailyTSS(userID uint, ftp int, days int) ([]DailyTSS, error) {
|
||||
if ftp <= 0 {
|
||||
return []DailyTSS{}, nil
|
||||
}
|
||||
|
||||
cutoff := time.Now().AddDate(0, 0, -days)
|
||||
ftpFloat := float64(ftp)
|
||||
|
||||
var results []DailyTSS
|
||||
err := database.DB.Model(&workout.Workout{}).
|
||||
Select(`
|
||||
TO_CHAR(scheduled_date, 'YYYY-MM-DD') as date,
|
||||
COALESCE(SUM(
|
||||
CASE WHEN avg_power > 0 AND duration > 0
|
||||
THEN (duration::float8 * avg_power::float8 * avg_power::float8) / (? * ? * 36.0)
|
||||
ELSE 0
|
||||
END
|
||||
), 0) as tss
|
||||
`, ftpFloat, ftpFloat).
|
||||
Where("user_id = ? AND status = ? AND scheduled_date >= ?", userID, "completed", cutoff).
|
||||
Group("date").
|
||||
Order("date ASC").
|
||||
Scan(&results).Error
|
||||
|
||||
if results == nil {
|
||||
results = []DailyTSS{}
|
||||
}
|
||||
|
||||
return results, err
|
||||
}
|
||||
|
||||
// GetPowerHistory returns power data points for completed workouts in the last N days.
|
||||
func (r *Repository) GetPowerHistory(userID uint, days int) ([]PowerPoint, error) {
|
||||
cutoff := time.Now().AddDate(0, 0, -days)
|
||||
|
||||
var results []PowerPoint
|
||||
err := database.DB.Model(&workout.Workout{}).
|
||||
Select(`
|
||||
TO_CHAR(scheduled_date, 'YYYY-MM-DD') as date,
|
||||
avg_power,
|
||||
max_power,
|
||||
duration,
|
||||
title
|
||||
`).
|
||||
Where("user_id = ? AND status = ? AND avg_power > 0 AND scheduled_date >= ?",
|
||||
userID, "completed", cutoff).
|
||||
Order("scheduled_date ASC").
|
||||
Scan(&results).Error
|
||||
|
||||
if results == nil {
|
||||
results = []PowerPoint{}
|
||||
}
|
||||
|
||||
return results, err
|
||||
}
|
||||
|
||||
// PersonalBest holds a single personal best record.
|
||||
type PersonalBest struct {
|
||||
Category string `json:"category"`
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package stats
|
||||
|
||||
import (
|
||||
"rideaware/pkg/database"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo *Repository
|
||||
}
|
||||
@@ -31,3 +35,37 @@ func (s *Service) GetMonthlyStats(userID uint, months int) ([]PeriodStats, error
|
||||
func (s *Service) GetPersonalBests(userID uint) ([]PersonalBest, error) {
|
||||
return s.repo.GetPersonalBests(userID)
|
||||
}
|
||||
|
||||
// getUserFTP retrieves the user's FTP from the user_profiles table.
|
||||
func (s *Service) getUserFTP(userID uint) int {
|
||||
var ftp int
|
||||
database.DB.Table("user_profiles").
|
||||
Where("user_id = ?", userID).
|
||||
Select("ftp").
|
||||
Scan(&ftp)
|
||||
return ftp
|
||||
}
|
||||
|
||||
// GetTrainingLoad returns daily TSS data and the user's FTP.
|
||||
func (s *Service) GetTrainingLoad(userID uint, days int) ([]DailyTSS, int, error) {
|
||||
if days <= 0 || days > 730 {
|
||||
days = 180
|
||||
}
|
||||
|
||||
ftp := s.getUserFTP(userID)
|
||||
|
||||
data, err := s.repo.GetDailyTSS(userID, ftp, days)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return data, ftp, nil
|
||||
}
|
||||
|
||||
// GetPowerHistory returns power data points for completed workouts.
|
||||
func (s *Service) GetPowerHistory(userID uint, days int) ([]PowerPoint, error) {
|
||||
if days <= 0 || days > 730 {
|
||||
days = 365
|
||||
}
|
||||
return s.repo.GetPowerHistory(userID, days)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user