internal/handler/admin_handler.go
Rama main · réplica de lectura del repositorio original.
package handler
import (
"database/sql"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/go-chi/chi/v5"
"github.com/johnzastrow/actalog/internal/domain"
"github.com/johnzastrow/actalog/internal/service"
"github.com/johnzastrow/actalog/pkg/logger"
)
// AdminHandler handles admin-only operations
type AdminHandler struct {
db *sql.DB
userWorkoutWODRepo domain.UserWorkoutWODRepository
wodRepo domain.WODRepository
movementRepo domain.MovementRepository
workoutRepo domain.WorkoutRepository
userRepo domain.UserRepository
wodService *service.WODService
movementService *service.MovementService
workoutTemplateService *service.WorkoutTemplateService
logger *logger.Logger
}
// NewAdminHandler creates a new admin handler
func NewAdminHandler(
db *sql.DB,
userWorkoutWODRepo domain.UserWorkoutWODRepository,
wodRepo domain.WODRepository,
movementRepo domain.MovementRepository,
workoutRepo domain.WorkoutRepository,
userRepo domain.UserRepository,
wodService *service.WODService,
movementService *service.MovementService,
workoutTemplateService *service.WorkoutTemplateService,
logger *logger.Logger,
) *AdminHandler {
return &AdminHandler{
db: db,
userWorkoutWODRepo: userWorkoutWODRepo,
wodRepo: wodRepo,
movementRepo: movementRepo,
workoutRepo: workoutRepo,
userRepo: userRepo,
wodService: wodService,
movementService: movementService,
workoutTemplateService: workoutTemplateService,
logger: logger,
}
}
// WODMismatch represents a WOD score_type mismatch
type WODMismatch struct {
ID int64 `json:"id"`
WODID int64 `json:"wod_id"`
WODName string `json:"wod_name"`
UserEmail string `json:"user_email"`
WorkoutDate string `json:"workout_date"`
ExpectedScoreType string `json:"expected_score_type"`
Issue string `json:"issue"`
TimeSeconds *int `json:"time_seconds,omitempty"`
Rounds *int `json:"rounds,omitempty"`
Reps *int `json:"reps,omitempty"`
Weight *float64 `json:"weight,omitempty"`
}
// DetectWODScoreTypeMismatches detects WOD records that don't match their score_type
// @Summary Detect WOD score type mismatches (Admin)
// @Description Find WOD records where data doesn't match the expected score_type
// @Tags admin
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]interface{} "List of mismatched records"
// @Failure 500 {object} ErrorResponse "Internal server error"
// @Router /admin/wod-mismatches [get]
func (h *AdminHandler) DetectWODScoreTypeMismatches(w http.ResponseWriter, r *http.Request) {
// Get all WOD performance records with WOD definitions
// Note: This query needs to be run across all users
query := `
SELECT uww.id, uww.wod_id, uww.time_seconds, uww.rounds, uww.reps, uww.weight,
w.name, w.score_type,
u.email,
uw.workout_date
FROM user_workout_wods uww
JOIN wods w ON uww.wod_id = w.id
JOIN user_workouts uw ON uww.user_workout_id = uw.id
JOIN users u ON uw.user_id = u.id
ORDER BY uw.workout_date DESC`
rows, err := h.db.Query(query)
if err != nil {
h.logger.Error("Failed to query WOD records: %v", err)
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"message": "Failed to query WOD records"})
return
}
defer rows.Close()
var mismatches []WODMismatch
for rows.Next() {
var (
id int64
wodID int64
timeSeconds *int
rounds *int
reps *int
weight *float64
wodName string
scoreType string
userEmail string
workoutDate string
)
err := rows.Scan(&id, &wodID, &timeSeconds, &rounds, &reps, &weight, &wodName, &scoreType, &userEmail, &workoutDate)
if err != nil {
h.logger.Error("Failed to scan WOD record: %v", err)
continue
}
// Check for mismatches based on score_type
var issue string
if scoreType == "Time (HH:MM:SS)" {
// Must have time_seconds, must NOT have rounds/reps/weight
if timeSeconds == nil {
issue = "Missing time_seconds for Time-based WOD"
} else if rounds != nil || reps != nil || weight != nil {
issue = "Has invalid fields (rounds/reps/weight) for Time-based WOD"
}
} else if scoreType == "Rounds+Reps" {
// Must have rounds, must NOT have time_seconds/weight
if rounds == nil {
issue = "Missing rounds for Rounds+Reps WOD"
} else if timeSeconds != nil || weight != nil {
issue = "Has invalid fields (time_seconds/weight) for Rounds+Reps WOD"
}
} else if scoreType == "Max Weight" {
// Must have weight, must NOT have time_seconds/rounds/reps
if weight == nil {
issue = "Missing weight for Max Weight WOD"
} else if timeSeconds != nil || rounds != nil || reps != nil {
issue = "Has invalid fields (time_seconds/rounds/reps) for Max Weight WOD"
}
}
// If there's an issue, add to mismatches
if issue != "" {
mismatches = append(mismatches, WODMismatch{
ID: id,
WODID: wodID,
WODName: wodName,
UserEmail: userEmail,
WorkoutDate: workoutDate,
ExpectedScoreType: scoreType,
Issue: issue,
TimeSeconds: timeSeconds,
Rounds: rounds,
Reps: reps,
Weight: weight,
})
}
}
if err := rows.Err(); err != nil {
h.logger.Error("Error iterating WOD records: %v", err)
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"message": "Error processing WOD records"})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"mismatches": mismatches,
"count": len(mismatches),
})
}
// FixWODScoreTypeMismatches deletes WOD records that don't match their score_type
// @Summary Fix WOD score type mismatches (Admin)
// @Description Delete WOD records that don't match their expected score_type
// @Tags admin
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]interface{} "Count of deleted records"
// @Failure 500 {object} ErrorResponse "Internal server error"
// @Router /admin/wod-mismatches/fix [post]
func (h *AdminHandler) FixWODScoreTypeMismatches(w http.ResponseWriter, r *http.Request) {
// First, get all mismatches
query := `
SELECT uww.id, uww.wod_id, uww.time_seconds, uww.rounds, uww.reps, uww.weight,
w.score_type
FROM user_workout_wods uww
JOIN wods w ON uww.wod_id = w.id`
rows, err := h.db.Query(query)
if err != nil {
h.logger.Error("Failed to query WOD records: %v", err)
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"message": "Failed to query WOD records"})
return
}
defer rows.Close()
var idsToDelete []int64
for rows.Next() {
var (
id int64
wodID int64
timeSeconds *int
rounds *int
reps *int
weight *float64
scoreType string
)
err := rows.Scan(&id, &wodID, &timeSeconds, &rounds, &reps, &weight, &scoreType)
if err != nil {
h.logger.Error("Failed to scan WOD record: %v", err)
continue
}
// Check for mismatches based on score_type
isMismatch := false
if scoreType == "Time (HH:MM:SS)" {
if timeSeconds == nil || rounds != nil || reps != nil || weight != nil {
isMismatch = true
}
} else if scoreType == "Rounds+Reps" {
if rounds == nil || timeSeconds != nil || weight != nil {
isMismatch = true
}
} else if scoreType == "Max Weight" {
if weight == nil || timeSeconds != nil || rounds != nil || reps != nil {
isMismatch = true
}
}
if isMismatch {
idsToDelete = append(idsToDelete, id)
}
}
if err := rows.Err(); err != nil {
h.logger.Error("Error iterating WOD records: %v", err)
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"message": "Error processing WOD records"})
return
}
// Delete mismatched records
deletedCount := 0
for _, id := range idsToDelete {
err := h.userWorkoutWODRepo.Delete(id)
if err != nil {
h.logger.Error("Failed to delete WOD record: id=%v error=%v", id, err)
continue
}
deletedCount++
}
h.logger.Info("Deleted mismatched WOD records: count=%v", deletedCount)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"deleted_count": deletedCount,
"total_found": len(idsToDelete),
})
}
// UpdateWODRecordRequest represents the request payload for updating a WOD record
type UpdateWODRecordRequest struct {
TimeSeconds *int `json:"time_seconds"`
Rounds *int `json:"rounds"`
Reps *int `json:"reps"`
Weight *float64 `json:"weight"`
Notes string `json:"notes"`
}
// UpdateWODRecord updates an individual WOD record
// @Summary Update WOD record (Admin)
// @Description Update a specific WOD performance record
// @Tags admin
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "WOD Record ID"
// @Param request body UpdateWODRecordRequest true "Updated record data"
// @Success 200 {object} map[string]interface{} "Success message"
// @Failure 400 {object} ErrorResponse "Invalid request or score type mismatch"
// @Failure 404 {object} ErrorResponse "Record not found"
// @Failure 500 {object} ErrorResponse "Internal server error"
// @Router /admin/wod-records/{id} [put]
func (h *AdminHandler) UpdateWODRecord(w http.ResponseWriter, r *http.Request) {
// Get record ID from URL
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
h.logger.Error("Invalid record ID: id=%v error=%v", idStr, err)
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"message": "Invalid record ID"})
return
}
// Parse request body
var req UpdateWODRecordRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Error("Failed to parse request body: %v", err)
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"message": "Invalid request body"})
return
}
// Get the existing record to find the WOD ID
existingRecord, err := h.userWorkoutWODRepo.GetByID(id)
if err != nil {
h.logger.Error("Failed to get existing WOD record: id=%v error=%v", id, err)
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{"message": "WOD record not found"})
return
}
// Get the WOD definition to validate score_type
wod, err := h.wodRepo.GetByID(existingRecord.WODID)
if err != nil {
h.logger.Error("Failed to get WOD definition: wod_id=%d error=%v", existingRecord.WODID, err)
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"message": "Failed to get WOD definition"})
return
}
// Validate that the update matches the score_type
scoreType := wod.ScoreType
if scoreType == "Time (HH:MM:SS)" {
if req.TimeSeconds == nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{
"message": fmt.Sprintf("WOD '%s' has score_type '%s' but time_seconds is missing", wod.Name, scoreType),
})
return
}
if req.Rounds != nil || req.Reps != nil || req.Weight != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{
"message": fmt.Sprintf("WOD '%s' has score_type '%s' but contains invalid fields (rounds/reps/weight)", wod.Name, scoreType),
})
return
}
} else if scoreType == "Rounds+Reps" {
if req.Rounds == nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{
"message": fmt.Sprintf("WOD '%s' has score_type '%s' but rounds is missing", wod.Name, scoreType),
})
return
}
if req.TimeSeconds != nil || req.Weight != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{
"message": fmt.Sprintf("WOD '%s' has score_type '%s' but contains invalid fields (time_seconds/weight)", wod.Name, scoreType),
})
return
}
} else if scoreType == "Max Weight" {
if req.Weight == nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{
"message": fmt.Sprintf("WOD '%s' has score_type '%s' but weight is missing", wod.Name, scoreType),
})
return
}
if req.TimeSeconds != nil || req.Rounds != nil || req.Reps != nil {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{
"message": fmt.Sprintf("WOD '%s' has score_type '%s' but contains invalid fields (time_seconds/rounds/reps)", wod.Name, scoreType),
})
return
}
}
// Update the record
updatedRecord := &domain.UserWorkoutWOD{
ID: id,
UserWorkoutID: existingRecord.UserWorkoutID,
WODID: existingRecord.WODID,
TimeSeconds: req.TimeSeconds,
Rounds: req.Rounds,
Reps: req.Reps,
Weight: req.Weight,
Notes: req.Notes,
IsPR: existingRecord.IsPR,
OrderIndex: existingRecord.OrderIndex,
}
if err := h.userWorkoutWODRepo.Update(updatedRecord); err != nil {
h.logger.Error("Failed to update WOD record: id=%v error=%v", id, err)
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"message": "Failed to update WOD record"})
return
}
h.logger.Info("Updated WOD record: id=%v wod_name=%v score_type=%v", id, wod.Name, scoreType)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"message": "WOD record updated successfully",
"id": id,
})
}
// ========== User-Created Content Management ==========
// CopyToStandardRequest represents the request to copy an item to standard
type CopyToStandardRequest struct {
NewName string `json:"new_name"`
}
// ListUserCreatedWODs lists all user-created WODs with creator info
// @Summary List user-created WODs (Admin)
// @Description Get all user-created WODs with creator information
// @Tags admin
// @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)"
// @Param search query string false "Search query"
// @Param score_type query string false "Filter by score type"
// @Param creator query string false "Filter by creator email"
// @Success 200 {object} map[string]interface{} "List of user-created WODs"
// @Failure 500 {object} ErrorResponse "Internal server error"
// @Router /admin/user-created/wods [get]
func (h *AdminHandler) ListUserCreatedWODs(w http.ResponseWriter, r *http.Request) {
// Parse pagination params
limit := 100
offset := 0
if l := r.URL.Query().Get("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
limit = parsed
}
}
if o := r.URL.Query().Get("offset"); o != "" {
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
offset = parsed
}
}
// Parse filter params
search := r.URL.Query().Get("search")
scoreType := r.URL.Query().Get("score_type")
creator := r.URL.Query().Get("creator")
wods, count, err := h.wodService.ListAllUserCreatedWithUserInfoFiltered(limit, offset, search, scoreType, creator)
if err != nil {
h.logger.Error("Failed to list user-created WODs: %v", err)
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to list user-created WODs"})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"data": wods,
"count": count,
})
}
// CopyWODToStandard converts a user-created WOD to become a standard WOD
// @Summary Copy WOD to standard (Admin)
// @Description Create a standard WOD from a user-created WOD
// @Tags admin
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "WOD ID"
// @Param request body CopyToStandardRequest true "New name for standard WOD"
// @Success 201 {object} domain.WOD "Created standard WOD"
// @Failure 400 {object} ErrorResponse "Invalid request"
// @Failure 404 {object} ErrorResponse "WOD not found"
// @Failure 409 {object} ErrorResponse "Name already exists"
// @Failure 500 {object} ErrorResponse "Internal server error"
// @Router /admin/user-created/wods/{id}/copy-to-standard [post]
func (h *AdminHandler) CopyWODToStandard(w http.ResponseWriter, r *http.Request) {
// Get WOD ID from URL
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
h.logger.Error("Invalid WOD ID: id=%v error=%v", idStr, err)
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid WOD ID"})
return
}
// Parse request body
var req CopyToStandardRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Error("Failed to parse request body: %v", err)
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid request body"})
return
}
// Copy to standard
wod, err := h.wodService.CopyToStandard(id, req.NewName)
if err != nil {
h.logger.Error("Failed to copy WOD to standard: id=%v error=%v", id, err)
// Check if it's a duplicate name error
if strings.Contains(err.Error(), "already exists") {
w.WriteHeader(http.StatusConflict)
} else if strings.Contains(err.Error(), "not found") {
w.WriteHeader(http.StatusNotFound)
} else {
w.WriteHeader(http.StatusInternalServerError)
}
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
h.logger.Info("Converted WOD to standard: source_id=%v new_id=%v name=%v", id, wod.ID, wod.Name)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(wod)
}
// ListUserCreatedMovements lists all user-created movements with creator info
// @Summary List user-created movements (Admin)
// @Description Get all user-created movements with creator information
// @Tags admin
// @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)"
// @Param search query string false "Search query"
// @Param type query string false "Filter by movement type"
// @Param creator query string false "Filter by creator email"
// @Success 200 {object} map[string]interface{} "List of user-created movements"
// @Failure 500 {object} ErrorResponse "Internal server error"
// @Router /admin/user-created/movements [get]
func (h *AdminHandler) ListUserCreatedMovements(w http.ResponseWriter, r *http.Request) {
// Parse pagination params
limit := 100
offset := 0
if l := r.URL.Query().Get("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
limit = parsed
}
}
if o := r.URL.Query().Get("offset"); o != "" {
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
offset = parsed
}
}
// Parse filter params
search := r.URL.Query().Get("search")
movementType := r.URL.Query().Get("type")
creator := r.URL.Query().Get("creator")
movements, count, err := h.movementService.ListAllUserCreatedWithUserInfoFiltered(limit, offset, search, movementType, creator)
if err != nil {
h.logger.Error("Failed to list user-created movements: %v", err)
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to list user-created movements"})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"data": movements,
"count": count,
})
}
// CopyMovementToStandard converts a user-created movement to become a standard movement
// @Summary Copy movement to standard (Admin)
// @Description Create a standard movement from a user-created movement
// @Tags admin
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Movement ID"
// @Param request body CopyToStandardRequest true "New name for standard movement"
// @Success 201 {object} domain.Movement "Created standard movement"
// @Failure 400 {object} ErrorResponse "Invalid request"
// @Failure 404 {object} ErrorResponse "Movement not found"
// @Failure 409 {object} ErrorResponse "Name already exists"
// @Failure 500 {object} ErrorResponse "Internal server error"
// @Router /admin/user-created/movements/{id}/copy-to-standard [post]
func (h *AdminHandler) CopyMovementToStandard(w http.ResponseWriter, r *http.Request) {
// Get movement ID from URL
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
h.logger.Error("Invalid movement ID: id=%v error=%v", idStr, err)
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid movement ID"})
return
}
// Parse request body
var req CopyToStandardRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Error("Failed to parse request body: %v", err)
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid request body"})
return
}
// Copy to standard
movement, err := h.movementService.CopyToStandard(id, req.NewName)
if err != nil {
h.logger.Error("Failed to copy movement to standard: id=%v error=%v", id, err)
// Check if it's a duplicate name error
if strings.Contains(err.Error(), "already exists") {
w.WriteHeader(http.StatusConflict)
} else if strings.Contains(err.Error(), "not found") {
w.WriteHeader(http.StatusNotFound)
} else {
w.WriteHeader(http.StatusInternalServerError)
}
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
h.logger.Info("Converted movement to standard: source_id=%v new_id=%v name=%v", id, movement.ID, movement.Name)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(movement)
}
// ListUserCreatedWorkouts lists all user-created workout templates with creator info
// @Summary List user-created workouts (Admin)
// @Description Get all user-created workout templates with creator information
// @Tags admin
// @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)"
// @Param search query string false "Search query"
// @Param creator query string false "Filter by creator email"
// @Success 200 {object} map[string]interface{} "List of user-created workouts"
// @Failure 500 {object} ErrorResponse "Internal server error"
// @Router /admin/user-created/workouts [get]
func (h *AdminHandler) ListUserCreatedWorkouts(w http.ResponseWriter, r *http.Request) {
// Parse pagination params
limit := 100
offset := 0
if l := r.URL.Query().Get("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
limit = parsed
}
}
if o := r.URL.Query().Get("offset"); o != "" {
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
offset = parsed
}
}
// Parse filter params
search := r.URL.Query().Get("search")
creator := r.URL.Query().Get("creator")
workouts, count, err := h.workoutTemplateService.ListAllUserCreatedWithUserInfoFiltered(limit, offset, search, creator)
if err != nil {
h.logger.Error("Failed to list user-created workouts: %v", err)
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": "Failed to list user-created workouts"})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"data": workouts,
"count": count,
})
}
// CopyWorkoutToStandard converts a user-created workout to become a standard workout
// @Summary Copy workout to standard (Admin)
// @Description Create a standard workout template from a user-created workout
// @Tags admin
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Workout ID"
// @Param request body CopyToStandardRequest true "New name for standard workout"
// @Success 201 {object} domain.Workout "Created standard workout"
// @Failure 400 {object} ErrorResponse "Invalid request"
// @Failure 404 {object} ErrorResponse "Workout not found"
// @Failure 409 {object} ErrorResponse "Name already exists"
// @Failure 500 {object} ErrorResponse "Internal server error"
// @Router /admin/user-created/workouts/{id}/copy-to-standard [post]
func (h *AdminHandler) CopyWorkoutToStandard(w http.ResponseWriter, r *http.Request) {
// Get workout ID from URL
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
h.logger.Error("Invalid workout ID: id=%v error=%v", idStr, err)
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid workout ID"})
return
}
// Parse request body
var req CopyToStandardRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Error("Failed to parse request body: %v", err)
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "Invalid request body"})
return
}
// Copy to standard
workout, err := h.workoutTemplateService.CopyToStandard(id, req.NewName)
if err != nil {
h.logger.Error("Failed to copy workout to standard: id=%v error=%v", id, err)
// Check if it's a duplicate name error
if strings.Contains(err.Error(), "already exists") {
w.WriteHeader(http.StatusConflict)
} else if strings.Contains(err.Error(), "not found") {
w.WriteHeader(http.StatusNotFound)
} else {
w.WriteHeader(http.StatusInternalServerError)
}
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
h.logger.Info("Converted workout to standard: source_id=%v new_id=%v name=%v", id, workout.ID, workout.Name)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(workout)
}