package handler import ( "encoding/json" "net/http" "strconv" "time" "github.com/go-chi/chi/v5" "github.com/johnzastrow/actalog/internal/domain" "github.com/johnzastrow/actalog/internal/service" "github.com/johnzastrow/actalog/pkg/logger" "github.com/johnzastrow/actalog/pkg/middleware" ) type SchedulingHandler struct { schedulingService *service.SchedulingService logger *logger.Logger } func NewSchedulingHandler(schedulingService *service.SchedulingService, logger *logger.Logger) *SchedulingHandler { return &SchedulingHandler{ schedulingService: schedulingService, logger: logger, } } // ============================================ // GYM LOCATION HANDLERS // ============================================ // CreateLocation handles POST /api/gyms/{gym_id}/locations func (h *SchedulingHandler) CreateLocation(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } gymIDStr := chi.URLParam(r, "gym_id") gymID, err := strconv.ParseInt(gymIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid gym ID") return } var req struct { Name string `json:"name"` Description *string `json:"description"` Address *string `json:"address"` Capacity int `json:"capacity"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } if req.Name == "" { respondError(w, http.StatusBadRequest, "Location name is required") return } location := &domain.GymLocation{ OrganizationID: gymID, Name: req.Name, Description: req.Description, Address: req.Address, Capacity: req.Capacity, } if err := h.schedulingService.CreateLocation(userID, location); err != nil { if err == service.ErrOrganizationNotFound { respondError(w, http.StatusNotFound, "Organization not found") return } h.logger.Error("Failed to create location: %v", err) respondError(w, http.StatusInternalServerError, "Failed to create location") return } respondJSON(w, http.StatusCreated, location) } // ListLocations handles GET /api/gyms/{gym_id}/locations func (h *SchedulingHandler) ListLocations(w http.ResponseWriter, r *http.Request) { gymIDStr := chi.URLParam(r, "gym_id") gymID, err := strconv.ParseInt(gymIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid gym ID") return } includeInactive := r.URL.Query().Get("include_inactive") == "true" locations, err := h.schedulingService.GetLocationsByOrganization(gymID, includeInactive) if err != nil { h.logger.Error("Failed to list locations: %v", err) respondError(w, http.StatusInternalServerError, "Failed to list locations") return } respondJSON(w, http.StatusOK, map[string]interface{}{ "locations": locations, "count": len(locations), }) } // GetLocation handles GET /api/gyms/{gym_id}/locations/{id} func (h *SchedulingHandler) GetLocation(w http.ResponseWriter, r *http.Request) { idStr := chi.URLParam(r, "id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid location ID") return } location, err := h.schedulingService.GetLocationByID(id) if err != nil { if err == service.ErrGymLocationNotFound { respondError(w, http.StatusNotFound, "Location not found") return } h.logger.Error("Failed to get location: %v", err) respondError(w, http.StatusInternalServerError, "Failed to get location") return } respondJSON(w, http.StatusOK, location) } // UpdateLocation handles PUT /api/gyms/{gym_id}/locations/{id} func (h *SchedulingHandler) UpdateLocation(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } idStr := chi.URLParam(r, "id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid location ID") return } var req struct { Name string `json:"name"` Description *string `json:"description"` Address *string `json:"address"` Capacity int `json:"capacity"` IsActive bool `json:"is_active"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } location := &domain.GymLocation{ ID: id, Name: req.Name, Description: req.Description, Address: req.Address, Capacity: req.Capacity, IsActive: req.IsActive, } if err := h.schedulingService.UpdateLocation(userID, location); err != nil { if err == service.ErrGymLocationNotFound { respondError(w, http.StatusNotFound, "Location not found") return } h.logger.Error("Failed to update location: %v", err) respondError(w, http.StatusInternalServerError, "Failed to update location") return } respondJSON(w, http.StatusOK, location) } // DeleteLocation handles DELETE /api/gyms/{gym_id}/locations/{id} func (h *SchedulingHandler) DeleteLocation(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } idStr := chi.URLParam(r, "id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid location ID") return } if err := h.schedulingService.DeleteLocation(userID, id); err != nil { if err == service.ErrGymLocationNotFound { respondError(w, http.StatusNotFound, "Location not found") return } h.logger.Error("Failed to delete location: %v", err) respondError(w, http.StatusInternalServerError, "Failed to delete location") return } respondJSON(w, http.StatusOK, map[string]string{"message": "Location deleted successfully"}) } // ============================================ // CLASS TEMPLATE HANDLERS // ============================================ // CreateTemplate handles POST /api/gyms/{gym_id}/templates func (h *SchedulingHandler) CreateTemplate(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } gymIDStr := chi.URLParam(r, "gym_id") gymID, err := strconv.ParseInt(gymIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid gym ID") return } var req struct { Name string `json:"name"` Description *string `json:"description"` WorkoutID *int64 `json:"workout_id"` DurationMinutes int `json:"duration_minutes"` DefaultCapacity int `json:"default_capacity"` DefaultLocationID *int64 `json:"default_location_id"` Color string `json:"color"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } if req.Name == "" { respondError(w, http.StatusBadRequest, "Template name is required") return } template := &domain.ClassTemplate{ OrganizationID: gymID, Name: req.Name, Description: req.Description, WorkoutID: req.WorkoutID, DurationMinutes: req.DurationMinutes, DefaultCapacity: req.DefaultCapacity, DefaultLocationID: req.DefaultLocationID, Color: req.Color, } if err := h.schedulingService.CreateTemplate(userID, template); err != nil { if err == service.ErrOrganizationNotFound { respondError(w, http.StatusNotFound, "Organization not found") return } h.logger.Error("Failed to create template: %v", err) respondError(w, http.StatusInternalServerError, "Failed to create template") return } respondJSON(w, http.StatusCreated, template) } // ListTemplates handles GET /api/gyms/{gym_id}/templates func (h *SchedulingHandler) ListTemplates(w http.ResponseWriter, r *http.Request) { gymIDStr := chi.URLParam(r, "gym_id") gymID, err := strconv.ParseInt(gymIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid gym ID") return } includeInactive := r.URL.Query().Get("include_inactive") == "true" templates, err := h.schedulingService.GetTemplatesByOrganization(gymID, includeInactive) if err != nil { h.logger.Error("Failed to list templates: %v", err) respondError(w, http.StatusInternalServerError, "Failed to list templates") return } respondJSON(w, http.StatusOK, map[string]interface{}{ "templates": templates, "count": len(templates), }) } // GetTemplate handles GET /api/templates/{id} func (h *SchedulingHandler) GetTemplate(w http.ResponseWriter, r *http.Request) { idStr := chi.URLParam(r, "id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid template ID") return } template, err := h.schedulingService.GetTemplateByID(id) if err != nil { if err == service.ErrClassTemplateNotFound { respondError(w, http.StatusNotFound, "Template not found") return } h.logger.Error("Failed to get template: %v", err) respondError(w, http.StatusInternalServerError, "Failed to get template") return } respondJSON(w, http.StatusOK, template) } // UpdateTemplate handles PUT /api/templates/{id} func (h *SchedulingHandler) UpdateTemplate(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } idStr := chi.URLParam(r, "id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid template ID") return } var req struct { Name string `json:"name"` Description *string `json:"description"` WorkoutID *int64 `json:"workout_id"` DurationMinutes int `json:"duration_minutes"` DefaultCapacity int `json:"default_capacity"` DefaultLocationID *int64 `json:"default_location_id"` Color string `json:"color"` IsActive bool `json:"is_active"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } template := &domain.ClassTemplate{ ID: id, Name: req.Name, Description: req.Description, WorkoutID: req.WorkoutID, DurationMinutes: req.DurationMinutes, DefaultCapacity: req.DefaultCapacity, DefaultLocationID: req.DefaultLocationID, Color: req.Color, IsActive: req.IsActive, } if err := h.schedulingService.UpdateTemplate(userID, template); err != nil { if err == service.ErrClassTemplateNotFound { respondError(w, http.StatusNotFound, "Template not found") return } h.logger.Error("Failed to update template: %v", err) respondError(w, http.StatusInternalServerError, "Failed to update template") return } respondJSON(w, http.StatusOK, template) } // DeleteTemplate handles DELETE /api/templates/{id} // Query params: // - mode: template_only (default), with_all_sessions, with_future_sessions func (h *SchedulingHandler) DeleteTemplate(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } idStr := chi.URLParam(r, "id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid template ID") return } // Check for mode parameter mode := r.URL.Query().Get("mode") if mode != "" { // Use the new DeleteTemplateWithMode method deleteMode := service.DeleteMode(mode) switch deleteMode { case service.DeleteModeTemplateOnly, service.DeleteModeWithAllSessions, service.DeleteModeWithFutureSessions: // Valid mode default: respondError(w, http.StatusBadRequest, "Invalid mode. Must be: template_only, with_all_sessions, or with_future_sessions") return } result, err := h.schedulingService.DeleteTemplateWithMode(userID, id, deleteMode) if err != nil { if err == service.ErrClassTemplateNotFound { respondError(w, http.StatusNotFound, "Template not found") return } h.logger.Error("Failed to delete template with mode: %v", err) respondError(w, http.StatusInternalServerError, "Failed to delete template") return } respondJSON(w, http.StatusOK, map[string]interface{}{ "message": "Template deleted successfully", "sessions_deleted": result.SessionsDeleted, "notifications_sent": result.NotificationsSent, "credits_refunded": result.CreditsRefunded, "mode": string(result.Mode), }) return } // Default behavior: just delete the template if err := h.schedulingService.DeleteTemplate(userID, id); err != nil { if err == service.ErrClassTemplateNotFound { respondError(w, http.StatusNotFound, "Template not found") return } h.logger.Error("Failed to delete template: %v", err) respondError(w, http.StatusInternalServerError, "Failed to delete template") return } respondJSON(w, http.StatusOK, map[string]string{"message": "Template deleted successfully"}) } // ============================================ // SCHEDULE PREVIEW HANDLERS // ============================================ // PreviewSchedule handles GET /api/templates/{id}/preview-schedule func (h *SchedulingHandler) PreviewSchedule(w http.ResponseWriter, r *http.Request) { idStr := chi.URLParam(r, "id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid template ID") return } // Parse months parameter (default 3) monthsStr := r.URL.Query().Get("months") months := 3 if monthsStr != "" { if m, err := strconv.Atoi(monthsStr); err == nil && m > 0 && m <= 12 { months = m } } dates, err := h.schedulingService.PreviewScheduleDates(id, months) if err != nil { if err == service.ErrClassTemplateNotFound { respondError(w, http.StatusNotFound, "Template not found") return } h.logger.Error("Failed to preview schedule: %v", err) respondError(w, http.StatusInternalServerError, "Failed to preview schedule") return } // Convert dates to strings for JSON response dateStrings := make([]string, len(dates)) for i, d := range dates { dateStrings[i] = d.Format(time.RFC3339) } respondJSON(w, http.StatusOK, map[string]interface{}{ "dates": dateStrings, "count": len(dates), }) } // ============================================ // SCHEDULE SLOT HANDLERS // ============================================ // CreateScheduleSlot handles POST /api/templates/{id}/slots func (h *SchedulingHandler) CreateScheduleSlot(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } templateIDStr := chi.URLParam(r, "id") templateID, err := strconv.ParseInt(templateIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid template ID") return } var req struct { LocationID *int64 `json:"location_id"` DayOfWeek int `json:"day_of_week"` DaysOfWeek []int `json:"days_of_week"` StartTime string `json:"start_time"` OverrideCapacity *int `json:"override_capacity"` RecurrenceInterval int `json:"recurrence_interval"` RecurrenceEndType string `json:"recurrence_end_type"` RecurrenceEndCount *int `json:"recurrence_end_count"` RecurrenceEndDate *string `json:"recurrence_end_date"` EffectiveStartDate *string `json:"effective_start_date"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } if req.StartTime == "" { respondError(w, http.StatusBadRequest, "Start time is required") return } // Use DaysOfWeek if provided, otherwise fall back to DayOfWeek daysOfWeek := req.DaysOfWeek dayOfWeek := req.DayOfWeek if len(daysOfWeek) > 0 { dayOfWeek = daysOfWeek[0] // Set legacy field to first day } else if dayOfWeek >= 0 { daysOfWeek = []int{dayOfWeek} } slot := &domain.ScheduleSlot{ TemplateID: templateID, LocationID: req.LocationID, DayOfWeek: dayOfWeek, DaysOfWeek: daysOfWeek, StartTime: req.StartTime, OverrideCapacity: req.OverrideCapacity, RecurrenceInterval: req.RecurrenceInterval, RecurrenceEndType: req.RecurrenceEndType, RecurrenceEndCount: req.RecurrenceEndCount, } // Parse recurrence end date if req.RecurrenceEndDate != nil && *req.RecurrenceEndDate != "" { t, err := time.Parse("2006-01-02", *req.RecurrenceEndDate) if err == nil { slot.RecurrenceEndDate = &t } } // Parse effective start date if req.EffectiveStartDate != nil && *req.EffectiveStartDate != "" { t, err := time.Parse("2006-01-02", *req.EffectiveStartDate) if err == nil { slot.EffectiveStartDate = &t } } if err := h.schedulingService.CreateScheduleSlot(userID, slot); err != nil { if err == service.ErrClassTemplateNotFound { respondError(w, http.StatusNotFound, "Template not found") return } if err == service.ErrInvalidDayOfWeek { respondError(w, http.StatusBadRequest, "Day of week must be 0-6 (Sunday-Saturday)") return } if err == service.ErrInvalidTimeFormat { respondError(w, http.StatusBadRequest, "Time must be in HH:MM format") return } h.logger.Error("Failed to create schedule slot: %v", err) respondError(w, http.StatusInternalServerError, "Failed to create schedule slot") return } respondJSON(w, http.StatusCreated, slot) } // ListScheduleSlots handles GET /api/templates/{id}/slots func (h *SchedulingHandler) ListScheduleSlots(w http.ResponseWriter, r *http.Request) { templateIDStr := chi.URLParam(r, "id") templateID, err := strconv.ParseInt(templateIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid template ID") return } includeInactive := r.URL.Query().Get("include_inactive") == "true" slots, err := h.schedulingService.GetScheduleSlotsByTemplate(templateID, includeInactive) if err != nil { h.logger.Error("Failed to list schedule slots: %v", err) respondError(w, http.StatusInternalServerError, "Failed to list schedule slots") return } respondJSON(w, http.StatusOK, map[string]interface{}{ "slots": slots, "count": len(slots), }) } // UpdateScheduleSlot handles PUT /api/templates/{template_id}/slots/{id} func (h *SchedulingHandler) UpdateScheduleSlot(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } idStr := chi.URLParam(r, "slot_id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid slot ID") return } var req struct { LocationID *int64 `json:"location_id"` DayOfWeek int `json:"day_of_week"` DaysOfWeek []int `json:"days_of_week"` StartTime string `json:"start_time"` OverrideCapacity *int `json:"override_capacity"` IsActive bool `json:"is_active"` RecurrenceInterval int `json:"recurrence_interval"` RecurrenceEndType string `json:"recurrence_end_type"` RecurrenceEndCount *int `json:"recurrence_end_count"` RecurrenceEndDate *string `json:"recurrence_end_date"` EffectiveStartDate *string `json:"effective_start_date"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } // Use DaysOfWeek if provided, otherwise fall back to DayOfWeek daysOfWeek := req.DaysOfWeek dayOfWeek := req.DayOfWeek if len(daysOfWeek) > 0 { dayOfWeek = daysOfWeek[0] // Set legacy field to first day } else if dayOfWeek >= 0 { daysOfWeek = []int{dayOfWeek} } slot := &domain.ScheduleSlot{ ID: id, LocationID: req.LocationID, DayOfWeek: dayOfWeek, DaysOfWeek: daysOfWeek, StartTime: req.StartTime, OverrideCapacity: req.OverrideCapacity, IsActive: req.IsActive, RecurrenceInterval: req.RecurrenceInterval, RecurrenceEndType: req.RecurrenceEndType, RecurrenceEndCount: req.RecurrenceEndCount, } // Parse recurrence end date if req.RecurrenceEndDate != nil && *req.RecurrenceEndDate != "" { t, err := time.Parse("2006-01-02", *req.RecurrenceEndDate) if err == nil { slot.RecurrenceEndDate = &t } } // Parse effective start date if req.EffectiveStartDate != nil && *req.EffectiveStartDate != "" { t, err := time.Parse("2006-01-02", *req.EffectiveStartDate) if err == nil { slot.EffectiveStartDate = &t } } if err := h.schedulingService.UpdateScheduleSlot(userID, slot); err != nil { if err == service.ErrScheduleSlotNotFound { respondError(w, http.StatusNotFound, "Schedule slot not found") return } if err == service.ErrInvalidDayOfWeek { respondError(w, http.StatusBadRequest, "Day of week must be 0-6 (Sunday-Saturday)") return } h.logger.Error("Failed to update schedule slot: %v", err) respondError(w, http.StatusInternalServerError, "Failed to update schedule slot") return } respondJSON(w, http.StatusOK, slot) } // DeleteScheduleSlot handles DELETE /api/templates/{template_id}/slots/{id} func (h *SchedulingHandler) DeleteScheduleSlot(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } idStr := chi.URLParam(r, "slot_id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid slot ID") return } if err := h.schedulingService.DeleteScheduleSlot(userID, id); err != nil { if err == service.ErrScheduleSlotNotFound { respondError(w, http.StatusNotFound, "Schedule slot not found") return } h.logger.Error("Failed to delete schedule slot: %v", err) respondError(w, http.StatusInternalServerError, "Failed to delete schedule slot") return } respondJSON(w, http.StatusOK, map[string]string{"message": "Schedule slot deleted successfully"}) } // ============================================ // CLASS SESSION HANDLERS // ============================================ // CreateSession handles POST /api/gyms/{gym_id}/sessions func (h *SchedulingHandler) CreateSession(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } gymIDStr := chi.URLParam(r, "gym_id") gymID, err := strconv.ParseInt(gymIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid gym ID") return } var req struct { TemplateID *int64 `json:"template_id"` LocationID *int64 `json:"location_id"` Name string `json:"name"` Description *string `json:"description"` WorkoutID *int64 `json:"workout_id"` StartTime string `json:"start_time"` EndTime string `json:"end_time"` Capacity int `json:"capacity"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } if req.StartTime == "" { respondError(w, http.StatusBadRequest, "Start time is required") return } startTime, err := time.Parse(time.RFC3339, req.StartTime) if err != nil { respondError(w, http.StatusBadRequest, "Invalid start time format (use RFC3339)") return } var endTime time.Time if req.EndTime != "" { endTime, err = time.Parse(time.RFC3339, req.EndTime) if err != nil { respondError(w, http.StatusBadRequest, "Invalid end time format (use RFC3339)") return } } else { // Default to 1 hour session endTime = startTime.Add(time.Hour) } session := &domain.ClassSession{ OrganizationID: gymID, TemplateID: req.TemplateID, LocationID: req.LocationID, Name: req.Name, Description: req.Description, WorkoutID: req.WorkoutID, StartTime: startTime, EndTime: endTime, Capacity: req.Capacity, } if err := h.schedulingService.CreateSession(userID, session); err != nil { if err == service.ErrOrganizationNotFound { respondError(w, http.StatusNotFound, "Organization not found") return } h.logger.Error("Failed to create session: %v", err) respondError(w, http.StatusInternalServerError, "Failed to create session") return } respondJSON(w, http.StatusCreated, session) } // ListSessions handles GET /api/gyms/{gym_id}/sessions func (h *SchedulingHandler) ListSessions(w http.ResponseWriter, r *http.Request) { gymIDStr := chi.URLParam(r, "gym_id") gymID, err := strconv.ParseInt(gymIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid gym ID") return } // Parse date range from query params startDateStr := r.URL.Query().Get("start_date") endDateStr := r.URL.Query().Get("end_date") var startDate, endDate time.Time if startDateStr != "" { startDate, err = time.Parse("2006-01-02", startDateStr) if err != nil { respondError(w, http.StatusBadRequest, "Invalid start_date format (use YYYY-MM-DD)") return } } else { // Default to start of today now := time.Now() startDate = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) } if endDateStr != "" { endDate, err = time.Parse("2006-01-02", endDateStr) if err != nil { respondError(w, http.StatusBadRequest, "Invalid end_date format (use YYYY-MM-DD)") return } // Add 1 day to include the end date endDate = endDate.Add(24*time.Hour - time.Second) } else { // Default to 7 days from start endDate = startDate.Add(7*24*time.Hour - time.Second) } // Check if user is authenticated (for user status) var sessions []*domain.ClassSession userID, hasUser := middleware.GetUserID(r.Context()) if hasUser { sessions, err = h.schedulingService.GetSessionsByOrganizationForUser(gymID, userID, startDate, endDate) } else { sessions, err = h.schedulingService.GetSessionsByOrganization(gymID, startDate, endDate) } if err != nil { h.logger.Error("Failed to list sessions: %v", err) respondError(w, http.StatusInternalServerError, "Failed to list sessions") return } respondJSON(w, http.StatusOK, map[string]interface{}{ "sessions": sessions, "count": len(sessions), "start_date": startDate.Format("2006-01-02"), "end_date": endDate.Format("2006-01-02"), }) } // GetSession handles GET /api/gyms/{gym_id}/sessions/{id} func (h *SchedulingHandler) GetSession(w http.ResponseWriter, r *http.Request) { idStr := chi.URLParam(r, "id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid session ID") return } // Load session with full workout details session, err := h.schedulingService.GetSessionByIDWithWorkout(id) if err != nil { if err == service.ErrClassSessionNotFound { respondError(w, http.StatusNotFound, "Session not found") return } h.logger.Error("Failed to get session: %v", err) respondError(w, http.StatusInternalServerError, "Failed to get session") return } respondJSON(w, http.StatusOK, session) } // UpdateSession handles PUT /api/gyms/{gym_id}/sessions/{id} func (h *SchedulingHandler) UpdateSession(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } idStr := chi.URLParam(r, "id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid session ID") return } var req struct { Name string `json:"name"` Description *string `json:"description"` WorkoutID *int64 `json:"workout_id"` StartTime string `json:"start_time"` EndTime string `json:"end_time"` Capacity int `json:"capacity"` LocationID *int64 `json:"location_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } startTime, err := time.Parse(time.RFC3339, req.StartTime) if err != nil && req.StartTime != "" { respondError(w, http.StatusBadRequest, "Invalid start time format") return } endTime, err := time.Parse(time.RFC3339, req.EndTime) if err != nil && req.EndTime != "" { respondError(w, http.StatusBadRequest, "Invalid end time format") return } session := &domain.ClassSession{ ID: id, Name: req.Name, Description: req.Description, WorkoutID: req.WorkoutID, StartTime: startTime, EndTime: endTime, Capacity: req.Capacity, LocationID: req.LocationID, } if err := h.schedulingService.UpdateSession(userID, session); err != nil { if err == service.ErrClassSessionNotFound { respondError(w, http.StatusNotFound, "Session not found") return } h.logger.Error("Failed to update session: %v", err) respondError(w, http.StatusInternalServerError, "Failed to update session") return } respondJSON(w, http.StatusOK, session) } // CancelSession handles POST /api/gyms/{gym_id}/sessions/{id}/cancel func (h *SchedulingHandler) CancelSession(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } idStr := chi.URLParam(r, "id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid session ID") return } var req struct { Reason string `json:"reason"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } if err := h.schedulingService.CancelSession(userID, id, req.Reason); err != nil { if err == service.ErrClassSessionNotFound { respondError(w, http.StatusNotFound, "Session not found") return } if err == service.ErrSessionNotActive { respondError(w, http.StatusBadRequest, "Session cannot be cancelled") return } h.logger.Error("Failed to cancel session: %v", err) respondError(w, http.StatusInternalServerError, "Failed to cancel session") return } respondJSON(w, http.StatusOK, map[string]string{"message": "Session cancelled successfully"}) } // ============================================ // RESERVATION HANDLERS // ============================================ // CreateReservation handles POST /api/sessions/{session_id}/reserve func (h *SchedulingHandler) CreateReservation(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } sessionIDStr := chi.URLParam(r, "session_id") sessionID, err := strconv.ParseInt(sessionIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid session ID") return } reservation, err := h.schedulingService.CreateReservation(userID, sessionID) if err != nil { if err == service.ErrClassSessionNotFound { respondError(w, http.StatusNotFound, "Session not found") return } if err == service.ErrSessionNotActive { respondError(w, http.StatusBadRequest, "Session is not available for reservations") return } if err == service.ErrAlreadyReserved { respondError(w, http.StatusConflict, "You already have a reservation for this session") return } if err == service.ErrSessionFull { respondError(w, http.StatusConflict, "Session is full") return } h.logger.Error("Failed to create reservation: %v", err) respondError(w, http.StatusInternalServerError, "Failed to create reservation") return } respondJSON(w, http.StatusCreated, reservation) } // CancelReservation handles DELETE /api/sessions/{session_id}/reserve func (h *SchedulingHandler) CancelReservation(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } sessionIDStr := chi.URLParam(r, "session_id") sessionID, err := strconv.ParseInt(sessionIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid session ID") return } var req struct { Reason string `json:"reason"` } // Reason is optional _ = json.NewDecoder(r.Body).Decode(&req) if err := h.schedulingService.CancelReservation(userID, sessionID, req.Reason); err != nil { if err == service.ErrReservationNotFound { respondError(w, http.StatusNotFound, "Reservation not found") return } if err == service.ErrCannotCancelReservation { respondError(w, http.StatusBadRequest, "This reservation cannot be cancelled") return } h.logger.Error("Failed to cancel reservation: %v", err) respondError(w, http.StatusInternalServerError, "Failed to cancel reservation") return } respondJSON(w, http.StatusOK, map[string]string{"message": "Reservation cancelled successfully"}) } // GetUserUpcomingReservations handles GET /api/users/me/reservations/upcoming func (h *SchedulingHandler) GetUserUpcomingReservations(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } reservations, err := h.schedulingService.GetUpcomingUserReservations(userID) if err != nil { h.logger.Error("Failed to get upcoming reservations: %v", err) respondError(w, http.StatusInternalServerError, "Failed to get reservations") return } respondJSON(w, http.StatusOK, map[string]interface{}{ "reservations": reservations, "count": len(reservations), }) } // GetSessionRoster handles GET /api/sessions/{session_id}/roster func (h *SchedulingHandler) GetSessionRoster(w http.ResponseWriter, r *http.Request) { sessionIDStr := chi.URLParam(r, "session_id") sessionID, err := strconv.ParseInt(sessionIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid session ID") return } reservations, err := h.schedulingService.GetReservationRoster(sessionID) if err != nil { h.logger.Error("Failed to get session roster: %v", err) respondError(w, http.StatusInternalServerError, "Failed to get roster") return } respondJSON(w, http.StatusOK, map[string]interface{}{ "roster": reservations, "count": len(reservations), }) } // CheckInReservation handles POST /api/sessions/{session_id}/check-in/{user_id} func (h *SchedulingHandler) CheckInReservation(w http.ResponseWriter, r *http.Request) { coachUserID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } reservationIDStr := chi.URLParam(r, "reservation_id") reservationID, err := strconv.ParseInt(reservationIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid reservation ID") return } if err := h.schedulingService.CheckInReservation(coachUserID, reservationID); err != nil { if err == service.ErrReservationNotFound { respondError(w, http.StatusNotFound, "Reservation not found") return } h.logger.Error("Failed to check in reservation: %v", err) respondError(w, http.StatusInternalServerError, "Failed to check in") return } respondJSON(w, http.StatusOK, map[string]string{"message": "Checked in successfully"}) } // ============================================ // COACH ASSIGNMENT HANDLERS // ============================================ // AssignCoach handles POST /api/gyms/{gym_id}/coaches func (h *SchedulingHandler) AssignCoach(w http.ResponseWriter, r *http.Request) { adminUserID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } gymIDStr := chi.URLParam(r, "gym_id") gymID, err := strconv.ParseInt(gymIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid gym ID") return } var req struct { UserID int64 `json:"user_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } if req.UserID == 0 { respondError(w, http.StatusBadRequest, "User ID is required") return } if err := h.schedulingService.AssignCoach(adminUserID, gymID, req.UserID); err != nil { h.logger.Error("Failed to assign coach: %v", err) respondError(w, http.StatusInternalServerError, "Failed to assign coach") return } respondJSON(w, http.StatusCreated, map[string]string{"message": "Coach assigned successfully"}) } // ListCoaches handles GET /api/gyms/{gym_id}/coaches func (h *SchedulingHandler) ListCoaches(w http.ResponseWriter, r *http.Request) { gymIDStr := chi.URLParam(r, "gym_id") gymID, err := strconv.ParseInt(gymIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid gym ID") return } includeInactive := r.URL.Query().Get("include_inactive") == "true" coaches, err := h.schedulingService.GetCoachesByOrganization(gymID, includeInactive) if err != nil { h.logger.Error("Failed to list coaches: %v", err) respondError(w, http.StatusInternalServerError, "Failed to list coaches") return } respondJSON(w, http.StatusOK, map[string]interface{}{ "coaches": coaches, "count": len(coaches), }) } // UnassignCoach handles DELETE /api/gyms/{gym_id}/coaches/{id} func (h *SchedulingHandler) UnassignCoach(w http.ResponseWriter, r *http.Request) { adminUserID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } idStr := chi.URLParam(r, "id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid coach assignment ID") return } if err := h.schedulingService.UnassignCoach(adminUserID, id); err != nil { if err == service.ErrCoachAssignmentNotFound { respondError(w, http.StatusNotFound, "Coach assignment not found") return } h.logger.Error("Failed to unassign coach: %v", err) respondError(w, http.StatusInternalServerError, "Failed to unassign coach") return } respondJSON(w, http.StatusOK, map[string]string{"message": "Coach unassigned successfully"}) } // ============================================ // COACH PORTAL HANDLERS // ============================================ // GetCoachSessions handles GET /api/coaches/me/sessions // Admins see all upcoming sessions across all organizations. // Coaches see only sessions they are assigned to. func (h *SchedulingHandler) GetCoachSessions(w http.ResponseWriter, r *http.Request) { coachUserID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } limitStr := r.URL.Query().Get("limit") limit := 20 if limitStr != "" { if l, err := strconv.Atoi(limitStr); err == nil && l > 0 { limit = l } } role, _ := middleware.GetUserRole(r.Context()) var sessions []*domain.ClassSession var err error if role == "admin" { sessions, err = h.schedulingService.GetAllUpcomingSessions(limit) } else { sessions, err = h.schedulingService.GetCoachUpcomingSessions(coachUserID, limit) } if err != nil { h.logger.Error("Failed to get coach sessions: %v", err) respondError(w, http.StatusInternalServerError, "Failed to get sessions") return } respondJSON(w, http.StatusOK, map[string]interface{}{ "sessions": sessions, "count": len(sessions), }) } // CompleteSession handles POST /api/sessions/{session_id}/complete func (h *SchedulingHandler) CompleteSession(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } sessionIDStr := chi.URLParam(r, "session_id") sessionID, err := strconv.ParseInt(sessionIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid session ID") return } if err := h.schedulingService.CompleteSession(userID, sessionID); err != nil { if err == service.ErrClassSessionNotFound { respondError(w, http.StatusNotFound, "Session not found") return } if err == service.ErrSessionNotActive { respondError(w, http.StatusBadRequest, "Session cannot be completed") return } h.logger.Error("Failed to complete session: %v", err) respondError(w, http.StatusInternalServerError, "Failed to complete session") return } respondJSON(w, http.StatusOK, map[string]string{"message": "Session completed successfully"}) } // AddSessionCoach handles POST /api/sessions/{session_id}/coaches func (h *SchedulingHandler) AddSessionCoach(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } sessionIDStr := chi.URLParam(r, "session_id") sessionID, err := strconv.ParseInt(sessionIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid session ID") return } var req struct { UserID int64 `json:"user_id"` IsLead bool `json:"is_lead"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } if req.UserID == 0 { respondError(w, http.StatusBadRequest, "user_id is required") return } if err := h.schedulingService.AddCoachToSession(userID, sessionID, req.UserID, req.IsLead); err != nil { if err == service.ErrClassSessionNotFound { respondError(w, http.StatusNotFound, "Session not found") return } h.logger.Error("Failed to add coach to session: %v", err) respondError(w, http.StatusInternalServerError, "Failed to add coach to session") return } respondJSON(w, http.StatusCreated, map[string]string{"message": "Coach added to session"}) } // RemoveSessionCoach handles DELETE /api/sessions/{session_id}/coaches/{user_id} func (h *SchedulingHandler) RemoveSessionCoach(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } sessionIDStr := chi.URLParam(r, "session_id") sessionID, err := strconv.ParseInt(sessionIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid session ID") return } coachUserIDStr := chi.URLParam(r, "user_id") coachUserID, err := strconv.ParseInt(coachUserIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid user ID") return } if err := h.schedulingService.RemoveCoachFromSession(userID, sessionID, coachUserID); err != nil { h.logger.Error("Failed to remove coach from session: %v", err) respondError(w, http.StatusInternalServerError, "Failed to remove coach from session") return } respondJSON(w, http.StatusOK, map[string]string{"message": "Coach removed from session"}) } // GetSessionCoaches handles GET /api/sessions/{session_id}/coaches func (h *SchedulingHandler) GetSessionCoaches(w http.ResponseWriter, r *http.Request) { sessionIDStr := chi.URLParam(r, "session_id") sessionID, err := strconv.ParseInt(sessionIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid session ID") return } coaches, err := h.schedulingService.GetSessionCoaches(sessionID) if err != nil { h.logger.Error("Failed to get session coaches: %v", err) respondError(w, http.StatusInternalServerError, "Failed to get session coaches") return } respondJSON(w, http.StatusOK, map[string]interface{}{ "coaches": coaches, "count": len(coaches), }) } // MarkNoShow handles POST /api/sessions/{session_id}/no-show/{reservation_id} func (h *SchedulingHandler) MarkNoShow(w http.ResponseWriter, r *http.Request) { coachUserID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } reservationIDStr := chi.URLParam(r, "reservation_id") reservationID, err := strconv.ParseInt(reservationIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid reservation ID") return } if err := h.schedulingService.MarkNoShow(coachUserID, reservationID); err != nil { if err == service.ErrReservationNotFound { respondError(w, http.StatusNotFound, "Reservation not found") return } h.logger.Error("Failed to mark no-show: %v", err) respondError(w, http.StatusInternalServerError, "Failed to mark no-show") return } respondJSON(w, http.StatusOK, map[string]string{"message": "Marked as no-show successfully"}) } // GetTemplateCoaches handles GET /api/templates/{id}/coaches func (h *SchedulingHandler) GetTemplateCoaches(w http.ResponseWriter, r *http.Request) { idStr := chi.URLParam(r, "id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid template ID") return } coaches, err := h.schedulingService.GetTemplateCoaches(id) if err != nil { h.logger.Error("Failed to get template coaches: %v", err) respondError(w, http.StatusInternalServerError, "Failed to get template coaches") return } respondJSON(w, http.StatusOK, map[string]interface{}{ "coaches": coaches, }) } // AddTemplateCoach handles POST /api/templates/{id}/coaches func (h *SchedulingHandler) AddTemplateCoach(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } idStr := chi.URLParam(r, "id") templateID, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid template ID") return } var req struct { UserID int64 `json:"user_id"` IsLead bool `json:"is_lead"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } if req.UserID == 0 { respondError(w, http.StatusBadRequest, "User ID is required") return } if err := h.schedulingService.AddTemplateCoach(userID, templateID, req.UserID, req.IsLead); err != nil { h.logger.Error("Failed to add template coach: %v", err) respondError(w, http.StatusInternalServerError, "Failed to add template coach") return } respondJSON(w, http.StatusCreated, map[string]string{"message": "Coach added successfully"}) } // RemoveTemplateCoach handles DELETE /api/templates/{id}/coaches/{user_id} func (h *SchedulingHandler) RemoveTemplateCoach(w http.ResponseWriter, r *http.Request) { adminUserID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } templateIDStr := chi.URLParam(r, "id") templateID, err := strconv.ParseInt(templateIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid template ID") return } userIDStr := chi.URLParam(r, "user_id") userID, err := strconv.ParseInt(userIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid user ID") return } if err := h.schedulingService.RemoveTemplateCoach(adminUserID, templateID, userID); err != nil { h.logger.Error("Failed to remove template coach: %v", err) respondError(w, http.StatusInternalServerError, "Failed to remove template coach") return } respondJSON(w, http.StatusOK, map[string]string{"message": "Coach removed successfully"}) } // BatchUpdateSessionWorkout handles PUT /api/admin/gyms/{gym_id}/sessions/batch-workout func (h *SchedulingHandler) BatchUpdateSessionWorkout(w http.ResponseWriter, r *http.Request) { adminUserID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } gymIDStr := chi.URLParam(r, "gym_id") gymID, err := strconv.ParseInt(gymIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid gym ID") return } var req struct { SessionIDs []int64 `json:"session_ids"` WorkoutID *int64 `json:"workout_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondError(w, http.StatusBadRequest, "Invalid request body") return } if len(req.SessionIDs) == 0 { respondError(w, http.StatusBadRequest, "session_ids is required") return } count, err := h.schedulingService.BatchUpdateSessionWorkout(adminUserID, gymID, req.SessionIDs, req.WorkoutID) if err != nil { h.logger.Error("Failed to batch update session workouts: %v", err) respondError(w, http.StatusInternalServerError, err.Error()) return } respondJSON(w, http.StatusOK, map[string]interface{}{ "updated_count": count, }) } // ============================================ // COACH-SPECIFIC HANDLERS // These handlers verify org-level coach access before delegating // to existing service methods. Used by /api/coaches/ routes. // ============================================ // CoachGetSessionRoster handles GET /api/coaches/sessions/{session_id}/roster func (h *SchedulingHandler) CoachGetSessionRoster(w http.ResponseWriter, r *http.Request) { coachUserID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } sessionIDStr := chi.URLParam(r, "session_id") sessionID, err := strconv.ParseInt(sessionIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid session ID") return } // Admins bypass org check; coaches must be assigned to the session's org role, _ := middleware.GetUserRole(r.Context()) if role != "admin" { if err := h.schedulingService.VerifyCoachAccessToSession(coachUserID, sessionID); err != nil { if err == service.ErrNotAuthorized { respondError(w, http.StatusForbidden, "Not a coach for this organization") return } if err == service.ErrClassSessionNotFound { respondError(w, http.StatusNotFound, "Session not found") return } h.logger.Error("Failed to verify coach access: %v", err) respondError(w, http.StatusInternalServerError, "Failed to verify access") return } } reservations, err := h.schedulingService.GetReservationRoster(sessionID) if err != nil { h.logger.Error("Failed to get session roster: %v", err) respondError(w, http.StatusInternalServerError, "Failed to get roster") return } respondJSON(w, http.StatusOK, map[string]interface{}{ "roster": reservations, "count": len(reservations), }) } // CoachCheckInReservation handles POST /api/coaches/sessions/{session_id}/check-in/{reservation_id} func (h *SchedulingHandler) CoachCheckInReservation(w http.ResponseWriter, r *http.Request) { coachUserID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } sessionIDStr := chi.URLParam(r, "session_id") sessionID, err := strconv.ParseInt(sessionIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid session ID") return } reservationIDStr := chi.URLParam(r, "reservation_id") reservationID, err := strconv.ParseInt(reservationIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid reservation ID") return } role, _ := middleware.GetUserRole(r.Context()) if role != "admin" { if err := h.schedulingService.VerifyCoachAccessToSession(coachUserID, sessionID); err != nil { if err == service.ErrNotAuthorized { respondError(w, http.StatusForbidden, "Not a coach for this organization") return } if err == service.ErrClassSessionNotFound { respondError(w, http.StatusNotFound, "Session not found") return } h.logger.Error("Failed to verify coach access: %v", err) respondError(w, http.StatusInternalServerError, "Failed to verify access") return } } if err := h.schedulingService.CheckInReservation(coachUserID, reservationID); err != nil { if err == service.ErrReservationNotFound { respondError(w, http.StatusNotFound, "Reservation not found") return } h.logger.Error("Failed to check in reservation: %v", err) respondError(w, http.StatusInternalServerError, "Failed to check in") return } respondJSON(w, http.StatusOK, map[string]string{"message": "Checked in successfully"}) } // CoachMarkNoShow handles POST /api/coaches/sessions/{session_id}/no-show/{reservation_id} func (h *SchedulingHandler) CoachMarkNoShow(w http.ResponseWriter, r *http.Request) { coachUserID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } sessionIDStr := chi.URLParam(r, "session_id") sessionID, err := strconv.ParseInt(sessionIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid session ID") return } reservationIDStr := chi.URLParam(r, "reservation_id") reservationID, err := strconv.ParseInt(reservationIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid reservation ID") return } role, _ := middleware.GetUserRole(r.Context()) if role != "admin" { if err := h.schedulingService.VerifyCoachAccessToSession(coachUserID, sessionID); err != nil { if err == service.ErrNotAuthorized { respondError(w, http.StatusForbidden, "Not a coach for this organization") return } if err == service.ErrClassSessionNotFound { respondError(w, http.StatusNotFound, "Session not found") return } h.logger.Error("Failed to verify coach access: %v", err) respondError(w, http.StatusInternalServerError, "Failed to verify access") return } } if err := h.schedulingService.MarkNoShow(coachUserID, reservationID); err != nil { if err == service.ErrReservationNotFound { respondError(w, http.StatusNotFound, "Reservation not found") return } h.logger.Error("Failed to mark no-show: %v", err) respondError(w, http.StatusInternalServerError, "Failed to mark no-show") return } respondJSON(w, http.StatusOK, map[string]string{"message": "Marked as no-show successfully"}) } // CoachCompleteSession handles POST /api/coaches/sessions/{session_id}/complete func (h *SchedulingHandler) CoachCompleteSession(w http.ResponseWriter, r *http.Request) { coachUserID, ok := middleware.GetUserID(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "Unauthorized") return } sessionIDStr := chi.URLParam(r, "session_id") sessionID, err := strconv.ParseInt(sessionIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "Invalid session ID") return } role, _ := middleware.GetUserRole(r.Context()) if role != "admin" { if err := h.schedulingService.VerifyCoachAccessToSession(coachUserID, sessionID); err != nil { if err == service.ErrNotAuthorized { respondError(w, http.StatusForbidden, "Not a coach for this organization") return } if err == service.ErrClassSessionNotFound { respondError(w, http.StatusNotFound, "Session not found") return } h.logger.Error("Failed to verify coach access: %v", err) respondError(w, http.StatusInternalServerError, "Failed to verify access") return } } if err := h.schedulingService.CompleteSession(coachUserID, sessionID); err != nil { if err == service.ErrClassSessionNotFound { respondError(w, http.StatusNotFound, "Session not found") return } if err == service.ErrSessionNotActive { respondError(w, http.StatusBadRequest, "Session cannot be completed") return } h.logger.Error("Failed to complete session: %v", err) respondError(w, http.StatusInternalServerError, "Failed to complete session") return } respondJSON(w, http.StatusOK, map[string]string{"message": "Session completed successfully"}) }