Ctrl K
ComunidadGuías…

internal/handler/performance_handler.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package handler

import (
	"net/http"
	"strconv"

	"github.com/go-chi/chi/v5"
	"github.com/johnzastrow/actalog/internal/domain"
	"github.com/johnzastrow/actalog/internal/repository"
	"github.com/johnzastrow/actalog/pkg/logger"
	"github.com/johnzastrow/actalog/pkg/middleware"
	"github.com/johnzastrow/actalog/pkg/prmath"
)

// PerformanceHandler handles performance tracking endpoints
type PerformanceHandler struct {
	movementRepo            *repository.MovementRepository
	wodRepo                 *repository.WODRepository
	userWorkoutMovementRepo *repository.UserWorkoutMovementRepository
	userWorkoutWODRepo      *repository.UserWorkoutWODRepository
	logger                  *logger.Logger
}

// MovementPerformanceWithRM extends UserWorkoutMovement with calculated 1RM
// @Description Movement performance with calculated 1RM values
type MovementPerformanceWithRM struct {
	*domain.UserWorkoutMovement
	Calculated1RM *float64 `json:"calculated_1rm,omitempty" example:"315.5"`
	Formula       *string  `json:"formula,omitempty" example:"Epley"`
}

// NewPerformanceHandler creates a new performance handler
func NewPerformanceHandler(
	movementRepo *repository.MovementRepository,
	wodRepo *repository.WODRepository,
	userWorkoutMovementRepo *repository.UserWorkoutMovementRepository,
	userWorkoutWODRepo *repository.UserWorkoutWODRepository,
	logger *logger.Logger,
) *PerformanceHandler {
	return &PerformanceHandler{
		movementRepo:            movementRepo,
		wodRepo:                 wodRepo,
		userWorkoutMovementRepo: userWorkoutMovementRepo,
		userWorkoutWODRepo:      userWorkoutWODRepo,
		logger:                  logger,
	}
}

// UnifiedSearch searches both movements and WODs
// @Summary      Unified search for movements and WODs
// @Description  Search across both movements and WODs in a single query
// @Tags         performance
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        q query string true "Search query"
// @Param        limit query int false "Max results per category (default 10)"
// @Success      200 {object} map[string]interface{} "Combined search results"
// @Failure      400 {object} ErrorResponse "Missing search query"
// @Failure      401 {object} ErrorResponse "Unauthorized"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /performance/search [get]
func (h *PerformanceHandler) UnifiedSearch(w http.ResponseWriter, r *http.Request) {
	// Extract user ID from context (for authorization)
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	query := r.URL.Query().Get("q")
	if query == "" {
		respondError(w, http.StatusBadRequest, "Search query is required")
		return
	}

	limitStr := r.URL.Query().Get("limit")
	limit := 10
	if limitStr != "" {
		if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
			limit = l
		}
	}

	if h.logger != nil {
		h.logger.Info("action=unified_search user_id=%d query=%s limit=%d", userID, query, limit)
	}

	// Search movements
	movements, err := h.movementRepo.Search(query, limit)
	if err != nil {
		if h.logger != nil {
			h.logger.Error("action=unified_search outcome=failure user_id=%d error=%v", userID, err)
		}
		respondError(w, http.StatusInternalServerError, "Failed to search movements")
		return
	}

	// Search WODs
	wods, err := h.wodRepo.Search(query, limit)
	if err != nil {
		if h.logger != nil {
			h.logger.Error("action=unified_search outcome=failure user_id=%d error=%v", userID, err)
		}
		respondError(w, http.StatusInternalServerError, "Failed to search WODs")
		return
	}

	// Format results
	type SearchResult struct {
		Type string      `json:"type"` // "movement" or "wod"
		ID   int64       `json:"id"`
		Name string      `json:"name"`
		Data interface{} `json:"data"` // Full movement or WOD object
	}

	results := []SearchResult{}

	for _, m := range movements {
		results = append(results, SearchResult{
			Type: "movement",
			ID:   m.ID,
			Name: m.Name,
			Data: m,
		})
	}

	for _, w := range wods {
		results = append(results, SearchResult{
			Type: "wod",
			ID:   w.ID,
			Name: w.Name,
			Data: w,
		})
	}

	if h.logger != nil {
		h.logger.Info("action=unified_search outcome=success user_id=%d movements=%d wods=%d", userID, len(movements), len(wods))
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"results": results,
		"count":   len(results),
	})
}

// GetMovementPerformance retrieves all performance history for a specific movement with calculated 1RM
// @Summary      Get movement performance history
// @Description  Retrieve all performance records for a movement with calculated 1RM values
// @Tags         performance
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id path int true "Movement ID"
// @Success      200 {object} map[string]interface{} "Performance history with best 1RM"
// @Failure      400 {object} ErrorResponse "Invalid movement ID"
// @Failure      401 {object} ErrorResponse "Unauthorized"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /performance/movements/{id} [get]
func (h *PerformanceHandler) GetMovementPerformance(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	movementIDStr := chi.URLParam(r, "id")
	movementID, err := strconv.ParseInt(movementIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid movement ID")
		return
	}

	if h.logger != nil {
		h.logger.Info("action=get_movement_performance user_id=%d movement_id=%d", userID, movementID)
	}

	// Get all performance records for this movement
	performances, err := h.userWorkoutMovementRepo.GetByUserIDAndMovementID(userID, movementID, 1000)
	if err != nil {
		if h.logger != nil {
			h.logger.Error("action=get_movement_performance outcome=failure user_id=%d error=%v", userID, err)
		}
		respondError(w, http.StatusInternalServerError, "Failed to retrieve movement performance")
		return
	}

	// Calculate 1RM for each performance and find the best
	performancesWithRM := make([]MovementPerformanceWithRM, 0, len(performances))
	var best1RM *float64
	var bestFormula *string

	for _, perf := range performances {
		perfWithRM := MovementPerformanceWithRM{
			UserWorkoutMovement: perf,
		}

		// Calculate 1RM if weight and reps are present
		if perf.Weight != nil && perf.Reps != nil && *perf.Weight > 0 && *perf.Reps > 0 {
			oneRM, formula := prmath.Calculate1RM(*perf.Weight, *perf.Reps)
			perfWithRM.Calculated1RM = &oneRM
			formulaStr := string(formula)
			perfWithRM.Formula = &formulaStr

			// Track best 1RM
			if best1RM == nil || oneRM > *best1RM {
				best1RM = &oneRM
				bestFormula = &formulaStr
			}
		}

		performancesWithRM = append(performancesWithRM, perfWithRM)
	}

	if h.logger != nil {
		h.logger.Info("action=get_movement_performance outcome=success user_id=%d movement_id=%d records=%d", userID, movementID, len(performances))
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"performances": performancesWithRM,
		"count":        len(performancesWithRM),
		"best_1rm":     best1RM,
		"best_formula": bestFormula,
	})
}

// GetWODPerformance retrieves all performance history for a specific WOD
// @Summary      Get WOD performance history
// @Description  Retrieve all performance records for a specific WOD
// @Tags         performance
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id path int true "WOD ID"
// @Success      200 {object} map[string]interface{} "WOD performance history"
// @Failure      400 {object} ErrorResponse "Invalid WOD ID"
// @Failure      401 {object} ErrorResponse "Unauthorized"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /performance/wods/{id} [get]
func (h *PerformanceHandler) GetWODPerformance(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	wodIDStr := chi.URLParam(r, "id")
	wodID, err := strconv.ParseInt(wodIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid WOD ID")
		return
	}

	if h.logger != nil {
		h.logger.Info("action=get_wod_performance user_id=%d wod_id=%d", userID, wodID)
	}

	// Get all performance records for this WOD
	performances, err := h.userWorkoutWODRepo.GetByUserIDAndWODID(userID, wodID, 1000)
	if err != nil {
		if h.logger != nil {
			h.logger.Error("action=get_wod_performance outcome=failure user_id=%d error=%v", userID, err)
		}
		respondError(w, http.StatusInternalServerError, "Failed to retrieve WOD performance")
		return
	}

	if h.logger != nil {
		h.logger.Info("action=get_wod_performance outcome=success user_id=%d wod_id=%d records=%d", userID, wodID, len(performances))
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"performances": performances,
		"count":        len(performances),
	})
}