Compare commits
13
Commits
292580e089
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
387b098aad | ||
|
|
50795d806e | ||
|
|
122115b5e9 | ||
|
|
e210d2a424 | ||
|
|
29ea2c6589 | ||
|
|
86682ce7c3 | ||
|
|
427ce3d5b3 | ||
|
|
9141b48056 | ||
|
|
4d6e825f16 | ||
|
|
102ac2c50a | ||
|
|
241d228e9c | ||
|
|
3c0adda955 | ||
|
|
7497b37ff2 |
+13
-14
@@ -14,12 +14,12 @@ func main() {
|
|||||||
// 1. Konek Database
|
// 1. Konek Database
|
||||||
config.ConnectDatabase()
|
config.ConnectDatabase()
|
||||||
|
|
||||||
// 2. AutoMigrate (Tambahkan ClassSchedule di sini agar dikenali GORM)
|
// 2. AutoMigrate
|
||||||
config.DB.AutoMigrate(&models.User{}, &models.Room{}, &models.Booking{}, &models.ClassSchedule{})
|
config.DB.AutoMigrate(&models.User{}, &models.Room{}, &models.Booking{}, &models.ClassSchedule{})
|
||||||
|
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
// 3. CORS Middleware (Agar Frontend bisa masuk)
|
// 3. CORS Middleware (Panggil fungsinya di sini)
|
||||||
r.Use(CORSMiddleware())
|
r.Use(CORSMiddleware())
|
||||||
|
|
||||||
r.GET("/", func(c *gin.Context) {
|
r.GET("/", func(c *gin.Context) {
|
||||||
@@ -58,35 +58,34 @@ func main() {
|
|||||||
// Admin (Manage Rooms)
|
// Admin (Manage Rooms)
|
||||||
protected.PUT("/admin/rooms/:id/status", controllers.UpdateRoomStatus)
|
protected.PUT("/admin/rooms/:id/status", controllers.UpdateRoomStatus)
|
||||||
|
|
||||||
// 🌟 RUTE BARU: Jadwal Kuliah (Untuk Halaman Web Admin)
|
// Jadwal Kuliah (Untuk Halaman Web Admin)
|
||||||
protected.GET("/schedules", controllers.GetSchedules)
|
protected.GET("/schedules", controllers.GetSchedules)
|
||||||
|
protected.POST("/schedules", controllers.CreateSchedule)
|
||||||
|
protected.PUT("/schedules/:id", controllers.UpdateSchedule)
|
||||||
|
protected.DELETE("/schedules/:id", controllers.DeleteSchedule)
|
||||||
}
|
}
|
||||||
// 5. Jalur IoT ESP32
|
|
||||||
|
// 5. Jalur IoT ESP32 & Kontrol Daya
|
||||||
r.POST("/api/sensor/energy", controllers.UpdateRoomPower)
|
r.POST("/api/sensor/energy", controllers.UpdateRoomPower)
|
||||||
|
|
||||||
// RUTE BARU: Untuk menerima ketukan pintu dari Hardware ESP32
|
|
||||||
r.POST("/api/hardware/verify", controllers.VerifyHardwareCode)
|
r.POST("/api/hardware/verify", controllers.VerifyHardwareCode)
|
||||||
|
|
||||||
// PASTIKAN BARIS INI ADA DI SINI:
|
|
||||||
r.POST("/api/hardware/control", controllers.ControlHardware)
|
r.POST("/api/hardware/control", controllers.ControlHardware)
|
||||||
|
|
||||||
r.GET("/api/hardware/status", controllers.GetHardwareStatus)
|
r.GET("/api/hardware/status", controllers.GetHardwareStatus)
|
||||||
|
|
||||||
r.GET("/api/hardware/power-status", controllers.GetPowerStatus)
|
r.GET("/api/hardware/power-status", controllers.GetPowerStatus)
|
||||||
|
|
||||||
r.POST("/api/power/global", controllers.GlobalPowerControl)
|
r.POST("/api/power/global", controllers.GlobalPowerControl)
|
||||||
|
|
||||||
r.Run(":8080")
|
r.Run(":8080")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... (fungsi CORSMiddleware tetap sama) ...
|
// =========================================================================
|
||||||
|
// FUNGSI MIDDLEWARE HARUS DITULIS DI LUAR FUNGSI MAIN
|
||||||
|
// =========================================================================
|
||||||
func CORSMiddleware() gin.HandlerFunc {
|
func CORSMiddleware() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, PATCH")
|
// Pastikan DELETE sudah masuk di sini
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, PATCH, DELETE")
|
||||||
|
|
||||||
if c.Request.Method == "OPTIONS" {
|
if c.Request.Method == "OPTIONS" {
|
||||||
c.AbortWithStatus(204)
|
c.AbortWithStatus(204)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func CreateBooking(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🌟 PERBAIKAN 1: Ambil Role User dari Token
|
// 🌟 Ambil Role User dari Token
|
||||||
userRoleInterface, _ := c.Get("role")
|
userRoleInterface, _ := c.Get("role")
|
||||||
userRole, _ := userRoleInterface.(string)
|
userRole, _ := userRoleInterface.(string)
|
||||||
|
|
||||||
@@ -55,6 +55,21 @@ func CreateBooking(c *gin.Context) {
|
|||||||
return
|
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)
|
// Overlap Check (Mencegah bentrok jadwal)
|
||||||
var count int64
|
var count int64
|
||||||
config.DB.Model(&models.Booking{}).Where(
|
config.DB.Model(&models.Booking{}).Where(
|
||||||
@@ -67,7 +82,7 @@ func CreateBooking(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🌟 PERBAIKAN 1 Lanjutan: Penentuan Status Otomatis
|
// Penentuan Status Otomatis
|
||||||
statusPeminjaman := "Pending" // Default untuk student
|
statusPeminjaman := "Pending" // Default untuk student
|
||||||
if userRole == "lecturer" {
|
if userRole == "lecturer" {
|
||||||
statusPeminjaman = "Approved" // Dosen otomatis disetujui!
|
statusPeminjaman = "Approved" // Dosen otomatis disetujui!
|
||||||
@@ -80,7 +95,7 @@ func CreateBooking(c *gin.Context) {
|
|||||||
StartTime: input.StartTime,
|
StartTime: input.StartTime,
|
||||||
EndTime: input.EndTime,
|
EndTime: input.EndTime,
|
||||||
Purpose: input.Purpose,
|
Purpose: input.Purpose,
|
||||||
Status: statusPeminjaman, // <-- Gunakan variabel statusPeminjaman
|
Status: statusPeminjaman,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := config.DB.Create(&booking).Error; err != nil {
|
if err := config.DB.Create(&booking).Error; err != nil {
|
||||||
@@ -95,7 +110,7 @@ func CreateBooking(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🌟 PERBAIKAN 3: Fungsi khusus untuk mengambil riwayat pribadi
|
// Fungsi khusus untuk mengambil riwayat pribadi
|
||||||
func GetMyBookings(c *gin.Context) {
|
func GetMyBookings(c *gin.Context) {
|
||||||
var bookings []models.Booking
|
var bookings []models.Booking
|
||||||
|
|
||||||
@@ -150,7 +165,6 @@ func UpdateBookingStatus(c *gin.Context) {
|
|||||||
|
|
||||||
booking.Status = input.Status
|
booking.Status = input.Status
|
||||||
|
|
||||||
// 🌟 PERBAIKAN 2: Menghapus blok Save dan JSON ganda yang bikin error
|
|
||||||
if err := config.DB.Save(&booking).Error; err != nil {
|
if err := config.DB.Save(&booking).Error; err != nil {
|
||||||
fmt.Println("🔥 ERROR DARI DATABASE:", err.Error())
|
fmt.Println("🔥 ERROR DARI DATABASE:", err.Error())
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
@@ -255,4 +269,4 @@ func GetAllBookings(c *gin.Context) {
|
|||||||
"status": "success",
|
"status": "success",
|
||||||
"data": bookings,
|
"data": bookings,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -7,9 +7,11 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv" // Digunakan untuk mengubah teks Watt menjadi angka
|
"strconv" // Digunakan untuk mengubah teks Watt menjadi angka
|
||||||
|
"strings" // Digunakan untuk manipulasi string (ToUpper/Trim)
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"s-class-backend/config" // Import database
|
"s-class-backend/config" // Import database
|
||||||
|
"s-class-backend/models" // Import models untuk query database
|
||||||
|
|
||||||
mqtt "github.com/eclipse/paho.mqtt.golang"
|
mqtt "github.com/eclipse/paho.mqtt.golang"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -25,7 +27,6 @@ type VerifyRequest struct {
|
|||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tambahan untuk kontrol daya global (ON/OFF)
|
|
||||||
type GlobalPowerRequest struct {
|
type GlobalPowerRequest struct {
|
||||||
Action string `json:"action"` // "on" atau "off"
|
Action string `json:"action"` // "on" atau "off"
|
||||||
}
|
}
|
||||||
@@ -41,7 +42,7 @@ var DeviceStatusCache = map[string]string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// FUNGSI 1: VERIFIKASI TOKEN (Bisa Master & Mahasiswa)
|
// FUNGSI 1: VERIFIKASI TOKEN & KODE MATA KULIAH (DOUBLE-LAYER PROTECTION)
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
func VerifyHardwareCode(c *gin.Context) {
|
func VerifyHardwareCode(c *gin.Context) {
|
||||||
var req VerifyRequest
|
var req VerifyRequest
|
||||||
@@ -51,8 +52,9 @@ func VerifyHardwareCode(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenInput := req.Token
|
tokenInput := strings.ToUpper(strings.TrimSpace(req.Token))
|
||||||
sekarang := time.Now()
|
loc, _ := time.LoadLocation("Asia/Jakarta")
|
||||||
|
sekarang := time.Now().In(loc)
|
||||||
sisaMenit := 0
|
sisaMenit := 0
|
||||||
isTokenValid := false
|
isTokenValid := false
|
||||||
|
|
||||||
@@ -65,71 +67,107 @@ func VerifyHardwareCode(c *gin.Context) {
|
|||||||
isTokenValid = true
|
isTokenValid = true
|
||||||
} else if tokenInput == "ADM999" {
|
} else if tokenInput == "ADM999" {
|
||||||
fmt.Printf("[VERIFY] Master Token Admin digunakan\n")
|
fmt.Printf("[VERIFY] Master Token Admin digunakan\n")
|
||||||
sisaMenit = 999
|
sisaMenit = 1440 // 12 jam
|
||||||
isTokenValid = true
|
isTokenValid = true
|
||||||
} else {
|
} else {
|
||||||
// =========================================================
|
// =========================================================
|
||||||
// 2. PENGECEKAN KELOMPOK TOKEN MAHASISWA (Cek Database & Waktu)
|
// 2. CEK TOKEN BOOKING VIA WEB (6 DIGIT)
|
||||||
// =========================================================
|
// =========================================================
|
||||||
var jamMulai time.Time
|
var booking models.Booking
|
||||||
var jamSelesai time.Time
|
errBooking := config.DB.First(&booking, "redeem_code = ? AND status = 'Approved'", tokenInput).Error
|
||||||
type ResultTime struct {
|
|
||||||
JamMulai time.Time `gorm:"column:start_time"`
|
|
||||||
JamSelesai time.Time `gorm:"column:end_time"`
|
|
||||||
}
|
|
||||||
var result ResultTime
|
|
||||||
|
|
||||||
// Cek Bookings
|
if errBooking == nil {
|
||||||
errBooking := config.DB.Table("bookings").
|
if booking.StartTime.Format("2006-01-02") != sekarang.Format("2006-01-02") {
|
||||||
Select("start_time, end_time").
|
c.JSON(http.StatusForbidden, gin.H{"error": "Peminjaman bukan untuk hari ini!"})
|
||||||
Where("redeem_code = ?", tokenInput).
|
return
|
||||||
Scan(&result).Error
|
}
|
||||||
|
|
||||||
if errBooking == nil && !result.JamSelesai.IsZero() {
|
toleransiMasuk := booking.StartTime.Add(-15 * time.Minute)
|
||||||
jamMulai = result.JamMulai
|
if sekarang.Before(toleransiMasuk) {
|
||||||
jamSelesai = result.JamSelesai
|
c.JSON(http.StatusForbidden, gin.H{"error": "Belum waktunya. Bisa masuk 15 menit sebelum kelas!"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if sekarang.After(booking.EndTime) {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "Waktu peminjaman sudah habis!"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sisaMenit = int(booking.EndTime.Sub(sekarang).Minutes())
|
||||||
|
if sisaMenit <= 0 {
|
||||||
|
sisaMenit = 1
|
||||||
|
}
|
||||||
isTokenValid = true
|
isTokenValid = true
|
||||||
} else {
|
|
||||||
// Cek Jadwal Kelas Tetap
|
|
||||||
errSchedule := config.DB.Table("class_schedules").
|
|
||||||
Select("start_time, end_time").
|
|
||||||
Where("kode_mk = ?", tokenInput).
|
|
||||||
Scan(&result).Error
|
|
||||||
|
|
||||||
if errSchedule == nil && !result.JamSelesai.IsZero() {
|
} else {
|
||||||
jamMulai = result.JamMulai
|
// =========================================================
|
||||||
jamSelesai = result.JamSelesai
|
// 3. CEK KODE MATA KULIAH DENGAN DOUBLE-LAYER PROTECTION
|
||||||
|
// =========================================================
|
||||||
|
var reqSchedule models.ClassSchedule
|
||||||
|
// Cari apakah Kode MK yang diketik eksis di database (contoh: "EE153")
|
||||||
|
errSchedule := config.DB.First(&reqSchedule, "kode_mk = ?", tokenInput).Error
|
||||||
|
|
||||||
|
if errSchedule == nil {
|
||||||
|
// PROTEKSI LAPIS 1: Cek apakah ada yang sedang Booking via Web sekarang
|
||||||
|
var countBookingAktif int64
|
||||||
|
config.DB.Model(&models.Booking{}).Where(
|
||||||
|
"room_id = ? AND status = 'Approved' AND start_time <= ? AND end_time >= ?",
|
||||||
|
reqSchedule.RoomID, sekarang, sekarang,
|
||||||
|
).Count(&countBookingAktif)
|
||||||
|
|
||||||
|
if countBookingAktif > 0 {
|
||||||
|
c.JSON(http.StatusConflict, gin.H{"error": "Akses Ditolak: Ruangan sedang dipakai peminjam resmi (Web)!"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// PROTEKSI LAPIS 2: Cek apakah ada JADWAL KELAS LAIN yang sedang berjalan di jam ini
|
||||||
|
// Ambil nama hari ini dalam Bahasa Indonesia
|
||||||
|
hariMap := map[time.Weekday]string{
|
||||||
|
time.Sunday: "Minggu", time.Monday: "Senin", time.Tuesday: "Selasa",
|
||||||
|
time.Wednesday: "Rabu", time.Thursday: "Kamis", time.Friday: "Jumat", time.Saturday: "Sabtu",
|
||||||
|
}
|
||||||
|
hariIni := hariMap[sekarang.Weekday()]
|
||||||
|
jamSekarang := sekarang.Format("15:04:05")
|
||||||
|
|
||||||
|
var activeClass models.ClassSchedule
|
||||||
|
errActive := config.DB.Where(
|
||||||
|
"room_id = ? AND hari = ? AND jam_mulai <= ? AND jam_selesai >= ?",
|
||||||
|
reqSchedule.RoomID, hariIni, jamSekarang, jamSekarang,
|
||||||
|
).First(&activeClass).Error
|
||||||
|
|
||||||
|
if errActive == nil {
|
||||||
|
// Ada kelas yang sedang berlangsung saat ini.
|
||||||
|
// Cek apakah kelas tersebut BERBEDA dengan Kode MK yang diinputkan pengguna
|
||||||
|
// 🌟 PERBAIKAN: Ubah KodeMk menjadi KodeMK (K huruf besar)
|
||||||
|
if activeClass.KodeMK != reqSchedule.KodeMK {
|
||||||
|
c.JSON(http.StatusConflict, gin.H{
|
||||||
|
// 🌟 PERBAIKAN: Ubah NamaMk menjadi NamaMK (K huruf besar)
|
||||||
|
"error": "Akses Ditolak: Ruangan sedang digunakan oleh jadwal kelas " + activeClass.NamaMK,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Jika kodenya SAMA (Dosen menginputkan kodenya di jam yang memang miliknya), abaikan & beri izin masuk.
|
||||||
|
}
|
||||||
|
|
||||||
|
// JIKA LOLOS SEMUA PROTEKSI: Berikan akses waktu berdasarkan jumlah SKS
|
||||||
|
// 🌟 PERBAIKAN: Pastikan ini sesuai dengan struct di models (JumlahSks atau JumlahSKS)
|
||||||
|
durasiMenit := reqSchedule.JumlahSks * 50
|
||||||
|
if durasiMenit <= 0 {
|
||||||
|
durasiMenit = 100 // Nilai aman (default 2 SKS) jika database kosong
|
||||||
|
}
|
||||||
|
|
||||||
|
sisaMenit = durasiMenit
|
||||||
isTokenValid = true
|
isTokenValid = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isTokenValid {
|
if !isTokenValid {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token salah atau tidak ditemukan"})
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token Web atau Kode MK tidak ditemukan!"})
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
jamMulaiHariIni := time.Date(sekarang.Year(), sekarang.Month(), sekarang.Day(),
|
|
||||||
jamMulai.Hour(), jamMulai.Minute(), jamMulai.Second(), 0, sekarang.Location())
|
|
||||||
jamSelesaiHariIni := time.Date(sekarang.Year(), sekarang.Month(), sekarang.Day(),
|
|
||||||
jamSelesai.Hour(), jamSelesai.Minute(), jamSelesai.Second(), 0, sekarang.Location())
|
|
||||||
|
|
||||||
batasMasukAwal := jamMulaiHariIni.Add(-15 * time.Minute)
|
|
||||||
if sekarang.Before(batasMasukAwal) {
|
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "Jadwal kelas belum dimulai"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
selisihWaktu := jamSelesaiHariIni.Sub(sekarang)
|
|
||||||
sisaMenit = int(selisihWaktu.Minutes())
|
|
||||||
|
|
||||||
if sisaMenit <= 0 {
|
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "Waktu peminjaman sudah habis"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================
|
// =========================================================
|
||||||
// 3. JIKA VALID (Master / Mahasiswa), BUKA GEMBOK VIA MQTT
|
// 4. JIKA VALID, BUKA GEMBOK VIA MQTT & HTTP LOKAL
|
||||||
// =========================================================
|
// =========================================================
|
||||||
DeviceStatusCache["lampu1"] = "on"
|
DeviceStatusCache["lampu1"] = "on"
|
||||||
DeviceStatusCache["lampu2"] = "on"
|
DeviceStatusCache["lampu2"] = "on"
|
||||||
@@ -147,6 +185,7 @@ func VerifyHardwareCode(c *gin.Context) {
|
|||||||
client := mqtt.NewClient(opts)
|
client := mqtt.NewClient(opts)
|
||||||
if tokenMQTT := client.Connect(); tokenMQTT.Wait() && tokenMQTT.Error() == nil {
|
if tokenMQTT := client.Connect(); tokenMQTT.Wait() && tokenMQTT.Error() == nil {
|
||||||
client.Publish("sclass/d101/auth", 0, true, "UNLOCK").Wait()
|
client.Publish("sclass/d101/auth", 0, true, "UNLOCK").Wait()
|
||||||
|
// Opsional: Jika masih mengandalkan MQTT untuk lampu
|
||||||
client.Publish("sclass/d101/lampu1", 0, false, "on").Wait()
|
client.Publish("sclass/d101/lampu1", 0, false, "on").Wait()
|
||||||
client.Publish("sclass/d101/lampu2", 0, false, "on").Wait()
|
client.Publish("sclass/d101/lampu2", 0, false, "on").Wait()
|
||||||
client.Disconnect(250)
|
client.Disconnect(250)
|
||||||
@@ -157,7 +196,7 @@ func VerifyHardwareCode(c *gin.Context) {
|
|||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"message": "Token Valid",
|
"message": "Akses Diberikan",
|
||||||
"duration_minutes": sisaMenit,
|
"duration_minutes": sisaMenit,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -172,7 +211,6 @@ func ControlHardware(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🌟 BLOK 1: KONTROL GEMBOK / OTORISASI RELAY
|
|
||||||
if req.Device == "auth" {
|
if req.Device == "auth" {
|
||||||
broker := os.Getenv("MQTT_BROKER")
|
broker := os.Getenv("MQTT_BROKER")
|
||||||
user := os.Getenv("MQTT_USER")
|
user := os.Getenv("MQTT_USER")
|
||||||
@@ -191,7 +229,6 @@ func ControlHardware(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- BLOK 2: KONTROL LAMPU ---
|
|
||||||
if req.Device == "lampu1" || req.Device == "lampu2" {
|
if req.Device == "lampu1" || req.Device == "lampu2" {
|
||||||
broker := os.Getenv("MQTT_BROKER")
|
broker := os.Getenv("MQTT_BROKER")
|
||||||
user := os.Getenv("MQTT_USER")
|
user := os.Getenv("MQTT_USER")
|
||||||
@@ -219,7 +256,6 @@ func ControlHardware(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- BLOK 3: KONTROL AC & PROYEKTOR ---
|
|
||||||
if req.Device == "ac" || req.Device == "projector" {
|
if req.Device == "ac" || req.Device == "projector" {
|
||||||
haURL := os.Getenv("HA_URL")
|
haURL := os.Getenv("HA_URL")
|
||||||
haToken := os.Getenv("HA_TOKEN")
|
haToken := os.Getenv("HA_TOKEN")
|
||||||
@@ -264,7 +300,7 @@ func ControlHardware(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// HELPER 1: MENGAMBIL ANGKA DARI HOME ASSISTANT (Untuk Daya/Watt)
|
// HELPER 1 & 2: HTTP GET KE HOME ASSISTANT
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
func fetchHAState(haURL string, haToken string, entityID string) float64 {
|
func fetchHAState(haURL string, haToken string, entityID string) float64 {
|
||||||
apiURL := fmt.Sprintf("%s/api/states/%s", haURL, entityID)
|
apiURL := fmt.Sprintf("%s/api/states/%s", haURL, entityID)
|
||||||
@@ -294,9 +330,6 @@ func fetchHAState(haURL string, haToken string, entityID string) float64 {
|
|||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// HELPER 2: MENGAMBIL STATUS TEKS DARI HOME ASSISTANT (Untuk ON/OFF)
|
|
||||||
// =========================================================================
|
|
||||||
func fetchHAStringState(haURL string, haToken string, entityID string) string {
|
func fetchHAStringState(haURL string, haToken string, entityID string) string {
|
||||||
apiURL := fmt.Sprintf("%s/api/states/%s", haURL, entityID)
|
apiURL := fmt.Sprintf("%s/api/states/%s", haURL, entityID)
|
||||||
|
|
||||||
@@ -325,7 +358,7 @@ func fetchHAStringState(haURL string, haToken string, entityID string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// FUNGSI 3: MENDAPATKAN STATUS DAYA DARI 3 MCB SEKALIGUS
|
// FUNGSI 3: MENDAPATKAN STATUS DAYA
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
func GetPowerStatus(c *gin.Context) {
|
func GetPowerStatus(c *gin.Context) {
|
||||||
haURL := os.Getenv("HA_URL")
|
haURL := os.Getenv("HA_URL")
|
||||||
@@ -343,92 +376,78 @@ func GetPowerStatus(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// FUNGSI 4: GLOBAL POWER CONTROL (Sequential ON/OFF dengan Jeda 1 Detik)
|
// FUNGSI 4: GLOBAL POWER CONTROL
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
func GlobalPowerControl(c *gin.Context) {
|
func GlobalPowerControl(c *gin.Context) {
|
||||||
var req GlobalPowerRequest
|
var req GlobalPowerRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Format data tidak valid"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Format data tidak valid"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
haURL := os.Getenv("HA_URL")
|
haURL := os.Getenv("HA_URL")
|
||||||
haToken := os.Getenv("HA_TOKEN")
|
haToken := os.Getenv("HA_TOKEN")
|
||||||
|
|
||||||
service := "turn_off"
|
service := "turn_off"
|
||||||
if req.Action == "on" {
|
if req.Action == "on" {
|
||||||
service = "turn_on"
|
service = "turn_on"
|
||||||
}
|
}
|
||||||
apiURL := fmt.Sprintf("%s/api/services/switch/%s", haURL, service)
|
apiURL := fmt.Sprintf("%s/api/services/switch/%s", haURL, service)
|
||||||
|
|
||||||
mcbs := []string{
|
// 🌟 PERBAIKAN: Hapus array dan loop, cukup gunakan 1 target saja
|
||||||
"switch.wifi_smart_meter_pro_switch", // Umum
|
targetSwitch := "switch.wifi_smart_meter_pro_switch"
|
||||||
"switch.wifi_smart_meter_pro_2_switch", // AC1
|
|
||||||
"switch.wifi_smart_meter_pro_3_switch", // AC2
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, mcb := range mcbs {
|
payload := map[string]string{"entity_id": targetSwitch}
|
||||||
payload := map[string]string{"entity_id": mcb}
|
jsonPayload, _ := json.Marshal(payload)
|
||||||
jsonPayload, _ := json.Marshal(payload)
|
|
||||||
|
|
||||||
reqHA, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonPayload))
|
reqHA, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonPayload))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
reqHA.Header.Set("Authorization", "Bearer "+haToken)
|
reqHA.Header.Set("Authorization", "Bearer "+haToken)
|
||||||
reqHA.Header.Set("Content-Type", "application/json")
|
reqHA.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
client := &http.Client{Timeout: 5 * time.Second}
|
client := &http.Client{Timeout: 5 * time.Second}
|
||||||
resp, err := client.Do(reqHA)
|
resp, err := client.Do(reqHA)
|
||||||
if err == nil && resp != nil {
|
if err == nil && resp != nil {
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("[MCB] Sinyal %s dikirim ke %s\n", req.Action, mcb)
|
fmt.Printf("[MCB] Sinyal %s dikirim ke %s\n", req.Action, targetSwitch)
|
||||||
|
|
||||||
if i < len(mcbs)-1 {
|
// Mematikan status virtual di dashboard
|
||||||
time.Sleep(1 * time.Second)
|
if req.Action == "off" {
|
||||||
}
|
DeviceStatusCache["lampu1"] = "off"
|
||||||
}
|
DeviceStatusCache["lampu2"] = "off"
|
||||||
|
DeviceStatusCache["ac"] = "off"
|
||||||
|
DeviceStatusCache["projector"] = "off"
|
||||||
|
}
|
||||||
|
|
||||||
if req.Action == "off" {
|
c.JSON(http.StatusOK, gin.H{
|
||||||
DeviceStatusCache["lampu1"] = "off"
|
"status": "success",
|
||||||
DeviceStatusCache["lampu2"] = "off"
|
"message": fmt.Sprintf("Daya utama berhasil di-%s", req.Action),
|
||||||
DeviceStatusCache["ac"] = "off"
|
})
|
||||||
DeviceStatusCache["projector"] = "off"
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"status": "success",
|
|
||||||
"message": fmt.Sprintf("Seluruh daya ruangan berhasil di-%s secara berurutan", req.Action),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// FUNGSI 5: MENGIRIM STATUS REAL-TIME KE WEB FRONTEND (SENSOR FUSION)
|
// FUNGSI 5: SENSOR FUSION FRONTEND
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
func GetHardwareStatus(c *gin.Context) {
|
func GetHardwareStatus(c *gin.Context) {
|
||||||
haURL := os.Getenv("HA_URL")
|
haURL := os.Getenv("HA_URL")
|
||||||
haToken := os.Getenv("HA_TOKEN")
|
haToken := os.Getenv("HA_TOKEN")
|
||||||
|
|
||||||
// 1. Cek status fisik MCB di Home Assistant
|
|
||||||
mcbUmum := fetchHAStringState(haURL, haToken, "switch.wifi_smart_meter_pro_switch")
|
mcbUmum := fetchHAStringState(haURL, haToken, "switch.wifi_smart_meter_pro_switch")
|
||||||
mcbAC1 := fetchHAStringState(haURL, haToken, "switch.wifi_smart_meter_pro_2_switch")
|
mcbAC1 := fetchHAStringState(haURL, haToken, "switch.wifi_smart_meter_pro_2_switch")
|
||||||
|
|
||||||
// 2. Ambil data dari Cache (Perintah terakhir dari Web/ESP32 melalui IR/MQTT)
|
|
||||||
statusLampu1 := DeviceStatusCache["lampu1"]
|
statusLampu1 := DeviceStatusCache["lampu1"]
|
||||||
statusLampu2 := DeviceStatusCache["lampu2"]
|
statusLampu2 := DeviceStatusCache["lampu2"]
|
||||||
statusProyektor := DeviceStatusCache["projector"]
|
statusProyektor := DeviceStatusCache["projector"]
|
||||||
statusAC := DeviceStatusCache["ac"]
|
statusAC := DeviceStatusCache["ac"]
|
||||||
|
|
||||||
// 3. LOGIKA SENSOR FUSION (Penggabungan Fisik & Virtual)
|
|
||||||
// Jika listrik MCB fisik terputus, maka otomatis perangkat di jalur tersebut PASTI MATI (off),
|
|
||||||
// mengabaikan apapun status terakhir yang ada di memori Cache.
|
|
||||||
if mcbUmum == "off" {
|
if mcbUmum == "off" {
|
||||||
statusLampu1 = "off"
|
statusLampu1 = "off"
|
||||||
statusLampu2 = "off"
|
statusLampu2 = "off"
|
||||||
statusProyektor = "off"
|
statusProyektor = "off"
|
||||||
|
|
||||||
// Sinkronkan kembali cache agar tidak ada "state" nyangkut
|
|
||||||
DeviceStatusCache["lampu1"] = "off"
|
DeviceStatusCache["lampu1"] = "off"
|
||||||
DeviceStatusCache["lampu2"] = "off"
|
DeviceStatusCache["lampu2"] = "off"
|
||||||
DeviceStatusCache["projector"] = "off"
|
DeviceStatusCache["projector"] = "off"
|
||||||
@@ -439,7 +458,6 @@ func GetHardwareStatus(c *gin.Context) {
|
|||||||
DeviceStatusCache["ac"] = "off"
|
DeviceStatusCache["ac"] = "off"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kirim data yang sudah divalidasi ke Web dan Layar ESP32
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"data": map[string]string{
|
"data": map[string]string{
|
||||||
@@ -449,4 +467,4 @@ func GetHardwareStatus(c *gin.Context) {
|
|||||||
"ac": statusAC,
|
"ac": statusAC,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,26 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"s-class-backend/config"
|
||||||
|
"s-class-backend/models"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
// UBAH DUA BARIS INI MENJADI s-class-backend
|
|
||||||
"s-class-backend/config"
|
|
||||||
"s-class-backend/models"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Fungsi untuk mengambil semua jadwal dari database
|
// Struktur data untuk menerima input dari Frontend (Form Modal)
|
||||||
|
type ScheduleInput struct {
|
||||||
|
KodeMK string `json:"kode_mk" binding:"required"`
|
||||||
|
NamaMK string `json:"nama_mk" binding:"required"`
|
||||||
|
RoomID int `json:"room_id" binding:"required"` // 🌟 PERBAIKAN: Ubah uint menjadi int
|
||||||
|
Hari string `json:"hari" binding:"required"`
|
||||||
|
JamMulai string `json:"jam_mulai" binding:"required"`
|
||||||
|
JamSelesai string `json:"jam_selesai" binding:"required"`
|
||||||
|
JumlahSks int `json:"jumlah_sks" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// 1. GET SCHEDULES (Menampilkan Data ke Tabel/Kalender)
|
||||||
|
// =========================================================
|
||||||
func GetSchedules(c *gin.Context) {
|
func GetSchedules(c *gin.Context) {
|
||||||
var schedules []models.ClassSchedule
|
var schedules []models.ClassSchedule
|
||||||
|
|
||||||
@@ -20,8 +31,100 @@ func GetSchedules(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kirim data ke frontend (React)
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"data": schedules,
|
"data": schedules,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// 2. CREATE SCHEDULE (Menambah Jadwal Baru)
|
||||||
|
// =========================================================
|
||||||
|
func CreateSchedule(c *gin.Context) {
|
||||||
|
var input ScheduleInput
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Format data tidak valid: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newSchedule := models.ClassSchedule{
|
||||||
|
KodeMK: input.KodeMK,
|
||||||
|
NamaMK: input.NamaMK,
|
||||||
|
RoomID: input.RoomID,
|
||||||
|
Hari: input.Hari,
|
||||||
|
JamMulai: input.JamMulai,
|
||||||
|
JamSelesai: input.JamSelesai,
|
||||||
|
JumlahSks: input.JumlahSks,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := config.DB.Create(&newSchedule).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Gagal menyimpan jadwal baru ke database"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, gin.H{
|
||||||
|
"message": "Jadwal berhasil ditambahkan",
|
||||||
|
"data": newSchedule,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// 3. UPDATE SCHEDULE (Mengedit Jadwal Saat UTS/Berubah)
|
||||||
|
// =========================================================
|
||||||
|
func UpdateSchedule(c *gin.Context) {
|
||||||
|
scheduleID := c.Param("id")
|
||||||
|
|
||||||
|
var input ScheduleInput
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Format data tidak valid"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var schedule models.ClassSchedule
|
||||||
|
// Cari jadwal lama di database berdasarkan ID
|
||||||
|
if err := config.DB.First(&schedule, scheduleID).Error; err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Jadwal tidak ditemukan"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timpa data lama dengan inputan baru dari Admin
|
||||||
|
schedule.KodeMK = input.KodeMK
|
||||||
|
schedule.NamaMK = input.NamaMK
|
||||||
|
schedule.RoomID = input.RoomID
|
||||||
|
schedule.Hari = input.Hari
|
||||||
|
schedule.JamMulai = input.JamMulai
|
||||||
|
schedule.JamSelesai = input.JamSelesai
|
||||||
|
schedule.JumlahSks = input.JumlahSks
|
||||||
|
|
||||||
|
// Simpan perubahan ke database
|
||||||
|
if err := config.DB.Save(&schedule).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Gagal memperbarui jadwal"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "Jadwal berhasil diperbarui",
|
||||||
|
"data": schedule,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// 4. DELETE SCHEDULE (Menghapus Jadwal)
|
||||||
|
// =========================================================
|
||||||
|
func DeleteSchedule(c *gin.Context) {
|
||||||
|
scheduleID := c.Param("id")
|
||||||
|
|
||||||
|
var schedule models.ClassSchedule
|
||||||
|
// Pastikan data yang mau dihapus itu memang ada
|
||||||
|
if err := config.DB.First(&schedule, scheduleID).Error; err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Jadwal tidak ditemukan"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hapus secara permanen dari database
|
||||||
|
if err := config.DB.Delete(&schedule).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Gagal menghapus jadwal"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Jadwal berhasil dihapus"})
|
||||||
|
}
|
||||||
@@ -60,4 +60,5 @@ type ClassSchedule struct {
|
|||||||
Hari string `gorm:"type:varchar(20);not null;column:hari" json:"hari"`
|
Hari string `gorm:"type:varchar(20);not null;column:hari" json:"hari"`
|
||||||
JamMulai string `gorm:"type:time;not null;column:jam_mulai" json:"jam_mulai"`
|
JamMulai string `gorm:"type:time;not null;column:jam_mulai" json:"jam_mulai"`
|
||||||
JamSelesai string `gorm:"type:time;not null;column:jam_selesai" json:"jam_selesai"`
|
JamSelesai string `gorm:"type:time;not null;column:jam_selesai" json:"jam_selesai"`
|
||||||
|
JumlahSks int `gorm:"column:jumlah_sks" json:"jumlah_sks"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
|
|||||||
<div className={`p-2 rounded-lg transition-colors ${pathname === '/admin/schedules' ? 'bg-white/60 shadow-sm' : 'bg-white/30 group-hover:bg-white/50'}`}>
|
<div className={`p-2 rounded-lg transition-colors ${pathname === '/admin/schedules' ? 'bg-white/60 shadow-sm' : 'bg-white/30 group-hover:bg-white/50'}`}>
|
||||||
<CalendarDays size={22} />
|
<CalendarDays size={22} />
|
||||||
</div>
|
</div>
|
||||||
<span>Jadwal Kuliah</span>
|
<span>Edit Jadwal</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -115,13 +115,11 @@ export default function PowerMonitoringPage() {
|
|||||||
if (!window.confirm(confirmMsg)) return;
|
if (!window.confirm(confirmMsg)) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jika yang ditekan bukan D101, ubah UI saja (simulasi)
|
|
||||||
if (roomName !== "Kelas D101") {
|
if (roomName !== "Kelas D101") {
|
||||||
setRoomDeviceStatus(prev => ({ ...prev, [roomIdKey]: { ...prev[roomIdKey], [deviceName]: !currentStatus } }));
|
setRoomDeviceStatus(prev => ({ ...prev, [roomIdKey]: { ...prev[roomIdKey], [deviceName]: !currentStatus } }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logika khusus D101 (Tembak ke Golang)
|
|
||||||
let backendDevice = "";
|
let backendDevice = "";
|
||||||
if (deviceName === 'AC') backendDevice = "ac";
|
if (deviceName === 'AC') backendDevice = "ac";
|
||||||
else if (deviceName === 'Proyektor') backendDevice = "projector";
|
else if (deviceName === 'Proyektor') backendDevice = "projector";
|
||||||
@@ -147,17 +145,15 @@ export default function PowerMonitoringPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// FUNGSI GLOBAL POWER (ON/OFF KHUSUS D101)
|
|
||||||
const handleGlobalPower = async (roomName: string, roomId: number, currentRelayStatus: boolean) => {
|
const handleGlobalPower = async (roomName: string, roomId: number, currentRelayStatus: boolean) => {
|
||||||
if (roomName === "Kelas D101") {
|
if (roomName === "Kelas D101") {
|
||||||
const actionType = currentRelayStatus ? "off" : "on";
|
const actionType = currentRelayStatus ? "off" : "on";
|
||||||
const confirmMessage = currentRelayStatus
|
const confirmMessage = currentRelayStatus
|
||||||
? `PERINGATAN FATAL: Anda yakin ingin MEMUTUS 3 MCB Daya di ${roomName}? (Proses berurutan ~3 detik)`
|
? `PERINGATAN FATAL: Anda yakin ingin MEMUTUS MCB Daya Utama di ${roomName}?`
|
||||||
: `RESTORE POWER: Anda yakin ingin MENYALAKAN KEMBALI 3 MCB Daya di ${roomName}? (Proses berurutan ~3 detik)`;
|
: `RESTORE POWER: Anda yakin ingin MENYALAKAN KEMBALI MCB Daya di ${roomName}?`;
|
||||||
|
|
||||||
if (!window.confirm(confirmMessage)) return;
|
if (!window.confirm(confirmMessage)) return;
|
||||||
|
|
||||||
// Ubah UI seketika
|
|
||||||
setRooms(prev => prev.map(r => r.name === roomName ? { ...r, isRelayOn: !currentRelayStatus } : r));
|
setRooms(prev => prev.map(r => r.name === roomName ? { ...r, isRelayOn: !currentRelayStatus } : r));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -172,7 +168,6 @@ export default function PowerMonitoringPage() {
|
|||||||
fetchPowerStatus();
|
fetchPowerStatus();
|
||||||
fetchDeviceStatus();
|
fetchDeviceStatus();
|
||||||
} else {
|
} else {
|
||||||
// Rollback UI jika gagal
|
|
||||||
setRooms(prev => prev.map(r => r.name === roomName ? { ...r, isRelayOn: currentRelayStatus } : r));
|
setRooms(prev => prev.map(r => r.name === roomName ? { ...r, isRelayOn: currentRelayStatus } : r));
|
||||||
alert("Gagal mengontrol MCB utama. Cek koneksi server.");
|
alert("Gagal mengontrol MCB utama. Cek koneksi server.");
|
||||||
}
|
}
|
||||||
@@ -181,7 +176,6 @@ export default function PowerMonitoringPage() {
|
|||||||
alert("Terjadi kesalahan jaringan saat menghubungi server.");
|
alert("Terjadi kesalahan jaringan saat menghubungi server.");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Dummy untuk kelas lain
|
|
||||||
const actionType = currentRelayStatus ? "Mematikan" : "Menyalakan";
|
const actionType = currentRelayStatus ? "Mematikan" : "Menyalakan";
|
||||||
if (window.confirm(`Simulasi ${actionType} daya di ${roomName}?`)) {
|
if (window.confirm(`Simulasi ${actionType} daya di ${roomName}?`)) {
|
||||||
setRooms(prev => prev.map(r => r.name === roomName ? { ...r, isRelayOn: !currentRelayStatus, power: 0 } : r));
|
setRooms(prev => prev.map(r => r.name === roomName ? { ...r, isRelayOn: !currentRelayStatus, power: 0 } : r));
|
||||||
|
|||||||
+68
-22
@@ -9,13 +9,46 @@ export default function AdminDashboard() {
|
|||||||
const [roomStats, setRoomStats] = useState<any[]>([]);
|
const [roomStats, setRoomStats] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// STATE BARU: Khusus menyimpan TOTAL DAYA D101
|
||||||
|
const [d101TotalPower, setD101TotalPower] = useState<number>(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchAdminData();
|
fetchAdminData();
|
||||||
// Refresh data konsumsi daya setiap 10 detik agar terlihat real-time
|
fetchD101Power(); // Panggil fungsi daya saat pertama kali render
|
||||||
const interval = setInterval(fetchAdminData, 10000);
|
|
||||||
return () => clearInterval(interval);
|
// Refresh data admin (ruangan & booking) setiap 10 detik
|
||||||
|
const intervalAdmin = setInterval(fetchAdminData, 10000);
|
||||||
|
// Refresh data konsumsi daya D101 setiap 3 detik (mengikuti halaman monitoring)
|
||||||
|
const intervalPower = setInterval(fetchD101Power, 3000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(intervalAdmin);
|
||||||
|
clearInterval(intervalPower);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// FUNGSI BARU: Tarik Data Daya (Power / Watt) KHUSUS D101
|
||||||
|
// =========================================================================
|
||||||
|
const fetchD101Power = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("http://172.17.172.17:8080/api/hardware/power-status");
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Kalkulasi total watt
|
||||||
|
const umum = parseFloat(data.umum) || 0;
|
||||||
|
const ac1 = parseFloat(data.ac1) || 0;
|
||||||
|
const ac2 = parseFloat(data.ac2) || 0;
|
||||||
|
const totalD101 = umum + ac1 + ac2;
|
||||||
|
|
||||||
|
setD101TotalPower(totalD101);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal mengambil data daya D101:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchAdminData = async () => {
|
const fetchAdminData = async () => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
@@ -37,7 +70,7 @@ export default function AdminDashboard() {
|
|||||||
|
|
||||||
const roomsWithDummyPower = (roomRes.data.data || []).map((room: any) => ({
|
const roomsWithDummyPower = (roomRes.data.data || []).map((room: any) => ({
|
||||||
...room,
|
...room,
|
||||||
// Simulasi daya acak antara 0 sampai 15 Watt untuk keperluan UI
|
// Simulasi daya acak antara 0 sampai 15 Watt (D101 akan di-override di bagian render)
|
||||||
power_consumption: Math.floor(Math.random() * 15)
|
power_consumption: Math.floor(Math.random() * 15)
|
||||||
}));
|
}));
|
||||||
setRoomStats(roomsWithDummyPower);
|
setRoomStats(roomsWithDummyPower);
|
||||||
@@ -49,7 +82,6 @@ export default function AdminDashboard() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// PERBAIKAN 1: Ubah tipe id menjadi 'string' karena kita menggunakan UUID
|
|
||||||
const handleAction = async (id: string, status: string) => {
|
const handleAction = async (id: string, status: string) => {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
@@ -76,22 +108,39 @@ export default function AdminDashboard() {
|
|||||||
<Activity className="text-blue-500" /> Real-time Room Energy
|
<Activity className="text-blue-500" /> Real-time Room Energy
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{/* TAMBAHKAN PENGURUTAN ABJAD DI SINI */}
|
|
||||||
{[...roomStats]
|
{[...roomStats]
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
.map((room) => (
|
.map((room) => {
|
||||||
<div key={room.room_id} className="bg-white p-5 rounded-xl border border-gray-100 shadow-sm">
|
|
||||||
<div className="flex justify-between items-start mb-3">
|
// LOGIKA INJEKSI: Timpa nilai dummy dengan daya asli jika ini Kelas D101
|
||||||
<span className="text-sm font-bold text-gray-400 uppercase tracking-tight">{room.name}</span>
|
const isD101 = room.name === "Kelas D101";
|
||||||
<div className={`h-3 w-3 rounded-full ${room.power_consumption > 5 ? 'bg-orange-500 animate-pulse' : 'bg-gray-300'}`} />
|
const displayedPower = isD101 ? d101TotalPower : room.power_consumption;
|
||||||
</div>
|
const isPowerActive = displayedPower > 0;
|
||||||
<div className="flex items-end gap-2">
|
|
||||||
<span className="text-3xl font-black text-gray-800">{room.power_consumption || 0}</span>
|
return (
|
||||||
<span className="text-gray-500 font-bold mb-1">Watts</span>
|
<div key={room.room_id} className={`bg-white p-5 rounded-xl border shadow-sm transition-all
|
||||||
</div>
|
${isD101 ? 'border-blue-200 border-l-4 border-l-blue-500 bg-blue-50/10' : 'border-gray-100'}`}>
|
||||||
<p className="text-[10px] text-gray-400 mt-2 font-medium">Last updated: Just now</p>
|
|
||||||
</div>
|
<div className="flex justify-between items-start mb-3">
|
||||||
))}
|
<span className={`text-sm font-bold uppercase tracking-tight ${isD101 ? 'text-blue-700' : 'text-gray-400'}`}>
|
||||||
|
{room.name} {isD101 && <span className="ml-2 text-[8px] bg-blue-500 text-white px-2 py-0.5 rounded-full">REAL SENSOR</span>}
|
||||||
|
</span>
|
||||||
|
<div className={`h-3 w-3 rounded-full ${isPowerActive ? (isD101 ? 'bg-green-500 animate-pulse' : 'bg-orange-500 animate-pulse') : 'bg-gray-300'}`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-end gap-2">
|
||||||
|
<span className={`text-3xl font-black ${isD101 ? 'text-blue-700' : 'text-gray-800'}`}>
|
||||||
|
{displayedPower.toFixed(1)}
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-500 font-bold mb-1">Watts</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[10px] text-gray-400 mt-2 font-medium">
|
||||||
|
{isD101 ? "Live auto-update via IoT" : "Last updated: Just now"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -116,12 +165,10 @@ export default function AdminDashboard() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-50">
|
<tbody className="divide-y divide-gray-50">
|
||||||
{pendingBookings.map((b) => (
|
{pendingBookings.map((b) => (
|
||||||
// PERBAIKAN 2: Gunakan booking_id sebagai key
|
|
||||||
<tr key={b.booking_id} className="hover:bg-gray-50/50 transition-colors">
|
<tr key={b.booking_id} className="hover:bg-gray-50/50 transition-colors">
|
||||||
<td className="p-4 font-bold text-gray-800 text-sm">{b.user?.full_name || "Tanpa Nama"}</td>
|
<td className="p-4 font-bold text-gray-800 text-sm">{b.user?.full_name || "Tanpa Nama"}</td>
|
||||||
<td className="p-4 text-sm font-medium text-gray-600">{b.room?.name || "Ruangan Tidak Diketahui"}</td>
|
<td className="p-4 text-sm font-medium text-gray-600">{b.room?.name || "Ruangan Tidak Diketahui"}</td>
|
||||||
|
|
||||||
{/* PERBAIKAN 3: Format Waktu Mulai dan Selesai yang Rapi */}
|
|
||||||
<td className="p-4 text-sm text-gray-600">
|
<td className="p-4 text-sm text-gray-600">
|
||||||
<div className="font-bold">
|
<div className="font-bold">
|
||||||
{new Date(b.start_time).toLocaleTimeString('id-ID', {hour: '2-digit', minute:'2-digit'})} - {new Date(b.end_time).toLocaleTimeString('id-ID', {hour: '2-digit', minute:'2-digit'})}
|
{new Date(b.start_time).toLocaleTimeString('id-ID', {hour: '2-digit', minute:'2-digit'})} - {new Date(b.end_time).toLocaleTimeString('id-ID', {hour: '2-digit', minute:'2-digit'})}
|
||||||
@@ -134,7 +181,6 @@ export default function AdminDashboard() {
|
|||||||
<td className="p-4 text-sm text-gray-600 truncate max-w-60">{b.purpose}</td>
|
<td className="p-4 text-sm text-gray-600 truncate max-w-60">{b.purpose}</td>
|
||||||
<td className="p-4">
|
<td className="p-4">
|
||||||
<div className="flex justify-center gap-2">
|
<div className="flex justify-center gap-2">
|
||||||
{/* PERBAIKAN 4: Gunakan b.booking_id di fungsi onClick */}
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handleAction(b.booking_id, 'Approved')}
|
onClick={() => handleAction(b.booking_id, 'Approved')}
|
||||||
className="p-2 bg-green-50 text-green-600 rounded-lg hover:bg-green-100 transition-colors"
|
className="p-2 bg-green-50 text-green-600 rounded-lg hover:bg-green-100 transition-colors"
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default function ManageRoomsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
{/* TAMBAHKAN [...rooms].sort(...) SEBELUM .map */}
|
{/* [...rooms].sort memastikan urutan kelas selalu rapi dari D101, D102, dst */}
|
||||||
{[...rooms]
|
{[...rooms]
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
.map((room) => (
|
.map((room) => (
|
||||||
|
|||||||
@@ -2,12 +2,26 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { CalendarDays, Plus, Trash2, Clock, MapPin, LayoutList, Calendar } from "lucide-react";
|
import { CalendarDays, Plus, Trash2, Clock, MapPin, LayoutList, Calendar, Edit, X, Save } from "lucide-react";
|
||||||
|
|
||||||
export default function SchedulesPage() {
|
export default function SchedulesPage() {
|
||||||
const [schedules, setSchedules] = useState<any[]>([]);
|
const [schedules, setSchedules] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [viewMode, setViewMode] = useState<"table" | "calendar">("table"); // State untuk pindah mode
|
const [viewMode, setViewMode] = useState<"table" | "calendar">("table");
|
||||||
|
|
||||||
|
// STATE UNTUK MODAL FORM (TAMBAH & EDIT)
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
schedule_id: null as number | null,
|
||||||
|
kode_mk: "",
|
||||||
|
nama_mk: "",
|
||||||
|
room_id: 1, // Default Kelas D101
|
||||||
|
hari: "Senin",
|
||||||
|
jam_mulai: "",
|
||||||
|
jam_selesai: "",
|
||||||
|
jumlah_sks: 2,
|
||||||
|
});
|
||||||
|
|
||||||
const hariUrutan = ["Senin", "Selasa", "Rabu", "Kamis", "Jumat"];
|
const hariUrutan = ["Senin", "Selasa", "Rabu", "Kamis", "Jumat"];
|
||||||
const listJam = ["07:00", "08:00", "09:00", "10:00", "11:00", "12:00", "13:00", "14:00", "15:00", "16:00"];
|
const listJam = ["07:00", "08:00", "09:00", "10:00", "11:00", "12:00", "13:00", "14:00", "15:00", "16:00"];
|
||||||
@@ -31,7 +45,92 @@ export default function SchedulesPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper untuk mengecek apakah ada matkul di hari dan jam tertentu (Untuk Mode Kalender)
|
// =========================================================================
|
||||||
|
// FUNGSI CRUD (TAMBAH, EDIT, HAPUS)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
// 1. Buka Modal untuk Tambah Baru
|
||||||
|
const handleAddClick = () => {
|
||||||
|
setFormData({
|
||||||
|
schedule_id: null,
|
||||||
|
kode_mk: "",
|
||||||
|
nama_mk: "",
|
||||||
|
room_id: 1,
|
||||||
|
hari: "Senin",
|
||||||
|
jam_mulai: "07:00",
|
||||||
|
jam_selesai: "08:40",
|
||||||
|
jumlah_sks: 2,
|
||||||
|
});
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Buka Modal untuk Edit Data Lama
|
||||||
|
const handleEditClick = (sched: any) => {
|
||||||
|
setFormData({
|
||||||
|
schedule_id: sched.schedule_id,
|
||||||
|
kode_mk: sched.kode_mk,
|
||||||
|
nama_mk: sched.nama_mk,
|
||||||
|
room_id: sched.room_id,
|
||||||
|
hari: sched.hari,
|
||||||
|
jam_mulai: sched.jam_mulai.substring(0, 5), // Potong detik jika ada (07:00:00 -> 07:00)
|
||||||
|
jam_selesai: sched.jam_selesai.substring(0, 5),
|
||||||
|
jumlah_sks: sched.jumlah_sks || 2,
|
||||||
|
});
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. Simpan Data (Create atau Update ke Backend)
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsSaving(true);
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
const headers = { Authorization: `Bearer ${token}` };
|
||||||
|
|
||||||
|
// Format ulang jam agar aman (tambahkan detik 00)
|
||||||
|
const payload = {
|
||||||
|
...formData,
|
||||||
|
jam_mulai: formData.jam_mulai.length === 5 ? `${formData.jam_mulai}:00` : formData.jam_mulai,
|
||||||
|
jam_selesai: formData.jam_selesai.length === 5 ? `${formData.jam_selesai}:00` : formData.jam_selesai,
|
||||||
|
jumlah_sks: Number(formData.jumlah_sks)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (formData.schedule_id) {
|
||||||
|
// UPDATE (PUT)
|
||||||
|
await axios.put(`http://172.17.172.17:8080/api/schedules/${formData.schedule_id}`, payload, { headers });
|
||||||
|
alert("Jadwal berhasil diperbarui!");
|
||||||
|
} else {
|
||||||
|
// CREATE BARU (POST)
|
||||||
|
await axios.post("http://172.17.172.17:8080/api/schedules", payload, { headers });
|
||||||
|
alert("Jadwal baru berhasil ditambahkan!");
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsModalOpen(false);
|
||||||
|
fetchSchedules(); // Refresh data di tabel
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("Gagal menyimpan jadwal:", err);
|
||||||
|
alert("Gagal menyimpan data: " + (err.response?.data?.error || err.message));
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. Hapus Data (Delete)
|
||||||
|
const handleDelete = async (id: number, namaMK: string) => {
|
||||||
|
if (!window.confirm(`Apakah Anda yakin ingin menghapus jadwal mata kuliah ${namaMK}?`)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
await axios.delete(`http://172.17.172.17:8080/api/schedules/${id}`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
alert("Jadwal berhasil dihapus!");
|
||||||
|
fetchSchedules();
|
||||||
|
} catch (err: any) {
|
||||||
|
alert("Gagal menghapus jadwal.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getMatkulDiSlot = (hari: string, jam: string) => {
|
const getMatkulDiSlot = (hari: string, jam: string) => {
|
||||||
return schedules.find((s) => {
|
return schedules.find((s) => {
|
||||||
if (s.hari !== hari) return false;
|
if (s.hari !== hari) return false;
|
||||||
@@ -53,12 +152,20 @@ export default function SchedulesPage() {
|
|||||||
<CalendarDays size={28} />
|
<CalendarDays size={28} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold text-gray-800">Jadwal Kuliah Statis</h2>
|
<h2 className="text-2xl font-bold text-gray-800">Edit Jadwal</h2>
|
||||||
<p className="text-gray-500 text-sm mt-1">Pantau kepadatan ruang kelas D101 dan kelola jadwal otomatis.</p>
|
<p className="text-gray-500 text-sm mt-1">Pantau dan Kelola Jadwal Ruangan.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
{/* Tombol Tambah Jadwal */}
|
||||||
|
<button
|
||||||
|
onClick={handleAddClick}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-bold hover:bg-blue-700 transition-all shadow-sm"
|
||||||
|
>
|
||||||
|
<Plus size={16} /> Tambah Jadwal
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Toggle View Mode */}
|
{/* Toggle View Mode */}
|
||||||
<div className="bg-gray-100 p-1 rounded-lg flex items-center border border-gray-200">
|
<div className="bg-gray-100 p-1 rounded-lg flex items-center border border-gray-200">
|
||||||
<button
|
<button
|
||||||
@@ -74,7 +181,6 @@ export default function SchedulesPage() {
|
|||||||
<Calendar size={14} /> Mode Kalender
|
<Calendar size={14} /> Mode Kalender
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -88,8 +194,8 @@ export default function SchedulesPage() {
|
|||||||
<th className="p-4">Kode MK</th>
|
<th className="p-4">Kode MK</th>
|
||||||
<th className="p-4">Mata Kuliah</th>
|
<th className="p-4">Mata Kuliah</th>
|
||||||
<th className="p-4">Ruangan</th>
|
<th className="p-4">Ruangan</th>
|
||||||
<th className="p-4">Hari</th>
|
<th className="p-4">Hari & Waktu</th>
|
||||||
<th className="p-4">Waktu</th>
|
<th className="p-4">SKS</th>
|
||||||
<th className="p-4 text-center">Aksi</th>
|
<th className="p-4 text-center">Aksi</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -97,24 +203,38 @@ export default function SchedulesPage() {
|
|||||||
{schedules.length > 0 ? (
|
{schedules.length > 0 ? (
|
||||||
schedules.map((sched) => (
|
schedules.map((sched) => (
|
||||||
<tr key={sched.schedule_id} className="hover:bg-blue-50/30 transition-colors">
|
<tr key={sched.schedule_id} className="hover:bg-blue-50/30 transition-colors">
|
||||||
<td className="p-4 font-bold text-gray-800 text-sm">{sched.kode_mk}</td>
|
<td className="p-4 font-bold text-blue-700 text-sm">{sched.kode_mk}</td>
|
||||||
<td className="p-4 text-sm font-semibold text-gray-600">{sched.nama_mk}</td>
|
<td className="p-4 text-sm font-semibold text-gray-800">{sched.nama_mk}</td>
|
||||||
<td className="p-4 text-sm font-bold text-blue-600 flex items-center gap-1.5 mt-2">
|
<td className="p-4 text-sm font-bold text-gray-600 flex items-center gap-1.5 mt-2">
|
||||||
<MapPin size={14} /> Kelas D101
|
<MapPin size={14} className="text-blue-500" /> Kelas D101
|
||||||
</td>
|
|
||||||
<td className="p-4 text-sm font-bold text-gray-700">
|
|
||||||
<span className="bg-gray-100 px-2 py-1 rounded-md">{sched.hari}</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4 text-sm text-gray-600">
|
<td className="p-4 text-sm text-gray-600">
|
||||||
<div className="flex items-center gap-1.5 font-medium">
|
<div className="font-bold text-gray-700">{sched.hari}</div>
|
||||||
<Clock size={14} className="text-gray-400" />
|
<div className="flex items-center gap-1.5 font-medium text-xs mt-0.5">
|
||||||
|
<Clock size={12} className="text-gray-400" />
|
||||||
{sched.jam_mulai.substring(0, 5)} - {sched.jam_selesai.substring(0, 5)}
|
{sched.jam_mulai.substring(0, 5)} - {sched.jam_selesai.substring(0, 5)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4 text-center">
|
<td className="p-4 text-sm font-bold text-gray-600 text-center">
|
||||||
<button className="p-2 bg-red-50 text-red-500 rounded-lg hover:bg-red-100 transition-colors">
|
<span className="bg-gray-100 px-2 py-1 rounded-md border">{sched.jumlah_sks}</span>
|
||||||
<Trash2 size={16} />
|
</td>
|
||||||
</button>
|
<td className="p-4">
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleEditClick(sched)}
|
||||||
|
className="p-2 bg-yellow-50 text-yellow-600 rounded-lg hover:bg-yellow-100 transition-colors"
|
||||||
|
title="Edit Jadwal"
|
||||||
|
>
|
||||||
|
<Edit size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(sched.schedule_id, sched.nama_mk)}
|
||||||
|
className="p-2 bg-red-50 text-red-500 rounded-lg hover:bg-red-100 transition-colors"
|
||||||
|
title="Hapus Jadwal"
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
@@ -149,7 +269,11 @@ export default function SchedulesPage() {
|
|||||||
return (
|
return (
|
||||||
<div key={hari} className="h-full">
|
<div key={hari} className="h-full">
|
||||||
{mtk ? (
|
{mtk ? (
|
||||||
<div className="bg-blue-50 border-l-4 border-blue-600 p-2 rounded text-left shadow-xs h-full flex flex-col justify-between">
|
<div
|
||||||
|
onClick={() => handleEditClick(mtk)}
|
||||||
|
className="bg-blue-50 border-l-4 border-blue-600 p-2 rounded text-left shadow-xs h-full flex flex-col justify-between cursor-pointer hover:bg-blue-100 transition-colors"
|
||||||
|
title="Klik untuk Edit"
|
||||||
|
>
|
||||||
<span className="text-xs font-bold text-blue-800 block truncate">{mtk.nama_mk}</span>
|
<span className="text-xs font-bold text-blue-800 block truncate">{mtk.nama_mk}</span>
|
||||||
<span className="text-[10px] font-semibold text-blue-500">{mtk.kode_mk}</span>
|
<span className="text-[10px] font-semibold text-blue-500">{mtk.kode_mk}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,7 +290,82 @@ export default function SchedulesPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ==================================== MODAL FORM CRUD ==================================== */}
|
||||||
|
{isModalOpen && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
|
||||||
|
<div className="bg-white rounded-2xl w-full max-w-lg shadow-xl overflow-hidden">
|
||||||
|
<div className="flex justify-between items-center p-5 border-b border-gray-100 bg-gray-50/50">
|
||||||
|
<h3 className="font-bold text-lg text-gray-800">
|
||||||
|
{formData.schedule_id ? "Edit Jadwal Kuliah" : "Tambah Jadwal Kuliah Baru"}
|
||||||
|
</h3>
|
||||||
|
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500">
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold text-gray-600 mb-1">Kode MK</label>
|
||||||
|
<input type="text" required value={formData.kode_mk} onChange={(e) => setFormData({...formData, kode_mk: e.target.value.toUpperCase()})}
|
||||||
|
className="w-full border border-gray-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none uppercase"
|
||||||
|
placeholder="Contoh: EE153"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold text-gray-600 mb-1">Jumlah SKS</label>
|
||||||
|
<input type="number" min="1" max="6" required value={formData.jumlah_sks} onChange={(e) => setFormData({...formData, jumlah_sks: Number(e.target.value)})}
|
||||||
|
className="w-full border border-gray-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold text-gray-600 mb-1">Nama Mata Kuliah</label>
|
||||||
|
<input type="text" required value={formData.nama_mk} onChange={(e) => setFormData({...formData, nama_mk: e.target.value})}
|
||||||
|
className="w-full border border-gray-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
|
||||||
|
placeholder="Contoh: Elektronika dan Sistem Digital"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold text-gray-600 mb-1">Hari Pelaksanaan</label>
|
||||||
|
<select value={formData.hari} onChange={(e) => setFormData({...formData, hari: e.target.value})}
|
||||||
|
className="w-full border border-gray-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none bg-white">
|
||||||
|
{hariUrutan.map(h => <option key={h} value={h}>{h}</option>)}
|
||||||
|
<option value="Sabtu">Sabtu</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold text-gray-600 mb-1">Jam Mulai</label>
|
||||||
|
<input type="time" required value={formData.jam_mulai} onChange={(e) => setFormData({...formData, jam_mulai: e.target.value})}
|
||||||
|
className="w-full border border-gray-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold text-gray-600 mb-1">Jam Selesai</label>
|
||||||
|
<input type="time" required value={formData.jam_selesai} onChange={(e) => setFormData({...formData, jam_selesai: e.target.value})}
|
||||||
|
className="w-full border border-gray-300 rounded-lg p-2.5 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 flex gap-3">
|
||||||
|
<button type="button" onClick={() => setIsModalOpen(false)} className="flex-1 py-2.5 rounded-lg text-sm font-bold text-gray-600 bg-gray-100 hover:bg-gray-200 transition-colors">
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button type="submit" disabled={isSaving} className={`flex-1 py-2.5 rounded-lg text-sm font-bold text-white transition-colors flex items-center justify-center gap-2 ${isSaving ? 'bg-blue-400' : 'bg-blue-600 hover:bg-blue-700'}`}>
|
||||||
|
<Save size={16} /> {isSaving ? "Menyimpan..." : "Simpan Jadwal"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -92,11 +92,23 @@ export default function AddBookingPage() {
|
|||||||
onChange={(e) => setSelectedRoomId(e.target.value)}
|
onChange={(e) => setSelectedRoomId(e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="" disabled>-- Pilih Ruangan yang Tersedia --</option>
|
<option value="" disabled>-- Pilih Ruangan yang Tersedia --</option>
|
||||||
{rooms.map(room => (
|
|
||||||
<option key={room.room_id} value={room.room_id}>
|
{/* PERBAIKAN: Sortir Ruangan & Disable yang berstatus Maintenance */}
|
||||||
{room.name} (Kapasitas: {room.capacity} | {room.floor})
|
{[...rooms]
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.map((room) => (
|
||||||
|
<option
|
||||||
|
key={room.room_id}
|
||||||
|
value={room.room_id}
|
||||||
|
disabled={room.status === 'Maintenance'}
|
||||||
|
className={room.status === 'Maintenance' ? 'text-gray-400 bg-gray-100' : 'text-gray-900'}
|
||||||
|
>
|
||||||
|
{room.name} {room.status === 'Maintenance'
|
||||||
|
? '[TIDAK TERSEDIA - SEDANG PERAWATAN]'
|
||||||
|
: `(Kapasitas: ${room.capacity} | ${room.floor})`}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export default function Dashboard() {
|
|||||||
className="flex items-center gap-2 bg-white border border-gray-300 text-gray-700 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-300 px-4 py-2 rounded-lg text-sm font-medium shadow-sm transition-all"
|
className="flex items-center gap-2 bg-white border border-gray-300 text-gray-700 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-300 px-4 py-2 rounded-lg text-sm font-medium shadow-sm transition-all"
|
||||||
>
|
>
|
||||||
<Clock size={18} />
|
<Clock size={18} />
|
||||||
Lihat Riwayat Booking
|
Riwayat Peminjaman
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user