Ctrl K
ComunidadGuías…

internal/handler/workout_template_handler.go

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

Archivo original ↗Preguntar a KiBot
package handler

import (
	"encoding/json"
	"net/http"
	"strconv"

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

type WorkoutTemplateService interface {
	Create(userID int64, userEmail, userRole, name string, introWarmup, notes *string, isStandard bool, movements []domain.WorkoutMovement, wods []domain.WorkoutWOD) (*domain.Workout, error)
	GetByID(id int64) (*domain.Workout, error)
	GetByIDWithDetails(id int64) (*domain.Workout, error)
	ListByUser(userID int64, limit, offset int) ([]*domain.Workout, error)
	ListStandard(limit, offset int) ([]*domain.Workout, error)
	Update(id, userID int64, userEmail, userRole, name string, introWarmup, notes *string, isStandard bool, movements []domain.WorkoutMovement, wods []domain.WorkoutWOD) (*domain.Workout, error)
	Delete(id, userID int64, userEmail, userRole string) error
}

type WorkoutTemplateHandler struct {
	service WorkoutTemplateService
}

func NewWorkoutTemplateHandler(service WorkoutTemplateService) *WorkoutTemplateHandler {
	return &WorkoutTemplateHandler{service: service}
}

// CreateTemplate handles POST /api/templates
// @Summary      Create workout template
// @Description  Create a new reusable workout template with movements and WODs
// @Tags         templates
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        request body object true "Template details with movements and WODs"
// @Success      201 {object} map[string]interface{} "Created template"
// @Failure      400 {object} ErrorResponse "Invalid request"
// @Failure      401 {object} ErrorResponse "Unauthorized"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /templates [post]
func (h *WorkoutTemplateHandler) CreateTemplate(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}
	userEmail, _ := middleware.GetUserEmail(r.Context())
	userRole, _ := middleware.GetUserRole(r.Context())

	var req struct {
		Name        string  `json:"name"`
		WorkoutType string  `json:"workout_type"` // Accept but ignore for now
		IntroWarmup *string `json:"intro_warmup"`
		Notes       *string `json:"notes"`
		IsStandard  bool    `json:"is_standard"` // Only admins can create standard workouts
		Movements   []struct {
			MovementID   int64    `json:"movement_id"`
			Sets         *int     `json:"sets"`
			Reps         *int     `json:"reps"`
			Weight       *float64 `json:"weight"`
			Time         *int     `json:"time"`      // Duration in seconds (alternate name)
			WorkTime     *int     `json:"work_time"` // Work duration in seconds
			Distance     *float64 `json:"distance"`
			IsRx         bool     `json:"is_rx"`        // Prescribed standard flag
			IsPR         bool     `json:"is_pr"`        // Personal record flag
			Instructions string   `json:"instructions"` // Markdown instructions
			Notes        string   `json:"notes"`
			OrderIndex   int      `json:"order_index"`
		} `json:"movements"`
		WODs []struct {
			WODID        int64   `json:"wod_id"`
			ScoreValue   *string `json:"score_value"`  // Time, rounds+reps, etc.
			Division     *string `json:"division"`     // rx, scaled, beginner
			IsPR         bool    `json:"is_pr"`        // Personal record flag
			Instructions string  `json:"instructions"` // Markdown instructions
			Notes        string  `json:"notes"`
			OrderIndex   int     `json:"order_index"`
		} `json:"wods"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid request body", http.StatusBadRequest)
		return
	}

	if req.Name == "" {
		http.Error(w, "Template name is required", http.StatusBadRequest)
		return
	}

	// Convert request movements to domain movements
	movements := make([]domain.WorkoutMovement, len(req.Movements))
	for i, m := range req.Movements {
		// Use Time if provided, otherwise fall back to WorkTime
		timeVal := m.Time
		if timeVal == nil {
			timeVal = m.WorkTime
		}
		movements[i] = domain.WorkoutMovement{
			MovementID:   m.MovementID,
			Sets:         m.Sets,
			Reps:         m.Reps,
			Weight:       m.Weight,
			Time:         timeVal,
			Distance:     m.Distance,
			IsRx:         m.IsRx,
			IsPR:         m.IsPR,
			Instructions: m.Instructions,
			Notes:        m.Notes,
			OrderIndex:   m.OrderIndex,
		}
	}

	// Convert request WODs to domain WODs
	wods := make([]domain.WorkoutWOD, len(req.WODs))
	for i, w := range req.WODs {
		wods[i] = domain.WorkoutWOD{
			WODID:        w.WODID,
			ScoreValue:   w.ScoreValue,
			Division:     w.Division,
			IsPR:         w.IsPR,
			Instructions: w.Instructions,
			Notes:        w.Notes,
			OrderIndex:   w.OrderIndex,
		}
	}

	template, err := h.service.Create(userID, userEmail, userRole, req.Name, req.IntroWarmup, req.Notes, req.IsStandard, movements, wods)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"template": template,
	})
}

// GetTemplate handles GET /api/templates/{id}
// @Summary      Get workout template
// @Description  Retrieve a specific workout template by ID with full details
// @Tags         templates
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id path int true "Template ID"
// @Success      200 {object} map[string]interface{} "Template details"
// @Failure      400 {object} ErrorResponse "Invalid template ID"
// @Failure      404 {object} ErrorResponse "Template not found"
// @Router       /templates/{id} [get]
func (h *WorkoutTemplateHandler) GetTemplate(w http.ResponseWriter, r *http.Request) {
	idStr := chi.URLParam(r, "id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		http.Error(w, "Invalid template ID", http.StatusBadRequest)
		return
	}

	template, err := h.service.GetByIDWithDetails(id)
	if err != nil {
		http.Error(w, err.Error(), http.StatusNotFound)
		return
	}
	if template == nil {
		http.Error(w, "Template not found", http.StatusNotFound)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"template": template,
	})
}

// ListMyTemplates handles GET /api/workouts/my-templates
// @Summary      List my workout templates
// @Description  Get all workout templates created by the current user
// @Tags         templates
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        limit query int false "Max results (default 100)"
// @Param        offset query int false "Skip N results (default 0)"
// @Success      200 {object} map[string]interface{} "List of user's templates"
// @Failure      401 {object} ErrorResponse "Unauthorized"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /workouts/my-templates [get]
func (h *WorkoutTemplateHandler) ListMyTemplates(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	limit := 100
	offset := 0

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

	if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" {
		if o, err := strconv.Atoi(offsetStr); err == nil {
			offset = o
		}
	}

	templates, err := h.service.ListByUser(userID, limit, offset)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// Ensure we always return an array, not null
	if templates == nil {
		templates = []*domain.Workout{}
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"workouts": templates,
	})
}

// ListStandardTemplates handles GET /api/templates
// @Summary      List standard workout templates
// @Description  Get all standard (system-provided) workout templates
// @Tags         templates
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        limit query int false "Max results (default 100)"
// @Param        offset query int false "Skip N results (default 0)"
// @Success      200 {object} map[string]interface{} "List of standard templates"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /templates [get]
func (h *WorkoutTemplateHandler) ListStandardTemplates(w http.ResponseWriter, r *http.Request) {
	limit := 100
	offset := 0

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

	if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" {
		if o, err := strconv.Atoi(offsetStr); err == nil {
			offset = o
		}
	}

	templates, err := h.service.ListStandard(limit, offset)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// Ensure we always return an array, not null
	if templates == nil {
		templates = []*domain.Workout{}
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"workouts": templates,
	})
}

// UpdateTemplate handles PUT /api/templates/{id}
// @Summary      Update workout template
// @Description  Update an existing workout template (owner only)
// @Tags         templates
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id path int true "Template ID"
// @Param        request body object true "Updated template details"
// @Success      200 {object} map[string]interface{} "Updated template"
// @Failure      400 {object} ErrorResponse "Invalid request"
// @Failure      401 {object} ErrorResponse "Unauthorized"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /templates/{id} [put]
func (h *WorkoutTemplateHandler) UpdateTemplate(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}
	userEmail, _ := middleware.GetUserEmail(r.Context())
	userRole, _ := middleware.GetUserRole(r.Context())

	idStr := chi.URLParam(r, "id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		http.Error(w, "Invalid template ID", http.StatusBadRequest)
		return
	}

	var req struct {
		Name        string  `json:"name"`
		WorkoutType string  `json:"workout_type"` // Accept but ignore for now
		IntroWarmup *string `json:"intro_warmup"`
		Notes       *string `json:"notes"`
		IsStandard  bool    `json:"is_standard"` // Only admins can change this
		Movements   []struct {
			MovementID   int64    `json:"movement_id"`
			Sets         *int     `json:"sets"`
			Reps         *int     `json:"reps"`
			Weight       *float64 `json:"weight"`
			Time         *int     `json:"time"`      // Duration in seconds (alternate name)
			WorkTime     *int     `json:"work_time"` // Work duration in seconds
			Distance     *float64 `json:"distance"`
			IsRx         bool     `json:"is_rx"`        // Prescribed standard flag
			IsPR         bool     `json:"is_pr"`        // Personal record flag
			Instructions string   `json:"instructions"` // Markdown instructions
			Notes        string   `json:"notes"`
			OrderIndex   int      `json:"order_index"`
		} `json:"movements"`
		WODs []struct {
			WODID        int64   `json:"wod_id"`
			ScoreValue   *string `json:"score_value"`  // Time, rounds+reps, etc.
			Division     *string `json:"division"`     // rx, scaled, beginner
			IsPR         bool    `json:"is_pr"`        // Personal record flag
			Instructions string  `json:"instructions"` // Markdown instructions
			Notes        string  `json:"notes"`
			OrderIndex   int     `json:"order_index"`
		} `json:"wods"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid request body", http.StatusBadRequest)
		return
	}

	if req.Name == "" {
		http.Error(w, "Template name is required", http.StatusBadRequest)
		return
	}

	// Convert request movements to domain movements
	movements := make([]domain.WorkoutMovement, len(req.Movements))
	for i, m := range req.Movements {
		// Use Time if provided, otherwise fall back to WorkTime
		timeVal := m.Time
		if timeVal == nil {
			timeVal = m.WorkTime
		}
		movements[i] = domain.WorkoutMovement{
			MovementID:   m.MovementID,
			Sets:         m.Sets,
			Reps:         m.Reps,
			Weight:       m.Weight,
			Time:         timeVal,
			Distance:     m.Distance,
			IsRx:         m.IsRx,
			IsPR:         m.IsPR,
			Instructions: m.Instructions,
			Notes:        m.Notes,
			OrderIndex:   m.OrderIndex,
		}
	}

	// Convert request WODs to domain WODs
	wods := make([]domain.WorkoutWOD, len(req.WODs))
	for i, w := range req.WODs {
		wods[i] = domain.WorkoutWOD{
			WODID:        w.WODID,
			ScoreValue:   w.ScoreValue,
			Division:     w.Division,
			IsPR:         w.IsPR,
			Instructions: w.Instructions,
			Notes:        w.Notes,
			OrderIndex:   w.OrderIndex,
		}
	}

	template, err := h.service.Update(id, userID, userEmail, userRole, req.Name, req.IntroWarmup, req.Notes, req.IsStandard, movements, wods)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"template": template,
	})
}

// DeleteTemplate handles DELETE /api/templates/{id}
// @Summary      Delete workout template
// @Description  Delete a workout template (owner only)
// @Tags         templates
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id path int true "Template ID"
// @Success      204 "Template deleted"
// @Failure      400 {object} ErrorResponse "Invalid template ID"
// @Failure      401 {object} ErrorResponse "Unauthorized"
// @Failure      403 {object} ErrorResponse "Not the template owner"
// @Router       /templates/{id} [delete]
func (h *WorkoutTemplateHandler) DeleteTemplate(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}
	userEmail, _ := middleware.GetUserEmail(r.Context())
	userRole, _ := middleware.GetUserRole(r.Context())

	idStr := chi.URLParam(r, "id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		http.Error(w, "Invalid template ID", http.StatusBadRequest)
		return
	}

	if err := h.service.Delete(id, userID, userEmail, userRole); err != nil {
		http.Error(w, err.Error(), http.StatusForbidden)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}