2025-11-05 15:06:07 +00:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
|
|
import (
|
2025-11-05 16:35:36 +00:00
|
|
|
"net/http"
|
|
|
|
|
"strconv"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"attune-heart-therapy/internal/middleware"
|
|
|
|
|
"attune-heart-therapy/internal/services"
|
|
|
|
|
|
2025-11-05 15:06:07 +00:00
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type BookingHandler struct {
|
2025-11-05 16:35:36 +00:00
|
|
|
bookingService services.BookingService
|
2025-11-05 15:06:07 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-05 16:35:36 +00:00
|
|
|
func NewBookingHandler(bookingService services.BookingService) *BookingHandler {
|
|
|
|
|
return &BookingHandler{
|
|
|
|
|
bookingService: bookingService,
|
|
|
|
|
}
|
2025-11-05 15:06:07 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-05 16:35:36 +00:00
|
|
|
// GetAvailableSlots handles GET /api/schedules for available slots
|
2025-11-05 15:06:07 +00:00
|
|
|
func (h *BookingHandler) GetAvailableSlots(c *gin.Context) {
|
2025-11-05 16:35:36 +00:00
|
|
|
// 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,
|
|
|
|
|
})
|
2025-11-05 15:06:07 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-05 16:35:36 +00:00
|
|
|
// CreateBooking handles POST /api/bookings for booking creation
|
2025-11-05 15:06:07 +00:00
|
|
|
func (h *BookingHandler) CreateBooking(c *gin.Context) {
|
2025-11-05 16:35:36 +00:00
|
|
|
// 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,
|
|
|
|
|
})
|
2025-11-05 15:06:07 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-05 16:35:36 +00:00
|
|
|
// GetUserBookings handles GET /api/bookings for user's booking history
|
2025-11-05 15:06:07 +00:00
|
|
|
func (h *BookingHandler) GetUserBookings(c *gin.Context) {
|
2025-11-05 16:35:36 +00:00
|
|
|
// 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'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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
"bookings": bookings,
|
|
|
|
|
})
|
2025-11-05 15:06:07 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-05 16:35:36 +00:00
|
|
|
// CancelBooking handles PUT /api/bookings/:id/cancel for booking cancellation
|
2025-11-05 15:06:07 +00:00
|
|
|
func (h *BookingHandler) CancelBooking(c *gin.Context) {
|
2025-11-05 16:35:36 +00:00
|
|
|
// 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",
|
|
|
|
|
})
|
2025-11-05 15:06:07 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-05 16:35:36 +00:00
|
|
|
// RescheduleBooking handles PUT /api/bookings/:id/reschedule for booking rescheduling
|
2025-11-05 15:06:07 +00:00
|
|
|
func (h *BookingHandler) RescheduleBooking(c *gin.Context) {
|
2025-11-05 16:35:36 +00:00
|
|
|
// 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",
|
|
|
|
|
})
|
2025-11-05 15:06:07 +00:00
|
|
|
}
|