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 User 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 } // 🌟 Ambil Role User dari Token userRoleInterface, _ := c.Get("role") userRole, _ := userRoleInterface.(string) // 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 } // ========================================================= // 🌟 PERBAIKAN: PROTEKSI STATUS MAINTENANCE RUANGAN // ========================================================= var targetRoom models.Room if err := config.DB.First(&targetRoom, input.RoomID).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Ruangan tidak ditemukan di database"}) return } if targetRoom.Status == "Maintenance" { c.JSON(http.StatusForbidden, gin.H{"error": "Akses Ditolak: Ruangan ini sedang dalam perawatan teknis dan tidak dapat dipesan saat ini."}) return } // ========================================================= // Overlap Check (Mencegah bentrok jadwal) var count int64 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 } // Penentuan Status Otomatis statusPeminjaman := "Pending" // Default untuk student if userRole == "lecturer" { statusPeminjaman = "Approved" // Dosen otomatis disetujui! } // Simpan Booking booking := models.Booking{ UserID: userID, RoomID: input.RoomID, StartTime: input.StartTime, EndTime: input.EndTime, Purpose: input.Purpose, Status: statusPeminjaman, } 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, }) } // Fungsi khusus untuk mengambil riwayat pribadi func GetMyBookings(c *gin.Context) { var bookings []models.Booking userID, _ := c.Get("user_id") role, _ := c.Get("role") if role == "admin" { // Admin bisa melihat semua riwayat 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 { // Mahasiswa/Dosen HANYA BISA melihat riwayat miliknya sendiri 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 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 } 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 { fmt.Println("🔥 ERROR DARI DATABASE:", err.Error()) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "message": "Status booking berhasil diperbarui!", "data": booking, }) } // INPUT UNTUK ESP32 type TokenInput struct { Token string `json:"token" binding:"required"` // Berubah menjadi 'token' menyesuaikan payload ESP32 } // VERIFIKASI KODE DARI ESP32 (Booking & Jadwal Kuliah) func VerifyRedeemCode(c *gin.Context) { var input TokenInput if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Format data salah"}) return } loc, _ := time.LoadLocation("Asia/Jakarta") currentTime := time.Now().In(loc) var endTime time.Time var roomName string // 1. CARI DI TABEL BOOKING (REDEEM CODE) var booking models.Booking errBooking := config.DB.Preload("Room").First(&booking, "redeem_code = ? AND status = 'Approved'", input.Token).Error if errBooking == nil { // LOGIKA BOOKING (Sudah menggunakan time.Time) if currentTime.Before(booking.StartTime) { c.JSON(http.StatusForbidden, gin.H{"error": "Belum waktunya peminjaman!"}) return } if currentTime.After(booking.EndTime) { c.JSON(http.StatusForbidden, gin.H{"error": "Waktu peminjaman sudah habis!"}) return } endTime = booking.EndTime roomName = booking.Room.Name } else { // 2. JIKA TIDAK KETEMU, CARI DI TABEL JADWAL KELAS (KODE MK) var schedule models.ClassSchedule errSchedule := config.DB.Preload("Room").First(&schedule, "kode_mk = ?", input.Token).Error if errSchedule != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "Token atau Kode MK tidak valid!"}) return } // LOGIKA JADWAL KULIAH (Konversi String Jam ke time.Time hari ini) todayStr := currentTime.Format("2006-01-02") // Gabungkan tanggal hari ini dengan jam dari database (JamMulai & JamSelesai) startTime, _ := time.Parse("2006-01-02 15:04:05", todayStr+" "+schedule.JamMulai) endTimeParsed, _ := time.Parse("2006-01-02 15:04:05", todayStr+" "+schedule.JamSelesai) if currentTime.Before(startTime) { c.JSON(http.StatusForbidden, gin.H{"error": "Belum waktunya jadwal kuliah!"}) return } if currentTime.After(endTimeParsed) { c.JSON(http.StatusForbidden, gin.H{"error": "Waktu perkuliahan sudah habis!"}) return } endTime = endTimeParsed roomName = schedule.Room.Name } // 3. HITUNG SISA WAKTU sisaWaktuMenit := int(endTime.Sub(currentTime).Minutes()) if sisaWaktuMenit < 1 { sisaWaktuMenit = 1 } c.JSON(http.StatusOK, gin.H{ "status": "success", "message": "Token valid!", "room": roomName, "duration_minutes": sisaWaktuMenit, }) } // GET ALL BOOKINGS (Untuk Calendar View semua user) func GetAllBookings(c *gin.Context) { var bookings []models.Booking 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 } c.JSON(http.StatusOK, gin.H{ "status": "success", "data": bookings, }) }