package controllers import ( "fmt" "strconv" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" "fotbal-club/internal/models" "fotbal-club/pkg/database" ) // FacilityController handles facility management operations type FacilityController struct { db *gorm.DB } // NewFacilityController creates a new facility controller func NewFacilityController() *FacilityController { return &FacilityController{ db: database.GetDB(), } } // FacilityListRequest represents query parameters for facility listing type FacilityListRequest struct { Type string `form:"type"` Status string `form:"status"` Page int `form:"page,default=1"` Limit int `form:"limit,default=20"` Search string `form:"search"` } // FacilityResponse represents a facility response type FacilityResponse struct { ID uint `json:"id"` Name string `json:"name"` Type string `json:"type"` Status string `json:"status"` Capacity int `json:"capacity"` Area float64 `json:"area"` Location string `json:"location"` IsIndoor bool `json:"is_indoor"` IsOutdoor bool `json:"is_outdoor"` ImageURL string `json:"image_url"` // Booking settings RequiresApproval bool `json:"requires_approval"` MinBookingDuration int `json:"min_booking_duration"` MaxBookingDuration int `json:"max_booking_duration"` BookingAdvanceDays int `json:"booking_advance_days"` PricePerHour float64 `json:"price_per_hour"` // Availability AvailabilityRules []models.FacilityAvailabilityRule `json:"availability_rules,omitempty"` // Counts BookingsCount int `json:"bookings_count,omitempty"` EquipmentCount int `json:"equipment_count,omitempty"` MaintenanceCount int `json:"maintenance_count,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // BookingRequest represents a booking request type BookingRequest struct { FacilityID uint `json:"facility_id" binding:"required"` Title string `json:"title" binding:"required"` Description string `json:"description"` StartTime string `json:"start_time" binding:"required"` // ISO 8601 format EndTime string `json:"end_time" binding:"required"` // ISO 8601 format AttendeesCount int `json:"attendees_count"` } // BookingResponse represents a booking response type BookingResponse struct { ID uint `json:"id"` FacilityID uint `json:"facility_id"` Facility FacilityResponse `json:"facility"` UserID uint `json:"user_id"` User models.User `json:"user"` Title string `json:"title"` Description string `json:"description"` StartTime time.Time `json:"start_time"` EndTime time.Time `json:"end_time"` Status string `json:"status"` TotalPrice float64 `json:"total_price"` PaymentStatus string `json:"payment_status"` AttendeesCount int `json:"attendees_count"` PublicNotes string `json:"public_notes"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // GetFacilities handles GET /api/v1/admin/facilities func (fc *FacilityController) GetFacilities(c *gin.Context) { var req FacilityListRequest if err := c.ShouldBindQuery(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } var facilities []models.Facility query := fc.db.Model(&models.Facility{}) // Apply filters if req.Type != "" { query = query.Where("type = ?", req.Type) } if req.Status != "" { query = query.Where("status = ?", req.Status) } if req.Search != "" { query = query.Where("name ILIKE ? OR description ILIKE ?", "%"+req.Search+"%", "%"+req.Search+"%") } // Count total var total int64 query.Count(&total) // Apply pagination offset := (req.Page - 1) * req.Limit query = query.Offset(offset).Limit(req.Limit) if err := query.Preload("AvailabilityRules").Find(&facilities).Error; err != nil { c.JSON(500, gin.H{"error": "Failed to fetch facilities"}) return } // Transform to response format var responses []FacilityResponse for _, facility := range facilities { // Count related records var bookingsCount, equipmentCount, maintenanceCount int64 fc.db.Model(&models.FacilityBooking{}).Where("facility_id = ?", facility.ID).Count(&bookingsCount) fc.db.Model(&models.FacilityEquipment{}).Where("facility_id = ?", facility.ID).Count(&equipmentCount) fc.db.Model(&models.FacilityMaintenance{}).Where("facility_id = ?", facility.ID).Count(&maintenanceCount) responses = append(responses, FacilityResponse{ ID: facility.ID, Name: facility.Name, Type: string(facility.Type), Status: string(facility.Status), Capacity: facility.Capacity, Area: facility.Area, Location: facility.Location, IsIndoor: facility.IsIndoor, IsOutdoor: facility.IsOutdoor, ImageURL: facility.ImageURL, RequiresApproval: facility.RequiresApproval, MinBookingDuration: facility.MinBookingDuration, MaxBookingDuration: facility.MaxBookingDuration, BookingAdvanceDays: facility.BookingAdvanceDays, PricePerHour: facility.PricePerHour, AvailabilityRules: facility.AvailabilityRules, BookingsCount: int(bookingsCount), EquipmentCount: int(equipmentCount), MaintenanceCount: int(maintenanceCount), CreatedAt: facility.CreatedAt, UpdatedAt: facility.UpdatedAt, }) } c.JSON(200, gin.H{ "facilities": responses, "total": total, "page": req.Page, "limit": req.Limit, }) } // GetFacility handles GET /api/v1/admin/facilities/:id func (fc *FacilityController) GetFacility(c *gin.Context) { id := c.Param("id") var facility models.Facility if err := fc.db.Preload("AvailabilityRules"). Preload("Bookings"). Preload("Equipment"). Preload("Maintenance"). First(&facility, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(404, gin.H{"error": "Facility not found"}) } else { c.JSON(500, gin.H{"error": "Failed to fetch facility"}) } return } c.JSON(200, gin.H{"facility": facility}) } // CreateFacility handles POST /api/v1/admin/facilities func (fc *FacilityController) CreateFacility(c *gin.Context) { var facility models.Facility if err := c.ShouldBindJSON(&facility); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } // Set default values if facility.Status == "" { facility.Status = models.FacilityStatusActive } if facility.MinBookingDuration == 0 { facility.MinBookingDuration = 30 } if facility.MaxBookingDuration == 0 { facility.MaxBookingDuration = 240 } if facility.BookingAdvanceDays == 0 { facility.BookingAdvanceDays = 30 } if err := fc.db.Create(&facility).Error; err != nil { c.JSON(500, gin.H{"error": "Failed to create facility"}) return } c.JSON(201, gin.H{"facility": facility}) } // UpdateFacility handles PUT /api/v1/admin/facilities/:id func (fc *FacilityController) UpdateFacility(c *gin.Context) { id := c.Param("id") var facility models.Facility if err := fc.db.First(&facility, id).Error; err != nil { if err == gorm.ErrRecordNotFound { c.JSON(404, gin.H{"error": "Facility not found"}) } else { c.JSON(500, gin.H{"error": "Failed to fetch facility"}) } return } var updates models.Facility if err := c.ShouldBindJSON(&updates); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } // Update fields if err := fc.db.Model(&facility).Updates(updates).Error; err != nil { c.JSON(500, gin.H{"error": "Failed to update facility"}) return } // Fetch updated facility if err := fc.db.Preload("AvailabilityRules").First(&facility, id).Error; err != nil { c.JSON(500, gin.H{"error": "Failed to fetch updated facility"}) return } c.JSON(200, gin.H{"facility": facility}) } // DeleteFacility handles DELETE /api/v1/admin/facilities/:id func (fc *FacilityController) DeleteFacility(c *gin.Context) { id := c.Param("id") if err := fc.db.Delete(&models.Facility{}, id).Error; err != nil { c.JSON(500, gin.H{"error": "Failed to delete facility"}) return } c.JSON(200, gin.H{"message": "Facility deleted successfully"}) } // GetFacilityBookings handles GET /api/v1/admin/facilities/:id/bookings func (fc *FacilityController) GetFacilityBookings(c *gin.Context) { facilityID := c.Param("id") // Parse query parameters page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) status := c.Query("status") startDate := c.Query("start_date") endDate := c.Query("end_date") var bookings []models.FacilityBooking query := fc.db.Model(&models.FacilityBooking{}).Where("facility_id = ?", facilityID) // Apply filters if status != "" { query = query.Where("status = ?", status) } if startDate != "" { query = query.Where("start_time >= ?", startDate) } if endDate != "" { query = query.Where("end_time <= ?", endDate) } // Count total var total int64 query.Count(&total) // Apply pagination offset := (page - 1) * limit query = query.Offset(offset).Limit(limit) if err := query.Preload("User").Preload("Facility").Order("start_time DESC").Find(&bookings).Error; err != nil { c.JSON(500, gin.H{"error": "Failed to fetch bookings"}) return } c.JSON(200, gin.H{ "bookings": bookings, "total": total, "page": page, "limit": limit, }) } // CreateBooking handles POST /api/v1/facilities/bookings func (fc *FacilityController) CreateBooking(c *gin.Context) { var req BookingRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } // Get user from context (assuming JWT middleware) userID, exists := c.Get("user_id") if !exists { c.JSON(401, gin.H{"error": "User not authenticated"}) return } // Parse times startTime, err := time.Parse(time.RFC3339, req.StartTime) if err != nil { c.JSON(400, gin.H{"error": "Invalid start_time format"}) return } endTime, err := time.Parse(time.RFC3339, req.EndTime) if err != nil { c.JSON(400, gin.H{"error": "Invalid end_time format"}) return } // Validate time range if endTime.Before(startTime) || endTime.Equal(startTime) { c.JSON(400, gin.H{"error": "End time must be after start time"}) return } // Get facility var facility models.Facility if err := fc.db.First(&facility, req.FacilityID).Error; err != nil { c.JSON(404, gin.H{"error": "Facility not found"}) return } // Check if facility is available if facility.Status != models.FacilityStatusActive { c.JSON(400, gin.H{"error": "Facility is not available for booking"}) return } // Check booking duration limits duration := endTime.Sub(startTime).Minutes() if int(duration) < facility.MinBookingDuration { c.JSON(400, gin.H{"error": fmt.Sprintf("Booking duration must be at least %d minutes", facility.MinBookingDuration)}) return } if int(duration) > facility.MaxBookingDuration { c.JSON(400, gin.H{"error": fmt.Sprintf("Booking duration cannot exceed %d minutes", facility.MaxBookingDuration)}) return } // Check advance booking limit if facility.BookingAdvanceDays > 0 { maxDate := time.Now().AddDate(0, 0, facility.BookingAdvanceDays) if startTime.After(maxDate) { c.JSON(400, gin.H{"error": fmt.Sprintf("Bookings cannot be made more than %d days in advance", facility.BookingAdvanceDays)}) return } } // Check for overlapping bookings var overlappingBooking models.FacilityBooking if err := fc.db.Where("facility_id = ? AND start_time < ? AND end_time > ? AND status NOT IN (?, ?)", req.FacilityID, endTime, startTime, string(models.BookingStatusCancelled), string(models.BookingStatusNoShow)). First(&overlappingBooking).Error; err == nil { c.JSON(409, gin.H{"error": "Time slot is already booked"}) return } // Calculate price totalPrice := facility.PricePerHour * (duration / 60) // Create booking booking := models.FacilityBooking{ FacilityID: req.FacilityID, UserID: userID.(uint), Title: req.Title, Description: req.Description, StartTime: startTime, EndTime: endTime, Status: models.BookingStatusPending, TotalPrice: totalPrice, AttendeesCount: req.AttendeesCount, } if facility.RequiresApproval { booking.Status = models.BookingStatusPending } else { booking.Status = models.BookingStatusConfirmed } if err := fc.db.Create(&booking).Error; err != nil { c.JSON(500, gin.H{"error": "Failed to create booking"}) return } // Load relationships for response fc.db.Preload("User").Preload("Facility").First(&booking, booking.ID) c.JSON(201, gin.H{"booking": booking}) } // GetPublicFacilities handles GET /api/v1/facilities func (fc *FacilityController) GetPublicFacilities(c *gin.Context) { var facilities []models.Facility query := fc.db.Model(&models.Facility{}).Where("status = ?", models.FacilityStatusActive) // Optional filters facilityType := c.Query("type") if facilityType != "" { query = query.Where("type = ?", facilityType) } if err := query.Find(&facilities).Error; err != nil { c.JSON(500, gin.H{"error": "Failed to fetch facilities"}) return } // Transform to public response format (limited fields) var responses []FacilityResponse for _, facility := range facilities { responses = append(responses, FacilityResponse{ ID: facility.ID, Name: facility.Name, Type: string(facility.Type), Status: string(facility.Status), Capacity: facility.Capacity, Area: facility.Area, Location: facility.Location, IsIndoor: facility.IsIndoor, IsOutdoor: facility.IsOutdoor, ImageURL: facility.ImageURL, RequiresApproval: facility.RequiresApproval, MinBookingDuration: facility.MinBookingDuration, MaxBookingDuration: facility.MaxBookingDuration, BookingAdvanceDays: facility.BookingAdvanceDays, PricePerHour: facility.PricePerHour, CreatedAt: facility.CreatedAt, UpdatedAt: facility.UpdatedAt, }) } c.JSON(200, gin.H{"facilities": responses}) } // GetFacilityAvailability handles GET /api/v1/facilities/:id/availability func (fc *FacilityController) GetFacilityAvailability(c *gin.Context) { facilityID := c.Param("id") // Parse date range startDate := c.Query("start_date") endDate := c.Query("end_date") if startDate == "" || endDate == "" { c.JSON(400, gin.H{"error": "start_date and end_date are required"}) return } start, err := time.Parse("2006-01-02", startDate) if err != nil { c.JSON(400, gin.H{"error": "Invalid start_date format, use YYYY-MM-DD"}) return } end, err := time.Parse("2006-01-02", endDate) if err != nil { c.JSON(400, gin.H{"error": "Invalid end_date format, use YYYY-MM-DD"}) return } // Get facility var facility models.Facility if err := fc.db.Preload("AvailabilityRules").First(&facility, facilityID).Error; err != nil { c.JSON(404, gin.H{"error": "Facility not found"}) return } // Get existing bookings var bookings []models.FacilityBooking if err := fc.db.Where("facility_id = ? AND start_time >= ? AND end_time <= ? AND status NOT IN (?, ?)", facilityID, start, end.AddDate(0, 0, 1), string(models.BookingStatusCancelled), string(models.BookingStatusNoShow)). Find(&bookings).Error; err != nil { c.JSON(500, gin.H{"error": "Failed to fetch bookings"}) return } // Generate availability slots availability := fc.generateAvailabilitySlots(facility, bookings, start, end) c.JSON(200, gin.H{ "facility": facility, "availability": availability, }) } // generateAvailabilitySlots creates available time slots for a facility func (fc *FacilityController) generateAvailabilitySlots(facility models.Facility, bookings []models.FacilityBooking, start, end time.Time) map[string][]map[string]interface{} { availability := make(map[string][]map[string]interface{}) // Initialize each day with empty slots for d := start; d.Before(end.AddDate(0, 0, 1)); d = d.AddDate(0, 0, 1) { dateStr := d.Format("2006-01-02") availability[dateStr] = []map[string]interface{}{} } // For each day, generate available slots based on rules and existing bookings for d := start; d.Before(end.AddDate(0, 0, 1)); d = d.AddDate(0, 0, 1) { dateStr := d.Format("2006-01-02") dayOfWeek := int(d.Weekday()) // Find availability rules for this day var dayRules []models.FacilityAvailabilityRule for _, rule := range facility.AvailabilityRules { if rule.DayOfWeek == dayOfWeek && rule.IsAvailable { // Check if rule is within date range if (rule.StartDate == nil || d.After(*rule.StartDate) || d.Equal(*rule.StartDate)) && (rule.EndDate == nil || d.Before(*rule.EndDate) || d.Equal(*rule.EndDate)) { dayRules = append(dayRules, rule) } } } // Generate slots for each rule for _, rule := range dayRules { ruleStart, _ := time.Parse("15:04", rule.StartTime) ruleEnd, _ := time.Parse("15:04", rule.EndTime) // Convert to full datetime slotStart := time.Date(d.Year(), d.Month(), d.Day(), ruleStart.Hour(), ruleStart.Minute(), 0, 0, d.Location()) slotEnd := time.Date(d.Year(), d.Month(), d.Day(), ruleEnd.Hour(), ruleEnd.Minute(), 0, 0, d.Location()) // Check for overlapping bookings isAvailable := true for _, booking := range bookings { if booking.StartTime.Before(slotEnd) && booking.EndTime.After(slotStart) { isAvailable = false break } } if isAvailable { availability[dateStr] = append(availability[dateStr], map[string]interface{}{ "start": slotStart.Format("15:04"), "end": slotEnd.Format("15:04"), "available": true, }) } } } return availability }