package controllers import ( "fmt" "net/http" "s-class-backend/config" "s-class-backend/helpers" "s-class-backend/models" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" ) type BookingInput struct { RoomID uint `json:"room_id" binding:"required"` StartTime time.Time `json:"start_time" binding:"required"` EndTime time.Time `json:"end_time" binding:"required"` Purpose string `json:"purpose" binding:"required"` } func CreateBooking(c *gin.Context) { var input BookingInput if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } //Handle UUID userIDInterface, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "User ID tidak ditemukan"}) return } userIDStr, ok := userIDInterface.(string) if !ok { c.JSON(http.StatusInternalServerError, gin.H{"error": "Format User ID token salah"}) return } userID, err := uuid.Parse(userIDStr) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Gagal memproses User ID"}) return } // Validasi Waktu if input.EndTime.Before(input.StartTime) { c.JSON(http.StatusBadRequest, gin.H{"error": "Waktu selesai tidak boleh lebih awal dari waktu mulai!"}) return } // (Overlap Check) var count int64 // KITA UBAH STATUSNYA MENJADI: status IN ('Pending', 'Approved') config.DB.Model(&models.Booking{}).Where( "room_id = ? AND status IN ('Pending', 'Approved') AND ((start_time < ? AND end_time > ?) OR (start_time < ? AND end_time > ?) OR (start_time >= ? AND end_time <= ?))", input.RoomID, input.EndTime, input.StartTime, input.EndTime, input.StartTime, input.StartTime, input.EndTime, ).Count(&count) if count > 0 { c.JSON(http.StatusConflict, gin.H{"error": "Ruangan sudah dibooking atau sedang dalam antrean persetujuan pada jam tersebut!"}) return } // Simpan Booking booking := models.Booking{ UserID: userID, RoomID: input.RoomID, StartTime: input.StartTime, EndTime: input.EndTime, Purpose: input.Purpose, Status: "Pending", } if err := config.DB.Create(&booking).Error; err != nil { fmt.Println("Error DB:", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "message": "Booking Berhasil diajukan!", "data": booking, }) } func GetUserBookings(c *gin.Context) { var bookings []models.Booking userID, _ := c.Get("user_id") role, _ := c.Get("role") if role == "admin" { if err := config.DB.Preload("Room").Preload("User").Order("created_at desc").Find(&bookings).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } } else { if err := config.DB.Preload("Room").Where("user_id = ?", userID).Order("created_at desc").Find(&bookings).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } } c.JSON(http.StatusOK, gin.H{"data": bookings}) } type UpdateStatusInput struct { Status string `json:"status" binding:"required"` } // UPDATE STATUS (ADMIN) func UpdateBookingStatus(c *gin.Context) { bookingID := c.Param("id") var input UpdateStatusInput if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Format data salah"}) return } var booking models.Booking // KUNCI PERBAIKAN: Gunakan Where("booking_id = ?") agar tepat sasaran if err := config.DB.Where("booking_id = ?", bookingID).First(&booking).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Data booking tidak ditemukan di database"}) return } // Gunakan Switch Case (Clean Code) yang sudah kita bahas sebelumnya switch input.Status { case "Approved": if booking.RedeemCode == "" { booking.RedeemCode = helpers.GenerateRedeemCode() } case "Rejected", "Cancelled": booking.RedeemCode = "" } booking.Status = input.Status if err := config.DB.Save(&booking).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Gagal menyimpan ke database"}) return } c.JSON(http.StatusOK, gin.H{ "message": "Status booking berhasil diperbarui!", "data": booking, }) booking.Status = input.Status // KITA BUKA KEDOK ERROR DARI POSTGRESQL: if err := config.DB.Save(&booking).Error; err != nil { fmt.Println("🔥 ERROR DARI DATABASE:", err.Error()) // Cetak teks merah di terminal c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) // Tembak ke pop-up web return } c.JSON(http.StatusOK, gin.H{ "message": "Status booking berhasil diperbarui!", "data": booking, }) } // INPUT UNTUK ESP3 type RedeemInput struct { RedeemCode string `json:"redeem_code" binding:"required"` } // VERIFIKASI KODE DARI ESP32 func VerifyRedeemCode(c *gin.Context) { var input RedeemInput if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Format data salah"}) return } var booking models.Booking if err := config.DB.Preload("Room").First(&booking, "redeem_code = ? AND status = 'Approved'", input.RedeemCode).Error; err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Kode tidak valid, belum disetujui, atau sudah digunakan!"}) return } booking.Status = "Completed" booking.RedeemCode = "" config.DB.Save(&booking) c.JSON(http.StatusOK, gin.H{ "message": "Kode valid!", "room": booking.Room.Name, "status": "success", }) } // GET ALL BOOKINGS (Untuk Calendar View semua user) func GetAllBookings(c *gin.Context) { var bookings []models.Booking // Preload digunakan untuk mengambil data Relasi (Nama Ruangan & Nama Peminjam) if err := config.DB.Preload("Room").Preload("User").Order("created_at desc").Find(&bookings).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Perhatikan: responsenya menggunakan "data: bookings" agar cocok dengan Frontend React-mu c.JSON(http.StatusOK, gin.H{ "status": "success", "data": bookings, }) }