// handlers/training_plan_handler.go package handlers import ( "encoding/json" "net/http" "strconv" "time" "api_bb/internal/models" "api_bb/internal/service" "api_bb/pkg/logger" "api_bb/pkg/middleware" "api_bb/pkg/utils" "go.uber.org/zap" ) type TrainingPlanHandler struct { logger logger.LoggerInterface trainingPlanService service.TrainingPlanService } func NewTrainingPlanHandler(trainingPlanService service.TrainingPlanService) *TrainingPlanHandler { return &TrainingPlanHandler{ logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "training_plan"))), trainingPlanService: trainingPlanService, } } // TrainingPlanResponse - DTO для ответа с планом тренировок type TrainingPlanResponse struct { ID uint `json:"id"` UserID uint `json:"user_id"` Title string `json:"title"` Description string `json:"description"` Weeks int `json:"weeks"` WorkoutsPerWeek int `json:"workouts_per_week"` TargetDistance string `json:"target_distance"` TargetDate time.Time `json:"target_date"` CurrentWeek int `json:"current_week"` Completed bool `json:"completed"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` Workouts []TrainingWorkoutResponse `json:"workouts,omitempty"` } // TrainingWorkoutResponse - DTO для ответа с тренировкой плана type TrainingWorkoutResponse struct { ID uint `json:"id"` PlanID uint `json:"plan_id"` Week int `json:"week"` Day int `json:"day"` Type models.WorkoutType `json:"type"` Description string `json:"description"` Distance float64 `json:"distance_km"` Duration int `json:"duration_min"` Completed bool `json:"completed"` CompletedAt *time.Time `json:"completed_at"` CreatedAt time.Time `json:"created_at"` } // CreateTrainingPlan создает новый план тренировок func (h *TrainingPlanHandler) CreateTrainingPlan(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling create training plan request", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.String("remote_addr", r.RemoteAddr), ) // Получаем пользователя из контекста user, ok := middleware.GetUserFromContext(r.Context()) if !ok { h.logger.Warn("create training plan failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } var req models.TrainingPlanCreateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.logger.Error("failed to decode JSON payload", zap.Error(err)) utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error()) return } h.logger.Debug("creating training plan", zap.Uint("user_id", user.ID), zap.String("title", req.Title), zap.Int("weeks", req.Weeks), zap.Int("workouts_per_week", req.WorkoutsPerWeek), ) // Создаем план тренировок через сервис plan, err := h.trainingPlanService.CreateTrainingPlan(user.ID, &req) if err != nil { h.logger.Error("failed to create training plan in service", zap.Uint("user_id", user.ID), zap.Error(err), ) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create training plan: "+err.Error()) return } h.logger.Info("training plan created successfully", zap.Uint("user_id", user.ID), zap.Uint("plan_id", plan.ID), ) utils.RespondWithJSON(w, http.StatusCreated, map[string]interface{}{ "message": "Training plan created successfully", "plan": toTrainingPlanResponse(plan), }) } // GetTrainingPlans возвращает все планы тренировок пользователя func (h *TrainingPlanHandler) GetTrainingPlans(w http.ResponseWriter, r *http.Request) { h.logger.Debug("handling get training plans request", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.String("remote_addr", r.RemoteAddr), ) // Получаем пользователя из контекста user, ok := middleware.GetUserFromContext(r.Context()) if !ok { h.logger.Warn("get training plans failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } h.logger.Debug("getting training plans for user", zap.Uint("user_id", user.ID)) // Получаем планы тренировок через сервис plans, err := h.trainingPlanService.GetTrainingPlansByUserID(user.ID) if err != nil { h.logger.Error("failed to get training plans from service", zap.Uint("user_id", user.ID), zap.Error(err), ) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get training plans: "+err.Error()) return } // Преобразуем в response формат var planResponses []TrainingPlanResponse for _, plan := range plans { planResponses = append(planResponses, toTrainingPlanResponse(&plan)) } h.logger.Debug("training plans retrieved successfully", zap.Uint("user_id", user.ID), zap.Int("plans_count", len(planResponses)), ) utils.RespondWithJSON(w, http.StatusOK, planResponses) } // GetTrainingPlanByID возвращает план тренировок по ID func (h *TrainingPlanHandler) GetTrainingPlanByID(w http.ResponseWriter, r *http.Request) { h.logger.Debug("handling get training plan by ID request", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.String("remote_addr", r.RemoteAddr), ) // Получаем пользователя из контекста user, ok := middleware.GetUserFromContext(r.Context()) if !ok { h.logger.Warn("get training plan failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } // Извлекаем ID плана из URL параметров planIDStr := r.URL.Query().Get("id") if planIDStr == "" { h.logger.Warn("get training plan failed - plan ID required") utils.RespondWithError(w, http.StatusBadRequest, "Plan ID is required") return } planID, err := strconv.ParseUint(planIDStr, 10, 32) if err != nil { h.logger.Warn("get training plan failed - invalid plan ID", zap.String("plan_id", planIDStr), zap.Error(err), ) utils.RespondWithError(w, http.StatusBadRequest, "Invalid plan ID") return } h.logger.Debug("getting training plan by ID", zap.Uint("user_id", user.ID), zap.Uint("plan_id", uint(planID)), ) // Получаем план тренировок через сервис plan, err := h.trainingPlanService.GetTrainingPlanByID(user.ID, uint(planID)) if err != nil { h.logger.Error("failed to get training plan from service", zap.Uint("user_id", user.ID), zap.Uint("plan_id", uint(planID)), zap.Error(err), ) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get training plan: "+err.Error()) return } h.logger.Debug("training plan retrieved successfully", zap.Uint("user_id", user.ID), zap.Uint("plan_id", uint(planID)), ) utils.RespondWithJSON(w, http.StatusOK, toTrainingPlanResponse(plan)) } // UpdateTrainingPlan обновляет план тренировок func (h *TrainingPlanHandler) UpdateTrainingPlan(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling update training plan request", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.String("remote_addr", r.RemoteAddr), ) // Получаем пользователя из контекста user, ok := middleware.GetUserFromContext(r.Context()) if !ok { h.logger.Warn("update training plan failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } // Извлекаем ID плана из URL параметров planIDStr := r.URL.Query().Get("id") if planIDStr == "" { h.logger.Warn("update training plan failed - plan ID required") utils.RespondWithError(w, http.StatusBadRequest, "Plan ID is required") return } planID, err := strconv.ParseUint(planIDStr, 10, 32) if err != nil { h.logger.Warn("update training plan failed - invalid plan ID", zap.String("plan_id", planIDStr), zap.Error(err), ) utils.RespondWithError(w, http.StatusBadRequest, "Invalid plan ID") return } var req models.TrainingPlanUpdateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.logger.Error("failed to decode JSON payload", zap.Error(err)) utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error()) return } h.logger.Info("updating training plan", zap.Uint("user_id", user.ID), zap.Uint("plan_id", uint(planID)), zap.String("title", req.Title), ) // Обновляем план тренировок через сервис plan, err := h.trainingPlanService.UpdateTrainingPlan(user.ID, uint(planID), &req) if err != nil { h.logger.Error("failed to update training plan in service", zap.Uint("user_id", user.ID), zap.Uint("plan_id", uint(planID)), zap.Error(err), ) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update training plan: "+err.Error()) return } h.logger.Info("training plan updated successfully", zap.Uint("user_id", user.ID), zap.Uint("plan_id", uint(planID)), ) utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{ "message": "Training plan updated successfully", "plan": toTrainingPlanResponse(plan), }) } // DeleteTrainingPlan удаляет план тренировок func (h *TrainingPlanHandler) DeleteTrainingPlan(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling delete training plan request", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.String("remote_addr", r.RemoteAddr), ) // Получаем пользователя из контекста user, ok := middleware.GetUserFromContext(r.Context()) if !ok { h.logger.Warn("delete training plan failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } // Извлекаем ID плана из URL параметров planIDStr := r.URL.Query().Get("id") if planIDStr == "" { h.logger.Warn("delete training plan failed - plan ID required") utils.RespondWithError(w, http.StatusBadRequest, "Plan ID is required") return } planID, err := strconv.ParseUint(planIDStr, 10, 32) if err != nil { h.logger.Warn("delete training plan failed - invalid plan ID", zap.String("plan_id", planIDStr), zap.Error(err), ) utils.RespondWithError(w, http.StatusBadRequest, "Invalid plan ID") return } h.logger.Info("deleting training plan", zap.Uint("user_id", user.ID), zap.Uint("plan_id", uint(planID)), ) // Удаляем план тренировок через сервис if err := h.trainingPlanService.DeleteTrainingPlan(user.ID, uint(planID)); err != nil { h.logger.Error("failed to delete training plan in service", zap.Uint("user_id", user.ID), zap.Uint("plan_id", uint(planID)), zap.Error(err), ) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete training plan: "+err.Error()) return } h.logger.Info("training plan deleted successfully", zap.Uint("user_id", user.ID), zap.Uint("plan_id", uint(planID)), ) utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{ "message": "Training plan deleted successfully", }) } // GetActiveTrainingPlan возвращает активный план тренировок пользователя func (h *TrainingPlanHandler) GetActiveTrainingPlan(w http.ResponseWriter, r *http.Request) { h.logger.Debug("handling get active training plan request", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.String("remote_addr", r.RemoteAddr), ) // Получаем пользователя из контекста user, ok := middleware.GetUserFromContext(r.Context()) if !ok { h.logger.Warn("get active training plan failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } h.logger.Debug("getting active training plan for user", zap.Uint("user_id", user.ID)) // Получаем активный план тренировок через сервис plan, err := h.trainingPlanService.GetActiveTrainingPlan(user.ID) if err != nil { h.logger.Error("failed to get active training plan from service", zap.Uint("user_id", user.ID), zap.Error(err), ) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get active training plan: "+err.Error()) return } h.logger.Debug("active training plan retrieved successfully", zap.Uint("user_id", user.ID), zap.Uint("plan_id", plan.ID), ) utils.RespondWithJSON(w, http.StatusOK, toTrainingPlanResponse(plan)) } // MarkTrainingPlanAsCompleted помечает план тренировок как завершенный func (h *TrainingPlanHandler) MarkTrainingPlanAsCompleted(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling mark training plan as completed request", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.String("remote_addr", r.RemoteAddr), ) // Получаем пользователя из контекста user, ok := middleware.GetUserFromContext(r.Context()) if !ok { h.logger.Warn("mark training plan as completed failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } // Извлекаем ID плана из URL параметров planIDStr := r.URL.Query().Get("id") if planIDStr == "" { h.logger.Warn("mark training plan as completed failed - plan ID required") utils.RespondWithError(w, http.StatusBadRequest, "Plan ID is required") return } planID, err := strconv.ParseUint(planIDStr, 10, 32) if err != nil { h.logger.Warn("mark training plan as completed failed - invalid plan ID", zap.String("plan_id", planIDStr), zap.Error(err), ) utils.RespondWithError(w, http.StatusBadRequest, "Invalid plan ID") return } h.logger.Info("marking training plan as completed", zap.Uint("user_id", user.ID), zap.Uint("plan_id", uint(planID)), ) // Помечаем план как завершенный через сервис if err := h.trainingPlanService.MarkTrainingPlanAsCompleted(user.ID, uint(planID)); err != nil { h.logger.Error("failed to mark training plan as completed in service", zap.Uint("user_id", user.ID), zap.Uint("plan_id", uint(planID)), zap.Error(err), ) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to mark training plan as completed: "+err.Error()) return } h.logger.Info("training plan marked as completed successfully", zap.Uint("user_id", user.ID), zap.Uint("plan_id", uint(planID)), ) utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{ "message": "Training plan marked as completed successfully", }) } // UpdateCurrentWeek обновляет текущую неделю плана тренировок func (h *TrainingPlanHandler) UpdateCurrentWeek(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling update current week request", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.String("remote_addr", r.RemoteAddr), ) // Получаем пользователя из контекста user, ok := middleware.GetUserFromContext(r.Context()) if !ok { h.logger.Warn("update current week failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } // Извлекаем ID плана из URL параметров planIDStr := r.URL.Query().Get("id") if planIDStr == "" { h.logger.Warn("update current week failed - plan ID required") utils.RespondWithError(w, http.StatusBadRequest, "Plan ID is required") return } planID, err := strconv.ParseUint(planIDStr, 10, 32) if err != nil { h.logger.Warn("update current week failed - invalid plan ID", zap.String("plan_id", planIDStr), zap.Error(err), ) utils.RespondWithError(w, http.StatusBadRequest, "Invalid plan ID") return } var req struct { CurrentWeek int `json:"current_week" validate:"required,min=1,max=52"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.logger.Error("failed to decode JSON payload", zap.Error(err)) utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error()) return } h.logger.Info("updating current week for training plan", zap.Uint("user_id", user.ID), zap.Uint("plan_id", uint(planID)), zap.Int("current_week", req.CurrentWeek), ) // Обновляем текущую неделю через сервис if err := h.trainingPlanService.UpdateCurrentWeek(user.ID, uint(planID), req.CurrentWeek); err != nil { h.logger.Error("failed to update current week in service", zap.Uint("user_id", user.ID), zap.Uint("plan_id", uint(planID)), zap.Error(err), ) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update current week: "+err.Error()) return } h.logger.Info("current week updated successfully", zap.Uint("user_id", user.ID), zap.Uint("plan_id", uint(planID)), zap.Int("current_week", req.CurrentWeek), ) utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{ "message": "Current week updated successfully", }) } // Вспомогательные функции для преобразования моделей в DTO func toTrainingPlanResponse(plan *models.TrainingPlan) TrainingPlanResponse { response := TrainingPlanResponse{ ID: plan.ID, UserID: plan.UserID, Title: plan.Title, Description: plan.Description, Weeks: plan.Weeks, WorkoutsPerWeek: plan.WorkoutsPerWeek, TargetDistance: plan.TargetDistance, TargetDate: plan.TargetDate, CurrentWeek: plan.CurrentWeek, Completed: plan.Completed, CreatedAt: plan.CreatedAt, UpdatedAt: plan.UpdatedAt, } // Преобразуем тренировки, если они загружены if plan.Workouts != nil { for _, workout := range plan.Workouts { response.Workouts = append(response.Workouts, toTrainingWorkoutResponse(&workout)) } } return response } func toTrainingWorkoutResponse(workout *models.TrainingWorkout) TrainingWorkoutResponse { return TrainingWorkoutResponse{ ID: workout.ID, PlanID: workout.PlanID, Week: workout.Week, Day: workout.Day, Type: workout.Type, Description: workout.Description, Distance: workout.Distance, Duration: workout.Duration, Completed: workout.Completed, CompletedAt: workout.CompletedAt, CreatedAt: workout.CreatedAt, } }