package handlers import ( "fmt" "net/http" "strconv" "time" "attune-heart-therapy/internal/middleware" "attune-heart-therapy/internal/models" "attune-heart-therapy/internal/repositories" "attune-heart-therapy/internal/services" "github.com/gin-gonic/gin" ) type BookingHandler struct { bookingService services.BookingService userRepo repositories.UserRepository jitsiService services.JitsiService } func NewBookingHandler(bookingService services.BookingService, userRepo repositories.UserRepository, jitsiService services.JitsiService) *BookingHandler { return &BookingHandler{ bookingService: bookingService, userRepo: userRepo, jitsiService: jitsiService, } } // BookingResponse represents a booking with personalized meeting link type BookingResponse struct { models.Booking PersonalizedMeetingURL string `json:"personalized_meeting_url,omitempty"` } // addPersonalizedLink adds a personalized meeting link to a booking func (h *BookingHandler) addPersonalizedLink(booking *models.Booking, user *models.User) BookingResponse { response := BookingResponse{ Booking: *booking, } // Only add personalized link if Jitsi URL exists if booking.JitsiRoomURL != "" && booking.JitsiRoomID != "" { displayName := fmt.Sprintf("%s %s", user.FirstName, user.LastName) if displayName == " " || displayName == "" { displayName = user.Email } // Generate JWT token for Jitsi authentication token, err := h.jitsiService.GenerateJitsiToken( booking.JitsiRoomID, displayName, user.Email, user.IsAdmin, ) var personalizedURL string if err == nil && token != "" { // Use JWT token if available personalizedURL = fmt.Sprintf("%s?jwt=%s", booking.JitsiRoomURL, token) } else { // Fallback to URL parameters personalizedURL = fmt.Sprintf("%s#userInfo.displayName=\"%s\"", booking.JitsiRoomURL, displayName) if user.IsAdmin { personalizedURL = fmt.Sprintf("%s&config.startWithAudioMuted=false&config.startWithVideoMuted=false", personalizedURL) } } response.PersonalizedMeetingURL = personalizedURL } return response } // GetAvailableSlots handles GET /api/schedules for available slots func (h *BookingHandler) GetAvailableSlots(c *gin.Context) { // Get date parameter from query string dateStr := c.Query("date") if dateStr == "" { c.JSON(http.StatusBadRequest, gin.H{ "error": "Date parameter is required (format: YYYY-MM-DD)", }) return } // Parse the date date, err := time.Parse("2006-01-02", dateStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid date format. Expected: YYYY-MM-DD", "details": err.Error(), }) return } // Get available slots slots, err := h.bookingService.GetAvailableSlots(date) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to get available slots", "details": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "date": dateStr, "slots": slots, }) } // CreateBooking handles POST /api/bookings for booking creation func (h *BookingHandler) CreateBooking(c *gin.Context) { // Get user ID from JWT token (set by auth middleware) userID, exists := middleware.GetUserIDFromContext(c) if !exists { c.JSON(http.StatusUnauthorized, gin.H{ "error": "User not authenticated", }) return } var req services.BookingRequest // Bind JSON request to struct if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid request format", "details": err.Error(), }) return } // Create the booking booking, err := h.bookingService.CreateBooking(userID, req) if err != nil { // Handle specific error cases if err.Error() == "schedule slot is not available" { c.JSON(http.StatusConflict, gin.H{ "error": "The selected time slot is no longer available", }) return } if err.Error() == "invalid schedule" { c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid schedule ID provided", }) return } c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to create booking", "details": err.Error(), }) return } c.JSON(http.StatusCreated, gin.H{ "message": "Booking created successfully", "booking": booking, }) } // GetUserBookings handles GET /api/bookings for user's booking history func (h *BookingHandler) GetUserBookings(c *gin.Context) { // Get user ID from JWT token (set by auth middleware) userID, exists := middleware.GetUserIDFromContext(c) if !exists { c.JSON(http.StatusUnauthorized, gin.H{ "error": "User not authenticated", }) return } // Get user details user, err := h.userRepo.GetByID(userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to get user information", "details": err.Error(), }) return } // Get user's bookings bookings, err := h.bookingService.GetUserBookings(userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to get user bookings", "details": err.Error(), }) return } // Add personalized meeting links to each booking bookingResponses := make([]BookingResponse, len(bookings)) for i, booking := range bookings { bookingResponses[i] = h.addPersonalizedLink(&booking, user) } c.JSON(http.StatusOK, gin.H{ "bookings": bookingResponses, }) } // CancelBooking handles PUT /api/bookings/:id/cancel for booking cancellation func (h *BookingHandler) CancelBooking(c *gin.Context) { // Get user ID from JWT token (set by auth middleware) userID, exists := middleware.GetUserIDFromContext(c) if !exists { c.JSON(http.StatusUnauthorized, gin.H{ "error": "User not authenticated", }) return } // Get booking ID from URL parameter bookingIDStr := c.Param("id") bookingID, err := strconv.ParseUint(bookingIDStr, 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid booking ID", }) return } // Cancel the booking if err := h.bookingService.CancelBooking(userID, uint(bookingID)); err != nil { // Handle specific error cases if err.Error() == "unauthorized: booking does not belong to user" { c.JSON(http.StatusForbidden, gin.H{ "error": "You can only cancel your own bookings", }) return } if err.Error() == "booking cannot be cancelled" { c.JSON(http.StatusBadRequest, gin.H{ "error": "This booking cannot be cancelled (must be at least 24 hours before scheduled time)", }) return } if err.Error() == "booking not found" { c.JSON(http.StatusNotFound, gin.H{ "error": "Booking not found", }) return } c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to cancel booking", "details": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "message": "Booking cancelled successfully", }) } // RescheduleBooking handles PUT /api/bookings/:id/reschedule for booking rescheduling func (h *BookingHandler) RescheduleBooking(c *gin.Context) { // Get user ID from JWT token (set by auth middleware) userID, exists := middleware.GetUserIDFromContext(c) if !exists { c.JSON(http.StatusUnauthorized, gin.H{ "error": "User not authenticated", }) return } // Get booking ID from URL parameter bookingIDStr := c.Param("id") bookingID, err := strconv.ParseUint(bookingIDStr, 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid booking ID", }) return } var req struct { NewScheduleID uint `json:"new_schedule_id" binding:"required"` } // Bind JSON request to struct if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid request format", "details": err.Error(), }) return } // Reschedule the booking if err := h.bookingService.RescheduleBooking(userID, uint(bookingID), req.NewScheduleID); err != nil { // Handle specific error cases if err.Error() == "unauthorized: booking does not belong to user" { c.JSON(http.StatusForbidden, gin.H{ "error": "You can only reschedule your own bookings", }) return } if err.Error() == "booking cannot be rescheduled" { c.JSON(http.StatusBadRequest, gin.H{ "error": "This booking cannot be rescheduled (must be at least 2 hours before scheduled time)", }) return } if err.Error() == "new schedule slot is not available" { c.JSON(http.StatusConflict, gin.H{ "error": "The new time slot is not available", }) return } if err.Error() == "booking not found" || err.Error() == "invalid new schedule" { c.JSON(http.StatusNotFound, gin.H{ "error": "Booking or new schedule not found", }) return } c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to reschedule booking", "details": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "message": "Booking rescheduled successfully", }) }