diff --git a/serv_nginx/api_bb/internal/handlers/training_plan_handlerюпщ b/serv_nginx/api_bb/internal/handlers/training_plan_handlerюпщ new file mode 100644 index 0000000..ff1d03f --- /dev/null +++ b/serv_nginx/api_bb/internal/handlers/training_plan_handlerюпщ @@ -0,0 +1,557 @@ +// 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, + } +} \ No newline at end of file diff --git a/serv_nginx/api_bb/internal/models/training_plan.go b/serv_nginx/api_bb/internal/models/training_plan.go index a8092d6..0dea5be 100644 --- a/serv_nginx/api_bb/internal/models/training_plan.go +++ b/serv_nginx/api_bb/internal/models/training_plan.go @@ -73,3 +73,13 @@ type TrainingPlanCreateRequest struct { TargetDistance string `json:"target_distance" validate:"max=50"` TargetDate time.Time `json:"target_date"` } + +// DTO для обновления плана тренировок +type TrainingPlanUpdateRequest struct { + Title string `json:"title" validate:"min=5,max=255"` + Description string `json:"description" validate:"max=1000"` + Weeks int `json:"weeks" validate:"min=1,max=52"` + WorkoutsPerWeek int `json:"workouts_per_week" validate:"min=1,max=7"` + TargetDistance string `json:"target_distance" validate:"max=50"` + TargetDate time.Time `json:"target_date"` +} diff --git a/serv_nginx/api_bb/internal/service/training_plan_service.go b/serv_nginx/api_bb/internal/service/training_plan_service.go new file mode 100644 index 0000000..ae98d0d --- /dev/null +++ b/serv_nginx/api_bb/internal/service/training_plan_service.go @@ -0,0 +1,291 @@ +// service/training_plan_service.go +package service + +import ( + "api_bb/internal/models" + "api_bb/internal/repository" + "api_bb/pkg/logger" + + "go.uber.org/zap" +) + +type TrainingPlanService interface { + CreateTrainingPlan(userID uint, req *models.TrainingPlanCreateRequest) (*models.TrainingPlan, error) + GetTrainingPlansByUserID(userID uint) ([]models.TrainingPlan, error) + GetTrainingPlanByID(userID uint, planID uint) (*models.TrainingPlan, error) + UpdateTrainingPlan(userID uint, planID uint, req *models.TrainingPlanUpdateRequest) (*models.TrainingPlan, error) + DeleteTrainingPlan(userID uint, planID uint) error + GetActiveTrainingPlan(userID uint) (*models.TrainingPlan, error) + MarkTrainingPlanAsCompleted(userID uint, planID uint) error + UpdateCurrentWeek(userID uint, planID uint, currentWeek int) error +} + +type trainingPlanService struct { + trainingPlanRepo repository.TrainingPlanRepository + logger logger.LoggerInterface +} + +func NewTrainingPlanService(trainingPlanRepo repository.TrainingPlanRepository) TrainingPlanService { + return &trainingPlanService{ + trainingPlanRepo: trainingPlanRepo, + logger: logger.NewWrapper(logger.Get().With(zap.String("service", "training_plan"))), + } +} + +// CreateTrainingPlan создает новый план тренировок +func (s *trainingPlanService) CreateTrainingPlan(userID uint, req *models.TrainingPlanCreateRequest) (*models.TrainingPlan, error) { + s.logger.Debug("creating training plan", + zap.Uint("user_id", userID), + zap.String("title", req.Title), + ) + + plan := &models.TrainingPlan{ + UserID: userID, + Title: req.Title, + Description: req.Description, + Weeks: req.Weeks, + WorkoutsPerWeek: req.WorkoutsPerWeek, + TargetDistance: req.TargetDistance, + TargetDate: req.TargetDate, + CurrentWeek: 1, + Completed: false, + } + + if err := s.trainingPlanRepo.Create(plan); err != nil { + s.logger.Error("failed to create training plan in repository", + zap.Uint("user_id", userID), + zap.Error(err), + ) + return nil, err + } + + s.logger.Debug("training plan created successfully", + zap.Uint("user_id", userID), + zap.Uint("plan_id", plan.ID), + ) + + return plan, nil +} + +// GetTrainingPlansByUserID возвращает все планы тренировок пользователя +func (s *trainingPlanService) GetTrainingPlansByUserID(userID uint) ([]models.TrainingPlan, error) { + s.logger.Debug("getting training plans for user", zap.Uint("user_id", userID)) + + plans, err := s.trainingPlanRepo.GetByUserID(userID) + if err != nil { + s.logger.Error("failed to get training plans from repository", + zap.Uint("user_id", userID), + zap.Error(err), + ) + return nil, err + } + + s.logger.Debug("training plans retrieved successfully", + zap.Uint("user_id", userID), + zap.Int("count", len(plans)), + ) + + return plans, nil +} + +// GetTrainingPlanByID возвращает план тренировок по ID +func (s *trainingPlanService) GetTrainingPlanByID(userID uint, planID uint) (*models.TrainingPlan, error) { + s.logger.Debug("getting training plan by ID", + zap.Uint("user_id", userID), + zap.Uint("plan_id", planID), + ) + + plan, err := s.trainingPlanRepo.GetByID(planID) + if err != nil { + s.logger.Error("failed to get training plan from repository", + zap.Uint("user_id", userID), + zap.Uint("plan_id", planID), + zap.Error(err), + ) + return nil, err + } + + // Проверяем, что план принадлежит пользователю + if plan.UserID != userID { + s.logger.Warn("training plan access denied - user mismatch", + zap.Uint("user_id", userID), + zap.Uint("plan_user_id", plan.UserID), + zap.Uint("plan_id", planID), + ) + return nil, repository.ErrNotFound + } + + s.logger.Debug("training plan retrieved successfully", + zap.Uint("user_id", userID), + zap.Uint("plan_id", planID), + ) + + return plan, nil +} + +// UpdateTrainingPlan обновляет план тренировок +func (s *trainingPlanService) UpdateTrainingPlan(userID uint, planID uint, req *models.TrainingPlanUpdateRequest) (*models.TrainingPlan, error) { + s.logger.Debug("updating training plan", + zap.Uint("user_id", userID), + zap.Uint("plan_id", planID), + ) + + // Сначала получаем существующий план + plan, err := s.GetTrainingPlanByID(userID, planID) + if err != nil { + return nil, err + } + + // Обновляем только переданные поля + if req.Title != "" { + plan.Title = req.Title + } + if req.Description != "" { + plan.Description = req.Description + } + if req.Weeks > 0 { + plan.Weeks = req.Weeks + } + if req.WorkoutsPerWeek > 0 { + plan.WorkoutsPerWeek = req.WorkoutsPerWeek + } + if req.TargetDistance != "" { + plan.TargetDistance = req.TargetDistance + } + if !req.TargetDate.IsZero() { + plan.TargetDate = req.TargetDate + } + + // Сохраняем обновления + if err := s.trainingPlanRepo.Update(plan); err != nil { + s.logger.Error("failed to update training plan in repository", + zap.Uint("user_id", userID), + zap.Uint("plan_id", planID), + zap.Error(err), + ) + return nil, err + } + + s.logger.Debug("training plan updated successfully", + zap.Uint("user_id", userID), + zap.Uint("plan_id", planID), + ) + + return plan, nil +} + +// DeleteTrainingPlan удаляет план тренировок +func (s *trainingPlanService) DeleteTrainingPlan(userID uint, planID uint) error { + s.logger.Debug("deleting training plan", + zap.Uint("user_id", userID), + zap.Uint("plan_id", planID), + ) + + // Проверяем, что план существует и принадлежит пользователю + _, err := s.GetTrainingPlanByID(userID, planID) + if err != nil { + return err + } + + // Удаляем план + if err := s.trainingPlanRepo.Delete(planID); err != nil { + s.logger.Error("failed to delete training plan from repository", + zap.Uint("user_id", userID), + zap.Uint("plan_id", planID), + zap.Error(err), + ) + return err + } + + s.logger.Debug("training plan deleted successfully", + zap.Uint("user_id", userID), + zap.Uint("plan_id", planID), + ) + + return nil +} + +// GetActiveTrainingPlan возвращает активный план тренировок пользователя +func (s *trainingPlanService) GetActiveTrainingPlan(userID uint) (*models.TrainingPlan, error) { + s.logger.Debug("getting active training plan for user", zap.Uint("user_id", userID)) + + plan, err := s.trainingPlanRepo.GetActivePlan(userID) + if err != nil { + s.logger.Error("failed to get active training plan from repository", + zap.Uint("user_id", userID), + zap.Error(err), + ) + return nil, err + } + + s.logger.Debug("active training plan retrieved successfully", + zap.Uint("user_id", userID), + zap.Uint("plan_id", plan.ID), + ) + + return plan, nil +} + +// MarkTrainingPlanAsCompleted помечает план тренировок как завершенный +func (s *trainingPlanService) MarkTrainingPlanAsCompleted(userID uint, planID uint) error { + s.logger.Debug("marking training plan as completed", + zap.Uint("user_id", userID), + zap.Uint("plan_id", planID), + ) + + // Проверяем, что план существует и принадлежит пользователю + _, err := s.GetTrainingPlanByID(userID, planID) + if err != nil { + return err + } + + // Помечаем как завершенный + if err := s.trainingPlanRepo.MarkAsCompleted(planID); err != nil { + s.logger.Error("failed to mark training plan as completed in repository", + zap.Uint("user_id", userID), + zap.Uint("plan_id", planID), + zap.Error(err), + ) + return err + } + + s.logger.Debug("training plan marked as completed successfully", + zap.Uint("user_id", userID), + zap.Uint("plan_id", planID), + ) + + return nil +} + +// UpdateCurrentWeek обновляет текущую неделю плана тренировок +func (s *trainingPlanService) UpdateCurrentWeek(userID uint, planID uint, currentWeek int) error { + s.logger.Debug("updating current week for training plan", + zap.Uint("user_id", userID), + zap.Uint("plan_id", planID), + zap.Int("current_week", currentWeek), + ) + + // Проверяем, что план существует и принадлежит пользователю + _, err := s.GetTrainingPlanByID(userID, planID) + if err != nil { + return err + } + + // Обновляем текущую неделю + if err := s.trainingPlanRepo.UpdateCurrentWeek(planID, currentWeek); err != nil { + s.logger.Error("failed to update current week in repository", + zap.Uint("user_id", userID), + zap.Uint("plan_id", planID), + zap.Error(err), + ) + return err + } + + s.logger.Debug("current week updated successfully", + zap.Uint("user_id", userID), + zap.Uint("plan_id", planID), + zap.Int("current_week", currentWeek), + ) + + return nil +} \ No newline at end of file