This commit is contained in:
Tomas Dvorak
2026-01-26 08:13:18 +01:00
parent aa036b6550
commit dfc079288f
505 changed files with 95755 additions and 5712 deletions
+578
View File
@@ -0,0 +1,578 @@
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
}