package handler import ( "encoding/json" "net/http" "strconv" "time" "github.com/go-chi/chi/v5" "github.com/johnzastrow/actalog/internal/domain" "github.com/johnzastrow/actalog/internal/service" "github.com/johnzastrow/actalog/pkg/logger" "github.com/johnzastrow/actalog/pkg/middleware" ) // UserWorkoutHandler handles logging workout instances type UserWorkoutHandler struct { userWorkoutService *service.UserWorkoutService logger *logger.Logger } // NewUserWorkoutHandler creates a new user workout handler func NewUserWorkoutHandler(userWorkoutService *service.UserWorkoutService, l *logger.Logger) *UserWorkoutHandler { return &UserWorkoutHandler{ userWorkoutService: userWorkoutService, logger: l, } } // LogWorkoutRequest represents a request to log a workout instance // @Description Request to log a workout (either from template or ad-hoc) type LogWorkoutRequest struct { WorkoutID *int64 `json:"workout_id,omitempty" example:"1"` // Template ID (optional for ad-hoc workouts) WorkoutName *string `json:"workout_name,omitempty" example:"Fran"` // Name for ad-hoc workouts (required if workout_id is null) WorkoutDate string `json:"workout_date" example:"2024-01-15"` // YYYY-MM-DD format WorkoutType *string `json:"workout_type,omitempty" example:"AMRAP"` TotalTime *int `json:"total_time,omitempty" example:"1200"` // Duration in seconds Notes *string `json:"notes,omitempty" example:"Felt strong today"` // Performance data Movements []MovementPerformance `json:"movements,omitempty"` WODs []WODPerformance `json:"wods,omitempty"` } // MovementPerformance represents performance data for a single movement // @Description Performance data recorded for a movement in a workout type MovementPerformance struct { MovementID int64 `json:"movement_id" example:"1"` Sets *int `json:"sets,omitempty" example:"5"` Reps *int `json:"reps,omitempty" example:"10"` Weight *float64 `json:"weight,omitempty" example:"135.5"` Time *int `json:"time,omitempty" example:"300"` // in seconds Distance *float64 `json:"distance,omitempty" example:"1000"` Notes string `json:"notes,omitempty" example:"Used belt"` RPE *int `json:"rpe,omitempty" example:"8"` // Rate of Perceived Exertion (2-10) OrderIndex int `json:"order_index" example:"0"` } // WODPerformance represents performance data for a single WOD // @Description Performance data recorded for a WOD in a workout type WODPerformance struct { WODID int64 `json:"wod_id" example:"1"` ScoreType *string `json:"score_type,omitempty" example:"Time"` // Time, Rounds+Reps, Max Weight ScoreValue *string `json:"score_value,omitempty" example:"12:30"` // Formatted score TimeSeconds *int `json:"time_seconds,omitempty" example:"750"` // For time-based WODs Rounds *int `json:"rounds,omitempty" example:"5"` // For AMRAP Reps *int `json:"reps,omitempty" example:"12"` // Remaining reps in AMRAP Weight *float64 `json:"weight,omitempty" example:"225.0"` // For max weight WODs Notes string `json:"notes,omitempty" example:"PR attempt"` RPE *int `json:"rpe,omitempty" example:"9"` // Rate of Perceived Exertion (2-10) OrderIndex int `json:"order_index" example:"0"` } // UpdateLoggedWorkoutRequest represents a request to update a logged workout // @Description Request to update an existing logged workout type UpdateLoggedWorkoutRequest struct { WorkoutName *string `json:"workout_name,omitempty" example:"Custom WOD"` // For ad-hoc workouts WorkoutType *string `json:"workout_type,omitempty" example:"EMOM"` TotalTime *int `json:"total_time,omitempty" example:"900"` Notes *string `json:"notes,omitempty" example:"Updated notes"` Movements []MovementPerformance `json:"movements,omitempty"` WODs []WODPerformance `json:"wods,omitempty"` } // UserWorkoutResponse represents a logged workout instance // @Description Complete logged workout with performance data type UserWorkoutResponse struct { ID int64 `json:"id" example:"1"` UserID int64 `json:"user_id" example:"1"` WorkoutID *int64 `json:"workout_id,omitempty" example:"1"` // Nullable for ad-hoc workouts WorkoutName string `json:"workout_name" example:"Fran"` WorkoutDate string `json:"workout_date" example:"2024-01-15"` WorkoutType *string `json:"workout_type,omitempty" example:"For Time"` TotalTime *int `json:"total_time,omitempty" example:"1200"` Notes *string `json:"notes,omitempty" example:"Felt great"` CreatedAt string `json:"created_at" example:"2024-01-15T10:30:00Z"` UpdatedAt string `json:"updated_at" example:"2024-01-15T10:30:00Z"` Movements []*domain.WorkoutMovement `json:"movements,omitempty"` // Template movements WODs []*domain.WorkoutWODWithDetails `json:"wods,omitempty"` // Template WODs PerformanceMovements []*domain.UserWorkoutMovement `json:"performance_movements,omitempty"` // Actual performance PerformanceWODs []*domain.UserWorkoutWOD `json:"performance_wods,omitempty"` // Actual performance WorkoutNotes *string `json:"workout_notes,omitempty" example:"Template notes"` } // LogWorkout logs a workout instance (user performs a workout template) // @Summary Log a workout // @Description Record a workout with performance data. Can be based on a template or ad-hoc. // @Tags workouts // @Accept json // @Produce json // @Security BearerAuth // @Param request body LogWorkoutRequest true "Workout data with optional performance" // @Success 201 {object} UserWorkoutResponse "Workout logged successfully" // @Failure 400 {object} ErrorResponse "Invalid request body or missing required fields" // @Failure 401 {object} ErrorResponse "Unauthorized" // @Failure 500 {object} ErrorResponse "Internal server error" // @Router /workouts [post] func (h *UserWorkoutHandler) LogWorkout(w http.ResponseWriter, r *http.Request) { // Extract user ID and email from JWT token in context userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } userEmail, _ := middleware.GetUserEmail(r.Context()) var req LogWorkoutRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if h.logger != nil { h.logger.Error("action=log_workout outcome=failure error=json_decode_error details=%v", err) } respondError(w, http.StatusBadRequest, "Invalid request body: "+err.Error()) return } // Validate required fields // Either workout_id (template-based) OR workout_name (ad-hoc) must be provided if (req.WorkoutID == nil || *req.WorkoutID == 0) && (req.WorkoutName == nil || *req.WorkoutName == "") { if h.logger != nil { h.logger.Warn("action=log_workout outcome=failure user_id=%d reason=missing_workout_id_and_name", userID) } respondError(w, http.StatusBadRequest, "Either workout_id or workout_name is required") return } if req.WorkoutDate == "" { respondError(w, http.StatusBadRequest, "Workout date is required") return } // Parse workout date workoutDate, err := time.Parse("2006-01-02", req.WorkoutDate) if err != nil { respondError(w, http.StatusBadRequest, "Invalid workout date format. Use YYYY-MM-DD") return } if h.logger != nil { if req.WorkoutID != nil { h.logger.Info("action=log_workout_attempt user_id=%d workout_id=%d date=%s", userID, *req.WorkoutID, req.WorkoutDate) } else { h.logger.Info("action=log_adhoc_workout_attempt user_id=%d workout_name=%s date=%s", userID, *req.WorkoutName, req.WorkoutDate) } } // Check if performance data was provided var userWorkout *domain.UserWorkout if len(req.Movements) > 0 || len(req.WODs) > 0 { // Convert request movements to domain movements movements := make([]*domain.UserWorkoutMovement, len(req.Movements)) for i, m := range req.Movements { movements[i] = &domain.UserWorkoutMovement{ MovementID: m.MovementID, Sets: m.Sets, Reps: m.Reps, Weight: m.Weight, Time: m.Time, Distance: m.Distance, Notes: m.Notes, RPE: m.RPE, OrderIndex: m.OrderIndex, } } // Convert request WODs to domain WODs wods := make([]*domain.UserWorkoutWOD, len(req.WODs)) for i, w := range req.WODs { wods[i] = &domain.UserWorkoutWOD{ WODID: w.WODID, ScoreType: w.ScoreType, ScoreValue: w.ScoreValue, TimeSeconds: w.TimeSeconds, Rounds: w.Rounds, Reps: w.Reps, Weight: w.Weight, Notes: w.Notes, RPE: w.RPE, OrderIndex: w.OrderIndex, } } // Log workout with performance data userWorkout, err = h.userWorkoutService.LogWorkoutWithPerformance( userID, userEmail, req.WorkoutID, req.WorkoutName, workoutDate, req.Notes, req.TotalTime, req.WorkoutType, movements, wods, ) } else { // Log workout without performance data userWorkout, err = h.userWorkoutService.LogWorkout(userID, userEmail, req.WorkoutID, req.WorkoutName, workoutDate, req.Notes, req.TotalTime, req.WorkoutType) } if err != nil { if h.logger != nil { h.logger.Error("action=log_workout outcome=failure user_id=%d error=%v", userID, err) } respondError(w, http.StatusInternalServerError, "Failed to log workout: "+err.Error()) return } // Retrieve logged workout with details logged, err := h.userWorkoutService.GetLoggedWorkout(userWorkout.ID, userID) if err != nil { if h.logger != nil { h.logger.Error("action=log_workout outcome=failure user_id=%d workout_id=%d error=retrieval_failed %v", userID, req.WorkoutID, err) } respondError(w, http.StatusInternalServerError, "Failed to retrieve logged workout") return } response := UserWorkoutResponse{ ID: logged.ID, UserID: logged.UserID, WorkoutID: logged.WorkoutID, WorkoutName: logged.WorkoutName, WorkoutDate: logged.WorkoutDate.Format("2006-01-02"), WorkoutType: logged.WorkoutType, TotalTime: logged.TotalTime, Notes: logged.Notes, CreatedAt: logged.CreatedAt.Format("2006-01-02T15:04:05Z"), UpdatedAt: logged.UpdatedAt.Format("2006-01-02T15:04:05Z"), Movements: logged.Movements, WODs: logged.WODs, PerformanceMovements: logged.PerformanceMovements, PerformanceWODs: logged.PerformanceWODs, WorkoutNotes: logged.WorkoutDescription, } if h.logger != nil { h.logger.Info("action=log_workout outcome=success user_id=%d logged_id=%d", userID, userWorkout.ID) } respondJSON(w, http.StatusCreated, response) } // GetLoggedWorkout retrieves a logged workout by ID // @Summary Get a logged workout // @Description Retrieve a specific logged workout by ID with all performance data // @Tags workouts // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "Logged workout ID" // @Success 200 {object} UserWorkoutResponse "Logged workout details" // @Failure 400 {object} ErrorResponse "Invalid workout ID" // @Failure 401 {object} ErrorResponse "Unauthorized" // @Failure 500 {object} ErrorResponse "Internal server error" // @Router /workouts/{id} [get] func (h *UserWorkoutHandler) GetLoggedWorkout(w http.ResponseWriter, r *http.Request) { // Extract user ID from JWT token in context userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } idStr := chi.URLParam(r, "id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid workout ID") return } if h.logger != nil { h.logger.Info("action=get_workout user_id=%d workout_id=%d", userID, id) } logged, err := h.userWorkoutService.GetLoggedWorkout(id, userID) if err != nil { if h.logger != nil { h.logger.Error("action=get_workout outcome=failure user_id=%d workout_id=%d error=%v", userID, id, err) } respondError(w, http.StatusInternalServerError, "Failed to retrieve logged workout: "+err.Error()) return } response := UserWorkoutResponse{ ID: logged.ID, UserID: logged.UserID, WorkoutID: logged.WorkoutID, WorkoutName: logged.WorkoutName, WorkoutDate: logged.WorkoutDate.Format("2006-01-02"), WorkoutType: logged.WorkoutType, TotalTime: logged.TotalTime, Notes: logged.Notes, CreatedAt: logged.CreatedAt.Format("2006-01-02T15:04:05Z"), UpdatedAt: logged.UpdatedAt.Format("2006-01-02T15:04:05Z"), Movements: logged.Movements, WODs: logged.WODs, PerformanceMovements: logged.PerformanceMovements, PerformanceWODs: logged.PerformanceWODs, WorkoutNotes: logged.WorkoutDescription, } respondJSON(w, http.StatusOK, response) } // ListLoggedWorkouts retrieves all workouts logged by the user // @Summary List logged workouts // @Description Retrieve all logged workouts for the user with pagination and optional date filtering // @Tags workouts // @Accept json // @Produce json // @Security BearerAuth // @Param limit query int false "Max results (default 20)" // @Param offset query int false "Skip N results (default 0)" // @Param start_date query string false "Filter start date (YYYY-MM-DD)" // @Param end_date query string false "Filter end date (YYYY-MM-DD)" // @Success 200 {object} map[string]interface{} "List of workouts with pagination" // @Failure 400 {object} ErrorResponse "Invalid date format" // @Failure 401 {object} ErrorResponse "Unauthorized" // @Failure 500 {object} ErrorResponse "Internal server error" // @Router /workouts [get] func (h *UserWorkoutHandler) ListLoggedWorkouts(w http.ResponseWriter, r *http.Request) { // Extract user ID from JWT token in context userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } // Parse pagination parameters limitStr := r.URL.Query().Get("limit") offsetStr := r.URL.Query().Get("offset") limit := 20 // default offset := 0 if limitStr != "" { if l, err := strconv.Atoi(limitStr); err == nil && l > 0 { limit = l } } if offsetStr != "" { if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 { offset = o } } // Check for date range filtering startDateStr := r.URL.Query().Get("start_date") endDateStr := r.URL.Query().Get("end_date") var workouts []*domain.UserWorkoutWithDetails var err error if startDateStr != "" && endDateStr != "" { startDate, err1 := time.Parse("2006-01-02", startDateStr) endDate, err2 := time.Parse("2006-01-02", endDateStr) if err1 != nil || err2 != nil { if h.logger != nil { h.logger.Warn("action=list_workouts outcome=failure user_id=%d reason=invalid_date_range start=%s end=%s", userID, startDateStr, endDateStr) } respondError(w, http.StatusBadRequest, "Invalid date format. Use YYYY-MM-DD") return } // Get basic workouts in range basicWorkouts, err := h.userWorkoutService.ListLoggedWorkoutsByDateRange(userID, startDate, endDate) if err != nil { if h.logger != nil { h.logger.Error("action=list_workouts outcome=failure user_id=%d error=%v", userID, err) } respondError(w, http.StatusInternalServerError, "Failed to retrieve logged workouts") return } // Get details for each for _, uw := range basicWorkouts { detailed, err := h.userWorkoutService.GetLoggedWorkout(uw.ID, userID) if err != nil { continue // Skip if error getting details } workouts = append(workouts, detailed) } } else { if h.logger != nil { h.logger.Info("action=list_workouts_attempt user_id=%d limit=%d offset=%d", userID, limit, offset) } workouts, err = h.userWorkoutService.ListLoggedWorkouts(userID, limit, offset) if err != nil { if h.logger != nil { h.logger.Error("action=list_workouts outcome=failure user_id=%d error=%v", userID, err) } respondError(w, http.StatusInternalServerError, "Failed to retrieve logged workouts") return } if h.logger != nil { h.logger.Info("action=list_workouts outcome=success user_id=%d returned=%d", userID, len(workouts)) } } // Build response var responses []UserWorkoutResponse for _, logged := range workouts { response := UserWorkoutResponse{ ID: logged.ID, UserID: logged.UserID, WorkoutID: logged.WorkoutID, WorkoutName: logged.WorkoutName, WorkoutDate: logged.WorkoutDate.Format("2006-01-02"), WorkoutType: logged.WorkoutType, TotalTime: logged.TotalTime, Notes: logged.Notes, CreatedAt: logged.CreatedAt.Format("2006-01-02T15:04:05Z"), UpdatedAt: logged.UpdatedAt.Format("2006-01-02T15:04:05Z"), Movements: logged.Movements, WODs: logged.WODs, PerformanceMovements: logged.PerformanceMovements, PerformanceWODs: logged.PerformanceWODs, WorkoutNotes: logged.WorkoutDescription, } responses = append(responses, response) } respondJSON(w, http.StatusOK, map[string]interface{}{ "workouts": responses, "limit": limit, "offset": offset, }) } // UpdateLoggedWorkout updates a logged workout // @Summary Update a logged workout // @Description Update an existing logged workout's details and performance data // @Tags workouts // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "Logged workout ID" // @Param request body UpdateLoggedWorkoutRequest true "Updated workout data" // @Success 200 {object} UserWorkoutResponse "Updated workout details" // @Failure 400 {object} ErrorResponse "Invalid request" // @Failure 401 {object} ErrorResponse "Unauthorized" // @Failure 403 {object} ErrorResponse "Forbidden - not your workout" // @Failure 404 {object} ErrorResponse "Workout not found" // @Failure 500 {object} ErrorResponse "Internal server error" // @Router /workouts/{id} [put] func (h *UserWorkoutHandler) UpdateLoggedWorkout(w http.ResponseWriter, r *http.Request) { // Extract user ID and email from JWT token in context userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } userEmail, _ := middleware.GetUserEmail(r.Context()) idStr := chi.URLParam(r, "id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid workout ID") return } var req UpdateLoggedWorkoutRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } // Update logged workout with individual fields if h.logger != nil { h.logger.Info("action=update_workout_attempt user_id=%d workout_id=%d", userID, id) } if err := h.userWorkoutService.UpdateLoggedWorkout(id, userID, userEmail, req.WorkoutName, req.Notes, req.TotalTime, req.WorkoutType); err != nil { switch err { case service.ErrUserWorkoutNotFound: if h.logger != nil { h.logger.Warn("action=update_workout outcome=failure user_id=%d workout_id=%d reason=not_found", userID, id) } respondError(w, http.StatusNotFound, "Logged workout not found") case service.ErrUnauthorizedWorkoutAccess: if h.logger != nil { h.logger.Warn("action=update_workout outcome=failure user_id=%d workout_id=%d reason=unauthorized", userID, id) } respondError(w, http.StatusForbidden, "You don't have permission to update this workout") default: if h.logger != nil { h.logger.Error("action=update_workout outcome=failure user_id=%d workout_id=%d error=%v", userID, id, err) } respondError(w, http.StatusInternalServerError, "Failed to update logged workout") } return } // Update movements if provided if len(req.Movements) > 0 { // Convert request movements to domain movements movements := make([]domain.UserWorkoutMovement, len(req.Movements)) for i, m := range req.Movements { movements[i] = domain.UserWorkoutMovement{ MovementID: m.MovementID, Sets: m.Sets, Reps: m.Reps, Weight: m.Weight, Time: m.Time, Distance: m.Distance, Notes: m.Notes, RPE: m.RPE, OrderIndex: m.OrderIndex, } } if err := h.userWorkoutService.UpdateWorkoutMovements(id, userID, movements); err != nil { if h.logger != nil { h.logger.Error("action=update_workout_movements outcome=failure user_id=%d workout_id=%d error=%v", userID, id, err) } respondError(w, http.StatusInternalServerError, "Failed to update workout movements") return } } // Update WODs if provided if len(req.WODs) > 0 { // Convert request WODs to domain WODs wods := make([]domain.UserWorkoutWOD, len(req.WODs)) for i, w := range req.WODs { wods[i] = domain.UserWorkoutWOD{ WODID: w.WODID, ScoreType: w.ScoreType, ScoreValue: w.ScoreValue, TimeSeconds: w.TimeSeconds, Rounds: w.Rounds, Reps: w.Reps, Weight: w.Weight, Notes: w.Notes, RPE: w.RPE, OrderIndex: w.OrderIndex, } } if err := h.userWorkoutService.UpdateWorkoutWODs(id, userID, wods); err != nil { if h.logger != nil { h.logger.Error("action=update_workout_wods outcome=failure user_id=%d workout_id=%d error=%v", userID, id, err) } respondError(w, http.StatusInternalServerError, "Failed to update workout WODs") return } } // Retrieve updated logged workout logged, err := h.userWorkoutService.GetLoggedWorkout(id, userID) if err != nil { respondError(w, http.StatusInternalServerError, "Failed to retrieve updated workout") return } response := UserWorkoutResponse{ ID: logged.ID, UserID: logged.UserID, WorkoutID: logged.WorkoutID, WorkoutName: logged.WorkoutName, WorkoutDate: logged.WorkoutDate.Format("2006-01-02"), WorkoutType: logged.WorkoutType, TotalTime: logged.TotalTime, Notes: logged.Notes, CreatedAt: logged.CreatedAt.Format("2006-01-02T15:04:05Z"), UpdatedAt: logged.UpdatedAt.Format("2006-01-02T15:04:05Z"), Movements: logged.Movements, WODs: logged.WODs, PerformanceMovements: logged.PerformanceMovements, PerformanceWODs: logged.PerformanceWODs, WorkoutNotes: logged.WorkoutDescription, } respondJSON(w, http.StatusOK, response) } // DeleteLoggedWorkout deletes a logged workout // @Summary Delete a logged workout // @Description Delete a specific logged workout and all its performance data // @Tags workouts // @Accept json // @Produce json // @Security BearerAuth // @Param id path int true "Logged workout ID" // @Success 200 {object} MessageResponse "Workout deleted successfully" // @Failure 400 {object} ErrorResponse "Invalid workout ID" // @Failure 401 {object} ErrorResponse "Unauthorized" // @Failure 403 {object} ErrorResponse "Forbidden - not your workout" // @Failure 500 {object} ErrorResponse "Internal server error" // @Router /workouts/{id} [delete] func (h *UserWorkoutHandler) DeleteLoggedWorkout(w http.ResponseWriter, r *http.Request) { // Extract user ID and email from JWT token in context userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } userEmail, _ := middleware.GetUserEmail(r.Context()) idStr := chi.URLParam(r, "id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid workout ID") return } if h.logger != nil { h.logger.Info("action=delete_workout_attempt user_id=%d workout_id=%d", userID, id) } if err := h.userWorkoutService.DeleteLoggedWorkout(id, userID, userEmail); err != nil { if err == service.ErrUnauthorized { if h.logger != nil { h.logger.Warn("action=delete_workout outcome=failure user_id=%d workout_id=%d reason=unauthorized", userID, id) } respondError(w, http.StatusForbidden, "You don't have permission to delete this workout") } else { if h.logger != nil { h.logger.Error("action=delete_workout outcome=failure user_id=%d workout_id=%d error=%v", userID, id, err) } respondError(w, http.StatusInternalServerError, "Failed to delete logged workout: "+err.Error()) } return } if h.logger != nil { h.logger.Info("action=delete_workout outcome=success user_id=%d workout_id=%d", userID, id) } respondJSON(w, http.StatusOK, map[string]string{"message": "Logged workout deleted successfully"}) } // GetMonthlyStats retrieves workout count for a specific month // @Summary Get monthly workout stats // @Description Retrieve the workout count for a specific month // @Tags workouts // @Accept json // @Produce json // @Security BearerAuth // @Param year query int true "Year (e.g., 2024)" // @Param month query int true "Month (1-12)" // @Success 200 {object} map[string]interface{} "Monthly stats with workout count" // @Failure 400 {object} ErrorResponse "Invalid year or month" // @Failure 401 {object} ErrorResponse "Unauthorized" // @Failure 500 {object} ErrorResponse "Internal server error" // @Router /workouts/stats/monthly [get] func (h *UserWorkoutHandler) GetMonthlyStats(w http.ResponseWriter, r *http.Request) { // Extract user ID from JWT token in context userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } // Parse year and month yearStr := r.URL.Query().Get("year") monthStr := r.URL.Query().Get("month") if yearStr == "" || monthStr == "" { respondError(w, http.StatusBadRequest, "Year and month are required") return } year, err1 := strconv.Atoi(yearStr) month, err2 := strconv.Atoi(monthStr) if err1 != nil || err2 != nil || month < 1 || month > 12 { respondError(w, http.StatusBadRequest, "Invalid year or month") return } count, err := h.userWorkoutService.GetWorkoutStatsForMonth(userID, year, month) if err != nil { respondError(w, http.StatusInternalServerError, "Failed to retrieve monthly stats") return } respondJSON(w, http.StatusOK, map[string]interface{}{ "year": year, "month": month, "workout_count": count, }) } // GetPersonalRecords retrieves all personal records (movements and WODs) for the user // @Summary Get personal records // @Description Retrieve all personal records for movements and WODs // @Tags workouts // @Accept json // @Produce json // @Security BearerAuth // @Param limit query int false "Max results per category (default 50)" // @Success 200 {object} map[string]interface{} "PR movements and PR WODs" // @Failure 401 {object} ErrorResponse "Unauthorized" // @Failure 500 {object} ErrorResponse "Internal server error" // @Router /workouts/personal-records [get] func (h *UserWorkoutHandler) GetPersonalRecords(w http.ResponseWriter, r *http.Request) { // Extract user ID from JWT token in context userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } // Parse limit parameter (default 50) limitStr := r.URL.Query().Get("limit") limit := 50 if limitStr != "" { if l, err := strconv.Atoi(limitStr); err == nil && l > 0 { limit = l } } if h.logger != nil { h.logger.Info("action=get_personal_records user_id=%d limit=%d", userID, limit) } // Get PR movements prMovements, err := h.userWorkoutService.GetPRMovements(userID, limit) if err != nil { if h.logger != nil { h.logger.Error("action=get_personal_records outcome=failure user_id=%d error=%v", userID, err) } respondError(w, http.StatusInternalServerError, "Failed to retrieve PR movements") return } // Get PR WODs prWODs, err := h.userWorkoutService.GetPRWODs(userID, limit) if err != nil { if h.logger != nil { h.logger.Error("action=get_personal_records outcome=failure user_id=%d error=%v", userID, err) } respondError(w, http.StatusInternalServerError, "Failed to retrieve PR WODs") return } if h.logger != nil { h.logger.Info("action=get_personal_records outcome=success user_id=%d movements=%d wods=%d", userID, len(prMovements), len(prWODs)) } respondJSON(w, http.StatusOK, map[string]interface{}{ "pr_movements": prMovements, "pr_wods": prWODs, }) } // RetroactiveFlagPRs analyzes all existing workouts and flags PRs based on historical data // @Summary Retroactively flag PRs // @Description Analyze all existing workouts and flag personal records based on historical data // @Tags workouts // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} map[string]interface{} "Count of flagged PRs" // @Failure 401 {object} ErrorResponse "Unauthorized" // @Failure 500 {object} ErrorResponse "Internal server error" // @Router /workouts/retroactive-prs [post] func (h *UserWorkoutHandler) RetroactiveFlagPRs(w http.ResponseWriter, r *http.Request) { // Extract user ID from JWT token in context userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } if h.logger != nil { h.logger.Info("action=retroactive_flag_prs user_id=%d", userID) } // Run retroactive PR flagging movementPRCount, wodPRCount, err := h.userWorkoutService.RetroactivelyFlagPRs(userID) if err != nil { if h.logger != nil { h.logger.Error("action=retroactive_flag_prs outcome=failure user_id=%d error=%v", userID, err) } respondError(w, http.StatusInternalServerError, "Failed to retroactively flag PRs") return } if h.logger != nil { h.logger.Info("action=retroactive_flag_prs outcome=success user_id=%d movement_prs=%d wod_prs=%d", userID, movementPRCount, wodPRCount) } respondJSON(w, http.StatusOK, map[string]interface{}{ "message": "PRs flagged successfully", "movement_pr_count": movementPRCount, "wod_pr_count": wodPRCount, }) } // GetActiveUsersStats handles GET /api/stats/active-users-this-month // @Summary Get active users this month // @Description Retrieve stats about active users who have logged workouts this month // @Tags workouts // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} map[string]interface{} "Active users stats" // @Failure 401 {object} ErrorResponse "Unauthorized" // @Failure 500 {object} ErrorResponse "Internal server error" // @Router /stats/active-users-this-month [get] func (h *UserWorkoutHandler) GetActiveUsersStats(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } stats, err := h.userWorkoutService.GetActiveUsersThisMonth(userID) if err != nil { if h.logger != nil { h.logger.Error("Failed to get active users stats: user_id=%d error=%v", userID, err) } respondError(w, http.StatusInternalServerError, "Failed to get active users stats") return } respondJSON(w, http.StatusOK, map[string]interface{}{ "users": stats, }) } // ErrorResponse represents an error response