lots of stuff, don't truly remember
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"rideaware/internal/config"
|
||||
@@ -37,15 +38,16 @@ func (h *Handler) CreateWorkout(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("UserID: %d", claims.UserID)
|
||||
|
||||
var req struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
ScheduledDate string `json:"scheduled_date"`
|
||||
Duration int `json:"duration"`
|
||||
Notes string `json:"notes"`
|
||||
WorkoutData *WorkoutDataJSON `json:"workout_data"`
|
||||
FileType string `json:"file_type"`
|
||||
EquipmentID *uint `json:"equipment_id"`
|
||||
Duration int `json:"duration"`
|
||||
Notes string `json:"notes"`
|
||||
Tags Tags `json:"tags"`
|
||||
WorkoutData *WorkoutDataJSON `json:"workout_data"`
|
||||
FileType string `json:"file_type"`
|
||||
EquipmentID *uint `json:"equipment_id"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
@@ -105,6 +107,7 @@ func (h *Handler) CreateWorkout(w http.ResponseWriter, r *http.Request) {
|
||||
ScheduledDate: scheduledDate,
|
||||
Duration: req.Duration,
|
||||
Notes: req.Notes,
|
||||
Tags: req.Tags,
|
||||
FileType: req.FileType,
|
||||
WorkoutData: *workoutData,
|
||||
EquipmentID: req.EquipmentID,
|
||||
@@ -127,11 +130,25 @@ func (h *Handler) CreateWorkout(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(workout)
|
||||
}
|
||||
|
||||
// GetWorkouts GET /api/protected/workouts
|
||||
// GetWorkouts GET /api/protected/workouts?tags=tag1,tag2
|
||||
func (h *Handler) GetWorkouts(w http.ResponseWriter, r *http.Request) {
|
||||
claims := r.Context().Value(middleware.UserContextKey).(*config.CustomClaims)
|
||||
|
||||
workouts, err := h.service.GetUserWorkouts(claims.UserID)
|
||||
tagsParam := r.URL.Query().Get("tags")
|
||||
var workouts []Workout
|
||||
var err error
|
||||
|
||||
if tagsParam != "" {
|
||||
tags := strings.Split(tagsParam, ",")
|
||||
// Trim whitespace from each tag
|
||||
for i := range tags {
|
||||
tags[i] = strings.TrimSpace(tags[i])
|
||||
}
|
||||
workouts, err = h.service.GetUserWorkoutsByTags(claims.UserID, tags)
|
||||
} else {
|
||||
workouts, err = h.service.GetUserWorkouts(claims.UserID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
@@ -212,8 +229,10 @@ func (h *Handler) UpdateWorkout(w http.ResponseWriter, r *http.Request) {
|
||||
MaxPower int `json:"max_power"`
|
||||
MaxHR int `json:"max_hr"`
|
||||
CaloriesBurned int `json:"calories_burned"`
|
||||
RPE int `json:"rpe"`
|
||||
Notes string `json:"notes"`
|
||||
EquipmentID *uint `json:"equipment_id"`
|
||||
Tags Tags `json:"tags"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
@@ -267,12 +286,18 @@ func (h *Handler) UpdateWorkout(w http.ResponseWriter, r *http.Request) {
|
||||
if req.CaloriesBurned > 0 {
|
||||
workout.CaloriesBurned = req.CaloriesBurned
|
||||
}
|
||||
if req.RPE > 0 && req.RPE <= 10 {
|
||||
workout.RPE = req.RPE
|
||||
}
|
||||
if req.Notes != "" {
|
||||
workout.Notes = req.Notes
|
||||
}
|
||||
if req.EquipmentID != nil {
|
||||
workout.EquipmentID = req.EquipmentID
|
||||
}
|
||||
if req.Tags != nil {
|
||||
workout.Tags = req.Tags
|
||||
}
|
||||
|
||||
if err := h.service.repo.UpdateWorkout(workout); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -309,6 +334,74 @@ func (h *Handler) DeleteWorkout(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// RemoveDuplicates POST /api/protected/workouts/remove-duplicates
|
||||
func (h *Handler) RemoveDuplicates(w http.ResponseWriter, r *http.Request) {
|
||||
claims := r.Context().Value(middleware.UserContextKey).(*config.CustomClaims)
|
||||
|
||||
removed, err := h.service.RemoveDuplicates(claims.UserID)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "failed to remove duplicates"})
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]int64{"removed": removed})
|
||||
}
|
||||
|
||||
// RescheduleWorkout PUT /api/protected/workouts/reschedule
|
||||
func (h *Handler) RescheduleWorkout(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 workout id"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
ScheduledDate string `json:"scheduled_date"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "invalid request"})
|
||||
return
|
||||
}
|
||||
|
||||
newDate, err := time.Parse("2006-01-02", req.ScheduledDate)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "invalid date format, use YYYY-MM-DD"})
|
||||
return
|
||||
}
|
||||
|
||||
workout, err := h.service.repo.GetWorkoutByID(uint(id), claims.UserID)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "workout not found"})
|
||||
return
|
||||
}
|
||||
|
||||
workout.ScheduledDate = newDate
|
||||
if err := h.service.repo.UpdateWorkout(workout); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]string{"error": "failed to reschedule workout"})
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(workout)
|
||||
}
|
||||
|
||||
// GetWorkoutTypes GET /api/protected/workout-types
|
||||
func (h *Handler) GetWorkoutTypes(w http.ResponseWriter, r *http.Request) {
|
||||
types := []map[string]interface{}{
|
||||
@@ -414,4 +507,4 @@ func (h *Handler) UploadWorkoutFile(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(workout)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package workout
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -26,11 +28,46 @@ type Workout struct {
|
||||
EquipmentID *uint `gorm:"index" json:"equipment_id"`
|
||||
FileURL string `gorm:"default:''" json:"file_url"`
|
||||
WorkoutData WorkoutDataJSON `gorm:"type:jsonb" json:"workout_data,omitempty"`
|
||||
RPE int `gorm:"default:0" json:"rpe"`
|
||||
Notes string `json:"notes"`
|
||||
Tags Tags `gorm:"type:jsonb;default:'[]'" json:"tags"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Tags is a JSONB array of string tags for workouts
|
||||
type Tags []string
|
||||
|
||||
// Scan implements the sql.Scanner interface for reading from the database
|
||||
func (t *Tags) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
*t = Tags{}
|
||||
return nil
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
return json.Unmarshal(v, t)
|
||||
case string:
|
||||
return json.Unmarshal([]byte(v), t)
|
||||
default:
|
||||
return fmt.Errorf("unsupported type for Tags: %T", value)
|
||||
}
|
||||
}
|
||||
|
||||
// Value implements the driver.Valuer interface for writing to the database
|
||||
func (t Tags) Value() (driver.Value, error) {
|
||||
if t == nil {
|
||||
return json.Marshal([]string{})
|
||||
}
|
||||
return json.Marshal(t)
|
||||
}
|
||||
|
||||
// String returns a comma-separated string representation
|
||||
func (t Tags) String() string {
|
||||
return strings.Join(t, ", ")
|
||||
}
|
||||
|
||||
type WorkoutDataJSON struct {
|
||||
Name string `json:"name"`
|
||||
Author string `json:"author"`
|
||||
@@ -64,4 +101,4 @@ func (w WorkoutDataJSON) Value() (driver.Value, error) {
|
||||
|
||||
func (Workout) TableName() string {
|
||||
return "workouts"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,17 @@ func (r *Repository) GetUserWorkouts(userID uint) ([]Workout, error) {
|
||||
return workouts, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetUserWorkoutsByTags(userID uint, tags []string) ([]Workout, error) {
|
||||
var workouts []Workout
|
||||
// Use PostgreSQL jsonb ?| operator to check if the tags array contains any of the given tags
|
||||
if err := database.DB.Where("user_id = ? AND tags ?| ?", userID, tags).
|
||||
Order("scheduled_date DESC").
|
||||
Find(&workouts).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return workouts, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetWorkoutsByDateRange(userID uint, start, end time.Time) ([]Workout, error) {
|
||||
var workouts []Workout
|
||||
if err := database.DB.Where("user_id = ? AND scheduled_date BETWEEN ? AND ?", userID, start, end).
|
||||
@@ -64,6 +75,33 @@ func (r *Repository) DeleteWorkout(id, userID uint) error {
|
||||
Delete(&Workout{}).Error
|
||||
}
|
||||
|
||||
func (r *Repository) RemoveDuplicates(userID uint) (int64, error) {
|
||||
// Find IDs to keep: the minimum ID for each (title, scheduled_date, duration) group
|
||||
// Delete all other workouts that are duplicates
|
||||
result := database.DB.Exec(`
|
||||
DELETE FROM workouts
|
||||
WHERE user_id = ? AND id NOT IN (
|
||||
SELECT MIN(id)
|
||||
FROM workouts
|
||||
WHERE user_id = ?
|
||||
GROUP BY title, scheduled_date, duration
|
||||
)
|
||||
`, userID, userID)
|
||||
if result.Error != nil {
|
||||
return 0, result.Error
|
||||
}
|
||||
return result.RowsAffected, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetCompletedWorkoutOnDate(userID uint, date string) (*Workout, error) {
|
||||
var w Workout
|
||||
if err := database.DB.Where("user_id = ? AND status = 'completed' AND DATE(scheduled_date) = ?", userID, date).
|
||||
First(&w).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &w, nil
|
||||
}
|
||||
|
||||
type EquipmentStat struct {
|
||||
EquipmentID uint `json:"equipment_id"`
|
||||
TotalRides int `json:"total_rides"`
|
||||
@@ -81,4 +119,4 @@ func (r *Repository) GetEquipmentStats(userID uint) ([]EquipmentStat, error) {
|
||||
return nil, err
|
||||
}
|
||||
return stats, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,13 @@ func (s *Service) GetUserWorkouts(userID uint) ([]Workout, error) {
|
||||
return s.repo.GetUserWorkouts(userID)
|
||||
}
|
||||
|
||||
func (s *Service) GetUserWorkoutsByTags(userID uint, tags []string) ([]Workout, error) {
|
||||
if len(tags) == 0 {
|
||||
return s.repo.GetUserWorkouts(userID)
|
||||
}
|
||||
return s.repo.GetUserWorkoutsByTags(userID, tags)
|
||||
}
|
||||
|
||||
func (s *Service) GetWorkoutsByMonth(userID uint, year, month int) ([]Workout, error) {
|
||||
return s.repo.GetWorkoutsByMonth(userID, year, month)
|
||||
}
|
||||
@@ -87,6 +94,10 @@ func (s *Service) DeleteWorkout(id, userID uint) error {
|
||||
return s.repo.DeleteWorkout(id, userID)
|
||||
}
|
||||
|
||||
func (s *Service) RemoveDuplicates(userID uint) (int64, error) {
|
||||
return s.repo.RemoveDuplicates(userID)
|
||||
}
|
||||
|
||||
func (s *Service) GetEquipmentStats(userID uint) ([]EquipmentStat, error) {
|
||||
return s.repo.GetEquipmentStats(userID)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user