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
@@ -0,0 +1,578 @@
package controllers
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"fotbal-club/internal/models"
"fotbal-club/pkg/database"
)
// EquipmentController handles equipment management operations
type EquipmentController struct {
db *gorm.DB
}
// NewEquipmentController creates a new equipment controller
func NewEquipmentController() *EquipmentController {
return &EquipmentController{
db: database.GetDB(),
}
}
// EquipmentListRequest represents query parameters for equipment listing
type EquipmentListRequest struct {
FacilityID uint `form:"facility_id"`
Category string `form:"category"`
Status string `form:"status"`
Page int `form:"page,default=1"`
Limit int `form:"limit,default=20"`
Search string `form:"search"`
}
// GetEquipment handles GET /api/v1/admin/equipment
func (ec *EquipmentController) GetEquipment(c *gin.Context) {
var req EquipmentListRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
var equipment []models.FacilityEquipment
query := ec.db.Model(&models.FacilityEquipment{})
// Apply filters
if req.FacilityID > 0 {
query = query.Where("facility_id = ?", req.FacilityID)
}
if req.Category != "" {
query = query.Where("category = ?", req.Category)
}
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("Facility").Find(&equipment).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to fetch equipment"})
return
}
c.JSON(200, gin.H{
"equipment": equipment,
"total": total,
"page": req.Page,
"limit": req.Limit,
})
}
// CreateEquipment handles POST /api/v1/admin/equipment
func (ec *EquipmentController) CreateEquipment(c *gin.Context) {
var equipment models.FacilityEquipment
if err := c.ShouldBindJSON(&equipment); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Set default values
if equipment.Status == "" {
equipment.Status = models.EquipmentStatusAvailable
}
if equipment.Available == 0 {
equipment.Available = equipment.Quantity
}
if err := ec.db.Create(&equipment).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to create equipment"})
return
}
c.JSON(201, gin.H{"equipment": equipment})
}
// UpdateEquipment handles PUT /api/v1/admin/equipment/:id
func (ec *EquipmentController) UpdateEquipment(c *gin.Context) {
id := c.Param("id")
var equipment models.FacilityEquipment
if err := ec.db.First(&equipment, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(404, gin.H{"error": "Equipment not found"})
} else {
c.JSON(500, gin.H{"error": "Failed to fetch equipment"})
}
return
}
var updates models.FacilityEquipment
if err := c.ShouldBindJSON(&updates); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
if err := ec.db.Model(&equipment).Updates(updates).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to update equipment"})
return
}
// Fetch updated equipment
if err := ec.db.Preload("Facility").First(&equipment, id).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to fetch updated equipment"})
return
}
c.JSON(200, gin.H{"equipment": equipment})
}
// DeleteEquipment handles DELETE /api/v1/admin/equipment/:id
func (ec *EquipmentController) DeleteEquipment(c *gin.Context) {
id := c.Param("id")
if err := ec.db.Delete(&models.FacilityEquipment{}, id).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to delete equipment"})
return
}
c.JSON(200, gin.H{"message": "Equipment deleted successfully"})
}
// MaintenanceController handles maintenance scheduling operations
type MaintenanceController struct {
db *gorm.DB
}
// NewMaintenanceController creates a new maintenance controller
func NewMaintenanceController() *MaintenanceController {
return &MaintenanceController{
db: database.GetDB(),
}
}
// MaintenanceListRequest represents query parameters for maintenance listing
type MaintenanceListRequest struct {
FacilityID uint `form:"facility_id"`
Type string `form:"type"`
Status string `form:"status"`
StartDate string `form:"start_date"`
EndDate string `form:"end_date"`
Page int `form:"page,default=1"`
Limit int `form:"limit,default=20"`
}
// GetMaintenance handles GET /api/v1/admin/maintenance
func (mc *MaintenanceController) GetMaintenance(c *gin.Context) {
var req MaintenanceListRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
var maintenance []models.FacilityMaintenance
query := mc.db.Model(&models.FacilityMaintenance{})
// Apply filters
if req.FacilityID > 0 {
query = query.Where("facility_id = ?", req.FacilityID)
}
if req.Type != "" {
query = query.Where("type = ?", req.Type)
}
if req.Status != "" {
query = query.Where("status = ?", req.Status)
}
if req.StartDate != "" {
query = query.Where("scheduled_date >= ?", req.StartDate)
}
if req.EndDate != "" {
query = query.Where("scheduled_date <= ?", req.EndDate)
}
// 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("Facility").Order("scheduled_date ASC").Find(&maintenance).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to fetch maintenance records"})
return
}
c.JSON(200, gin.H{
"maintenance": maintenance,
"total": total,
"page": req.Page,
"limit": req.Limit,
})
}
// CreateMaintenance handles POST /api/v1/admin/maintenance
func (mc *MaintenanceController) CreateMaintenance(c *gin.Context) {
var maintenance models.FacilityMaintenance
if err := c.ShouldBindJSON(&maintenance); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Set default status
if maintenance.Status == "" {
maintenance.Status = "scheduled"
}
if err := mc.db.Create(&maintenance).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to create maintenance record"})
return
}
// If facility is unavailable during maintenance, update facility status
if maintenance.IsFacilityUnavailable && maintenance.ScheduledDate != nil {
mc.db.Model(&models.Facility{}).Where("id = ?", maintenance.FacilityID).
Update("status", models.FacilityStatusMaintenance)
}
c.JSON(201, gin.H{"maintenance": maintenance})
}
// UpdateMaintenance handles PUT /api/v1/admin/maintenance/:id
func (mc *MaintenanceController) UpdateMaintenance(c *gin.Context) {
id := c.Param("id")
var maintenance models.FacilityMaintenance
if err := mc.db.First(&maintenance, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(404, gin.H{"error": "Maintenance record not found"})
} else {
c.JSON(500, gin.H{"error": "Failed to fetch maintenance record"})
}
return
}
var updates models.FacilityMaintenance
if err := c.ShouldBindJSON(&updates); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Store old values for comparison
oldStatus := maintenance.Status
oldIsUnavailable := maintenance.IsFacilityUnavailable
if err := mc.db.Model(&maintenance).Updates(updates).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to update maintenance record"})
return
}
// Fetch updated maintenance
if err := mc.db.Preload("Facility").First(&maintenance, id).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to fetch updated maintenance record"})
return
}
// Update facility status if maintenance is completed
if oldStatus != "completed" && maintenance.Status == "completed" && oldIsUnavailable {
mc.db.Model(&models.Facility{}).Where("id = ?", maintenance.FacilityID).
Update("status", models.FacilityStatusActive)
}
c.JSON(200, gin.H{"maintenance": maintenance})
}
// WeatherController handles weather integration for outdoor facilities
type WeatherController struct {
db *gorm.DB
apiKey string // OpenWeatherMap API key
}
// NewWeatherController creates a new weather controller
func NewWeatherController(apiKey string) *WeatherController {
return &WeatherController{
db: database.GetDB(),
apiKey: apiKey,
}
}
// WeatherResponse represents weather data
type WeatherResponse struct {
DateTime time.Time `json:"date_time"`
Temperature float64 `json:"temperature"`
Humidity float64 `json:"humidity"`
Precipitation float64 `json:"precipitation"`
WindSpeed float64 `json:"wind_speed"`
WindDirection int `json:"wind_direction"`
WeatherCode string `json:"weather_code"`
Description string `json:"description"`
IsSuitable bool `json:"is_suitable"`
Recommendations string `json:"recommendations"`
}
// OpenWeatherMapResponse represents API response from OpenWeatherMap
type OpenWeatherMapResponse struct {
List []struct {
Dt int64 `json:"dt"`
Main struct {
Temp float64 `json:"temp"`
Humidity int `json:"humidity"`
} `json:"main"`
Weather []struct {
ID int `json:"id"`
Main string `json:"main"`
Description string `json:"description"`
} `json:"weather"`
Wind struct {
Speed float64 `json:"speed"`
Deg int `json:"deg"`
} `json:"wind"`
Rain struct {
OneHour float64 `json:"1h"`
} `json:"rain"`
Snow struct {
OneHour float64 `json:"1h"`
} `json:"snow"`
} `json:"list"`
}
// GetWeatherForecast handles GET /api/v1/facilities/:id/weather
func (wc *WeatherController) GetWeatherForecast(c *gin.Context) {
facilityID := c.Param("id")
// Get facility
var facility models.Facility
if err := wc.db.First(&facility, facilityID).Error; err != nil {
c.JSON(404, gin.H{"error": "Facility not found"})
return
}
// Only provide weather for outdoor facilities
if !facility.IsOutdoor {
c.JSON(400, gin.H{"error": "Weather data only available for outdoor facilities"})
return
}
// Try to get cached weather data first
var cachedWeather []models.WeatherCondition
twoHoursAgo := time.Now().Add(-2 * time.Hour)
if err := wc.db.Where("facility_id = ? AND created_at > ?", facilityID, twoHoursAgo).
Order("date_time ASC").Find(&cachedWeather).Error; err == nil && len(cachedWeather) > 0 {
var responses []WeatherResponse
for _, weather := range cachedWeather {
responses = append(responses, WeatherResponse{
DateTime: weather.DateTime,
Temperature: weather.Temperature,
Humidity: weather.Humidity,
Precipitation: weather.Precipitation,
WindSpeed: weather.WindSpeed,
WindDirection: weather.WindDirection,
WeatherCode: weather.WeatherCode,
Description: weather.Description,
IsSuitable: weather.IsSuitable,
Recommendations: weather.Recommendations,
})
}
c.JSON(200, gin.H{"weather": responses})
return
}
// Fetch fresh weather data if no recent cache
if wc.apiKey == "" {
c.JSON(503, gin.H{"error": "Weather service not configured"})
return
}
// For demo purposes, return mock data if API key is not set
// In production, you would call OpenWeatherMap API here
mockWeather := wc.generateMockWeather(facility)
// Cache the weather data
for _, weather := range mockWeather {
wc.db.Create(&models.WeatherCondition{
FacilityID: facility.ID,
DateTime: weather.DateTime,
Temperature: weather.Temperature,
Humidity: weather.Humidity,
Precipitation: weather.Precipitation,
WindSpeed: weather.WindSpeed,
WindDirection: weather.WindDirection,
WeatherCode: weather.WeatherCode,
Description: weather.Description,
IsSuitable: weather.IsSuitable,
Recommendations: weather.Recommendations,
})
}
c.JSON(200, gin.H{"weather": mockWeather})
}
// generateMockWeather creates sample weather data for demonstration
func (wc *WeatherController) generateMockWeather(facility models.Facility) []WeatherResponse {
var weather []WeatherResponse
now := time.Now()
// Generate 5-day forecast
for i := 0; i < 5; i++ {
date := now.AddDate(0, 0, i)
// Generate 3 time slots per day (morning, afternoon, evening)
for _, hour := range []int{9, 15, 18} {
dateTime := time.Date(date.Year(), date.Month(), date.Day(), hour, 0, 0, 0, date.Location())
// Mock weather conditions
temp := 15.0 + float64(i) + float64(hour/10)
humidity := 60.0 + float64(i*5)
windSpeed := 10.0 + float64(i)
precipitation := 0.0
isSuitable := true
recommendations := "Dobré podmínky pro trénink"
// Add some rain on random days
if i == 2 && hour == 15 {
precipitation = 5.0
isSuitable = false
recommendations = "Déšť - doporučeno přerušit trénink nebo přesunout dovnitř"
}
weather = append(weather, WeatherResponse{
DateTime: dateTime,
Temperature: temp,
Humidity: humidity,
Precipitation: precipitation,
WindSpeed: windSpeed,
WindDirection: 180 + i*10,
WeatherCode: "800",
Description: "Jasno",
IsSuitable: isSuitable,
Recommendations: recommendations,
})
}
}
return weather
}
// BookingCalendarController handles calendar view for bookings
type BookingCalendarController struct {
db *gorm.DB
}
// NewBookingCalendarController creates a new booking calendar controller
func NewBookingCalendarController() *BookingCalendarController {
return &BookingCalendarController{
db: database.GetDB(),
}
}
// CalendarEvent represents a calendar event
type CalendarEvent struct {
ID uint `json:"id"`
Title string `json:"title"`
Start time.Time `json:"start"`
End time.Time `json:"end"`
Type string `json:"type"` // "booking", "maintenance", "unavailable"
Status string `json:"status"`
FacilityName string `json:"facility_name"`
UserEmail string `json:"user_email,omitempty"`
Color string `json:"color"` // For calendar display
}
// GetCalendarEvents handles GET /api/v1/facilities/calendar
func (bcc *BookingCalendarController) GetCalendarEvents(c *gin.Context) {
// Parse date range
startDate := c.Query("start")
endDate := c.Query("end")
facilityID := c.Query("facility_id")
if startDate == "" || endDate == "" {
c.JSON(400, gin.H{"error": "start and end parameters are required"})
return
}
start, err := time.Parse(time.RFC3339, startDate)
if err != nil {
c.JSON(400, gin.H{"error": "Invalid start date format"})
return
}
end, err := time.Parse(time.RFC3339, endDate)
if err != nil {
c.JSON(400, gin.H{"error": "Invalid end date format"})
return
}
var events []CalendarEvent
// Get bookings
var bookings []models.FacilityBooking
bookingQuery := bcc.db.Preload("Facility").Preload("User").
Where("start_time >= ? AND end_time <= ? AND status NOT IN (?, ?)",
start, end, string(models.BookingStatusCancelled), string(models.BookingStatusNoShow))
if facilityID != "" {
bookingQuery = bookingQuery.Where("facility_id = ?", facilityID)
}
bookingQuery.Find(&bookings)
for _, booking := range bookings {
color := "#3B82F6" // Blue for confirmed bookings
if booking.Status == models.BookingStatusPending {
color = "#F59E0B" // Orange for pending
}
events = append(events, CalendarEvent{
ID: booking.ID,
Title: booking.Title,
Start: booking.StartTime,
End: booking.EndTime,
Type: "booking",
Status: string(booking.Status),
FacilityName: booking.Facility.Name,
UserEmail: booking.User.Email,
Color: color,
})
}
// Get maintenance
var maintenance []models.FacilityMaintenance
maintenanceQuery := bcc.db.Preload("Facility").
Where("scheduled_date >= ? AND scheduled_date <= ? AND is_facility_unavailable = ?", start, end, true)
if facilityID != "" {
maintenanceQuery = maintenanceQuery.Where("facility_id = ?", facilityID)
}
maintenanceQuery.Find(&maintenance)
for _, m := range maintenance {
endTime := m.ScheduledDate.Add(time.Duration(m.EstimatedDuration) * time.Minute)
events = append(events, CalendarEvent{
ID: m.ID,
Title: fmt.Sprintf("Údržba: %s", m.Title),
Start: *m.ScheduledDate,
End: endTime,
Type: "maintenance",
Status: m.Status,
FacilityName: m.Facility.Name,
Color: "#EF4444", // Red for maintenance
})
}
c.JSON(200, gin.H{"events": events})
}