Compare commits
11
Commits
3c0adda955
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
387b098aad | ||
|
|
50795d806e | ||
|
|
122115b5e9 | ||
|
|
e210d2a424 | ||
|
|
29ea2c6589 | ||
|
|
86682ce7c3 | ||
|
|
427ce3d5b3 | ||
|
|
9141b48056 | ||
|
|
4d6e825f16 | ||
|
|
102ac2c50a | ||
|
|
241d228e9c |
+13
-14
@@ -14,12 +14,12 @@ func main() {
|
||||
// 1. Konek Database
|
||||
config.ConnectDatabase()
|
||||
|
||||
// 2. AutoMigrate (Tambahkan ClassSchedule di sini agar dikenali GORM)
|
||||
// 2. AutoMigrate
|
||||
config.DB.AutoMigrate(&models.User{}, &models.Room{}, &models.Booking{}, &models.ClassSchedule{})
|
||||
|
||||
r := gin.Default()
|
||||
|
||||
// 3. CORS Middleware (Agar Frontend bisa masuk)
|
||||
// 3. CORS Middleware (Panggil fungsinya di sini)
|
||||
r.Use(CORSMiddleware())
|
||||
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
@@ -58,35 +58,34 @@ func main() {
|
||||
// Admin (Manage Rooms)
|
||||
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.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)
|
||||
|
||||
// RUTE BARU: Untuk menerima ketukan pintu dari Hardware ESP32
|
||||
r.POST("/api/hardware/verify", controllers.VerifyHardwareCode)
|
||||
|
||||
// PASTIKAN BARIS INI ADA DI SINI:
|
||||
r.POST("/api/hardware/control", controllers.ControlHardware)
|
||||
|
||||
r.GET("/api/hardware/status", controllers.GetHardwareStatus)
|
||||
|
||||
r.GET("/api/hardware/power-status", controllers.GetPowerStatus)
|
||||
|
||||
r.POST("/api/power/global", controllers.GlobalPowerControl)
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
|
||||
// ... (fungsi CORSMiddleware tetap sama) ...
|
||||
|
||||
// =========================================================================
|
||||
// FUNGSI MIDDLEWARE HARUS DITULIS DI LUAR FUNGSI MAIN
|
||||
// =========================================================================
|
||||
func CORSMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
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-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" {
|
||||
c.AbortWithStatus(204)
|
||||
|
||||
@@ -45,7 +45,7 @@ func CreateBooking(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 🌟 PERBAIKAN 1: Ambil Role User dari Token
|
||||
// 🌟 Ambil Role User dari Token
|
||||
userRoleInterface, _ := c.Get("role")
|
||||
userRole, _ := userRoleInterface.(string)
|
||||
|
||||
@@ -55,6 +55,21 @@ func CreateBooking(c *gin.Context) {
|
||||
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(
|
||||
@@ -67,7 +82,7 @@ func CreateBooking(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 🌟 PERBAIKAN 1 Lanjutan: Penentuan Status Otomatis
|
||||
// Penentuan Status Otomatis
|
||||
statusPeminjaman := "Pending" // Default untuk student
|
||||
if userRole == "lecturer" {
|
||||
statusPeminjaman = "Approved" // Dosen otomatis disetujui!
|
||||
@@ -80,7 +95,7 @@ func CreateBooking(c *gin.Context) {
|
||||
StartTime: input.StartTime,
|
||||
EndTime: input.EndTime,
|
||||
Purpose: input.Purpose,
|
||||
Status: statusPeminjaman, // <-- Gunakan variabel statusPeminjaman
|
||||
Status: statusPeminjaman,
|
||||
}
|
||||
|
||||
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) {
|
||||
var bookings []models.Booking
|
||||
|
||||
@@ -150,7 +165,6 @@ func UpdateBookingStatus(c *gin.Context) {
|
||||
|
||||
booking.Status = input.Status
|
||||
|
||||
// 🌟 PERBAIKAN 2: Menghapus blok Save dan JSON ganda yang bikin error
|
||||
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()})
|
||||
|
||||
@@ -42,7 +42,7 @@ var DeviceStatusCache = map[string]string{
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// FUNGSI 1: VERIFIKASI TOKEN & KODE MATA KULIAH (ON-DEMAND)
|
||||
// FUNGSI 1: VERIFIKASI TOKEN & KODE MATA KULIAH (DOUBLE-LAYER PROTECTION)
|
||||
// =========================================================================
|
||||
func VerifyHardwareCode(c *gin.Context) {
|
||||
var req VerifyRequest
|
||||
@@ -63,11 +63,11 @@ func VerifyHardwareCode(c *gin.Context) {
|
||||
// =========================================================
|
||||
if tokenInput == "CS2026" {
|
||||
fmt.Printf("[VERIFY] Master Token CS digunakan\n")
|
||||
sisaMenit = 120
|
||||
sisaMenit = 60
|
||||
isTokenValid = true
|
||||
} else if tokenInput == "ADM999" {
|
||||
fmt.Printf("[VERIFY] Master Token Admin digunakan\n")
|
||||
sisaMenit = 720 // 24 Jam
|
||||
sisaMenit = 1440 // 12 jam
|
||||
isTokenValid = true
|
||||
} else {
|
||||
// =========================================================
|
||||
@@ -77,15 +77,14 @@ func VerifyHardwareCode(c *gin.Context) {
|
||||
errBooking := config.DB.First(&booking, "redeem_code = ? AND status = 'Approved'", tokenInput).Error
|
||||
|
||||
if errBooking == nil {
|
||||
// Cek apakah hari ini (tanggal) sama dengan hari booking
|
||||
if booking.StartTime.Format("2006-01-02") != sekarang.Format("2006-01-02") {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Peminjaman bukan untuk hari ini"})
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Peminjaman bukan untuk hari ini!"})
|
||||
return
|
||||
}
|
||||
|
||||
toleransiMasuk := booking.StartTime.Add(-15 * time.Minute)
|
||||
if sekarang.Before(toleransiMasuk) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Belum waktunya. Bisa masuk 15 mnt sblm jam peminjaman!"})
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Belum waktunya. Bisa masuk 15 menit sebelum kelas!"})
|
||||
return
|
||||
}
|
||||
if sekarang.After(booking.EndTime) {
|
||||
@@ -101,40 +100,74 @@ func VerifyHardwareCode(c *gin.Context) {
|
||||
|
||||
} else {
|
||||
// =========================================================
|
||||
// 3. CEK KODE MATA KULIAH (ON-DEMAND FLEKSIBEL)
|
||||
// 3. CEK KODE MATA KULIAH DENGAN DOUBLE-LAYER PROTECTION
|
||||
// =========================================================
|
||||
var schedule models.ClassSchedule
|
||||
// Abaikan hari dan jam, cukup cek apakah kode matkul eksis di database
|
||||
errSchedule := config.DB.First(&schedule, "kode_mk = ?", tokenInput).Error
|
||||
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 {
|
||||
// KODE MATKUL EKSIS.
|
||||
// CEK BENTROK: Apakah ada orang yang SEDANG booking via web sekarang?
|
||||
// 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 >= ?",
|
||||
schedule.RoomID, sekarang, sekarang,
|
||||
reqSchedule.RoomID, sekarang, sekarang,
|
||||
).Count(&countBookingAktif)
|
||||
|
||||
if countBookingAktif > 0 {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "Ruangan sedang dipakai peminjam resmi (Web)!"})
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "Akses Ditolak: Ruangan sedang dipakai peminjam resmi (Web)!"})
|
||||
return
|
||||
}
|
||||
|
||||
// Jika aman (tidak ada yang booking), berikan akses 120 Menit!
|
||||
sisaMenit = 120
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
if !isTokenValid {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token atau Kode MK tidak ditemukan / salah!"})
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token Web atau Kode MK tidak ditemukan!"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// 4. JIKA VALID (Master / Matkul / Web), BUKA GEMBOK VIA MQTT
|
||||
// 4. JIKA VALID, BUKA GEMBOK VIA MQTT & HTTP LOKAL
|
||||
// =========================================================
|
||||
DeviceStatusCache["lampu1"] = "on"
|
||||
DeviceStatusCache["lampu2"] = "on"
|
||||
@@ -152,6 +185,7 @@ func VerifyHardwareCode(c *gin.Context) {
|
||||
client := mqtt.NewClient(opts)
|
||||
if tokenMQTT := client.Connect(); tokenMQTT.Wait() && tokenMQTT.Error() == nil {
|
||||
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/lampu2", 0, false, "on").Wait()
|
||||
client.Disconnect(250)
|
||||
@@ -360,14 +394,10 @@ func GlobalPowerControl(c *gin.Context) {
|
||||
}
|
||||
apiURL := fmt.Sprintf("%s/api/services/switch/%s", haURL, service)
|
||||
|
||||
mcbs := []string{
|
||||
"switch.wifi_smart_meter_pro_switch", // Umum
|
||||
"switch.wifi_smart_meter_pro_2_switch", // AC1
|
||||
"switch.wifi_smart_meter_pro_3_switch", // AC2
|
||||
}
|
||||
// 🌟 PERBAIKAN: Hapus array dan loop, cukup gunakan 1 target saja
|
||||
targetSwitch := "switch.wifi_smart_meter_pro_switch"
|
||||
|
||||
for i, mcb := range mcbs {
|
||||
payload := map[string]string{"entity_id": mcb}
|
||||
payload := map[string]string{"entity_id": targetSwitch}
|
||||
jsonPayload, _ := json.Marshal(payload)
|
||||
|
||||
reqHA, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonPayload))
|
||||
@@ -382,13 +412,9 @@ func GlobalPowerControl(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[MCB] Sinyal %s dikirim ke %s\n", req.Action, mcb)
|
||||
|
||||
if i < len(mcbs)-1 {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
fmt.Printf("[MCB] Sinyal %s dikirim ke %s\n", req.Action, targetSwitch)
|
||||
|
||||
// Mematikan status virtual di dashboard
|
||||
if req.Action == "off" {
|
||||
DeviceStatusCache["lampu1"] = "off"
|
||||
DeviceStatusCache["lampu2"] = "off"
|
||||
@@ -398,7 +424,7 @@ func GlobalPowerControl(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"message": fmt.Sprintf("Seluruh daya ruangan berhasil di-%s secara berurutan", req.Action),
|
||||
"message": fmt.Sprintf("Daya utama berhasil di-%s", req.Action),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2,15 +2,26 @@ package controllers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
// UBAH DUA BARIS INI MENJADI s-class-backend
|
||||
"s-class-backend/config"
|
||||
"s-class-backend/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
var schedules []models.ClassSchedule
|
||||
|
||||
@@ -20,8 +31,100 @@ func GetSchedules(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Kirim data ke frontend (React)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"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"`
|
||||
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"`
|
||||
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'}`}>
|
||||
<CalendarDays size={22} />
|
||||
</div>
|
||||
<span>Jadwal Kuliah</span>
|
||||
<span>Edit Jadwal</span>
|
||||
</Link>
|
||||
|
||||
</nav>
|
||||
|
||||
@@ -115,13 +115,11 @@ export default function PowerMonitoringPage() {
|
||||
if (!window.confirm(confirmMsg)) return;
|
||||
}
|
||||
|
||||
// Jika yang ditekan bukan D101, ubah UI saja (simulasi)
|
||||
if (roomName !== "Kelas D101") {
|
||||
setRoomDeviceStatus(prev => ({ ...prev, [roomIdKey]: { ...prev[roomIdKey], [deviceName]: !currentStatus } }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Logika khusus D101 (Tembak ke Golang)
|
||||
let backendDevice = "";
|
||||
if (deviceName === 'AC') backendDevice = "ac";
|
||||
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) => {
|
||||
if (roomName === "Kelas D101") {
|
||||
const actionType = currentRelayStatus ? "off" : "on";
|
||||
const confirmMessage = currentRelayStatus
|
||||
? `PERINGATAN FATAL: Anda yakin ingin MEMUTUS 3 MCB Daya di ${roomName}? (Proses berurutan ~3 detik)`
|
||||
: `RESTORE POWER: Anda yakin ingin MENYALAKAN KEMBALI 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 MCB Daya di ${roomName}?`;
|
||||
|
||||
if (!window.confirm(confirmMessage)) return;
|
||||
|
||||
// Ubah UI seketika
|
||||
setRooms(prev => prev.map(r => r.name === roomName ? { ...r, isRelayOn: !currentRelayStatus } : r));
|
||||
|
||||
try {
|
||||
@@ -172,7 +168,6 @@ export default function PowerMonitoringPage() {
|
||||
fetchPowerStatus();
|
||||
fetchDeviceStatus();
|
||||
} else {
|
||||
// Rollback UI jika gagal
|
||||
setRooms(prev => prev.map(r => r.name === roomName ? { ...r, isRelayOn: currentRelayStatus } : r));
|
||||
alert("Gagal mengontrol MCB utama. Cek koneksi server.");
|
||||
}
|
||||
@@ -181,7 +176,6 @@ export default function PowerMonitoringPage() {
|
||||
alert("Terjadi kesalahan jaringan saat menghubungi server.");
|
||||
}
|
||||
} else {
|
||||
// Dummy untuk kelas lain
|
||||
const actionType = currentRelayStatus ? "Mematikan" : "Menyalakan";
|
||||
if (window.confirm(`Simulasi ${actionType} daya di ${roomName}?`)) {
|
||||
setRooms(prev => prev.map(r => r.name === roomName ? { ...r, isRelayOn: !currentRelayStatus, power: 0 } : r));
|
||||
|
||||
+62
-16
@@ -9,13 +9,46 @@ export default function AdminDashboard() {
|
||||
const [roomStats, setRoomStats] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// STATE BARU: Khusus menyimpan TOTAL DAYA D101
|
||||
const [d101TotalPower, setD101TotalPower] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAdminData();
|
||||
// Refresh data konsumsi daya setiap 10 detik agar terlihat real-time
|
||||
const interval = setInterval(fetchAdminData, 10000);
|
||||
return () => clearInterval(interval);
|
||||
fetchD101Power(); // Panggil fungsi daya saat pertama kali render
|
||||
|
||||
// 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 () => {
|
||||
try {
|
||||
const token = localStorage.getItem("token");
|
||||
@@ -37,7 +70,7 @@ export default function AdminDashboard() {
|
||||
|
||||
const roomsWithDummyPower = (roomRes.data.data || []).map((room: any) => ({
|
||||
...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)
|
||||
}));
|
||||
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) => {
|
||||
try {
|
||||
const token = localStorage.getItem("token");
|
||||
@@ -76,22 +108,39 @@ export default function AdminDashboard() {
|
||||
<Activity className="text-blue-500" /> Real-time Room Energy
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* TAMBAHKAN PENGURUTAN ABJAD DI SINI */}
|
||||
{[...roomStats]
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((room) => (
|
||||
<div key={room.room_id} className="bg-white p-5 rounded-xl border border-gray-100 shadow-sm">
|
||||
.map((room) => {
|
||||
|
||||
// LOGIKA INJEKSI: Timpa nilai dummy dengan daya asli jika ini Kelas D101
|
||||
const isD101 = room.name === "Kelas D101";
|
||||
const displayedPower = isD101 ? d101TotalPower : room.power_consumption;
|
||||
const isPowerActive = displayedPower > 0;
|
||||
|
||||
return (
|
||||
<div key={room.room_id} className={`bg-white p-5 rounded-xl border shadow-sm transition-all
|
||||
${isD101 ? 'border-blue-200 border-l-4 border-l-blue-500 bg-blue-50/10' : 'border-gray-100'}`}>
|
||||
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<span className="text-sm font-bold text-gray-400 uppercase tracking-tight">{room.name}</span>
|
||||
<div className={`h-3 w-3 rounded-full ${room.power_consumption > 5 ? 'bg-orange-500 animate-pulse' : 'bg-gray-300'}`} />
|
||||
<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 text-gray-800">{room.power_consumption || 0}</span>
|
||||
<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">Last updated: Just now</p>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -116,12 +165,10 @@ export default function AdminDashboard() {
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-50">
|
||||
{pendingBookings.map((b) => (
|
||||
// PERBAIKAN 2: Gunakan booking_id sebagai key
|
||||
<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 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">
|
||||
<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'})}
|
||||
@@ -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">
|
||||
<div className="flex justify-center gap-2">
|
||||
{/* PERBAIKAN 4: Gunakan b.booking_id di fungsi onClick */}
|
||||
<button
|
||||
onClick={() => handleAction(b.booking_id, 'Approved')}
|
||||
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 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]
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map((room) => (
|
||||
|
||||
@@ -2,12 +2,26 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
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() {
|
||||
const [schedules, setSchedules] = useState<any[]>([]);
|
||||
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 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) => {
|
||||
return schedules.find((s) => {
|
||||
if (s.hari !== hari) return false;
|
||||
@@ -53,12 +152,20 @@ export default function SchedulesPage() {
|
||||
<CalendarDays size={28} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-800">Jadwal Kuliah Statis</h2>
|
||||
<p className="text-gray-500 text-sm mt-1">Pantau kepadatan ruang kelas D101 dan kelola jadwal otomatis.</p>
|
||||
<h2 className="text-2xl font-bold text-gray-800">Edit Jadwal</h2>
|
||||
<p className="text-gray-500 text-sm mt-1">Pantau dan Kelola Jadwal Ruangan.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 */}
|
||||
<div className="bg-gray-100 p-1 rounded-lg flex items-center border border-gray-200">
|
||||
<button
|
||||
@@ -74,7 +181,6 @@ export default function SchedulesPage() {
|
||||
<Calendar size={14} /> Mode Kalender
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -88,8 +194,8 @@ export default function SchedulesPage() {
|
||||
<th className="p-4">Kode MK</th>
|
||||
<th className="p-4">Mata Kuliah</th>
|
||||
<th className="p-4">Ruangan</th>
|
||||
<th className="p-4">Hari</th>
|
||||
<th className="p-4">Waktu</th>
|
||||
<th className="p-4">Hari & Waktu</th>
|
||||
<th className="p-4">SKS</th>
|
||||
<th className="p-4 text-center">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -97,24 +203,38 @@ export default function SchedulesPage() {
|
||||
{schedules.length > 0 ? (
|
||||
schedules.map((sched) => (
|
||||
<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 text-sm font-semibold text-gray-600">{sched.nama_mk}</td>
|
||||
<td className="p-4 text-sm font-bold text-blue-600 flex items-center gap-1.5 mt-2">
|
||||
<MapPin size={14} /> 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 className="p-4 font-bold text-blue-700 text-sm">{sched.kode_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-gray-600 flex items-center gap-1.5 mt-2">
|
||||
<MapPin size={14} className="text-blue-500" /> Kelas D101
|
||||
</td>
|
||||
<td className="p-4 text-sm text-gray-600">
|
||||
<div className="flex items-center gap-1.5 font-medium">
|
||||
<Clock size={14} className="text-gray-400" />
|
||||
<div className="font-bold text-gray-700">{sched.hari}</div>
|
||||
<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)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
<button className="p-2 bg-red-50 text-red-500 rounded-lg hover:bg-red-100 transition-colors">
|
||||
<td className="p-4 text-sm font-bold text-gray-600 text-center">
|
||||
<span className="bg-gray-100 px-2 py-1 rounded-md border">{sched.jumlah_sks}</span>
|
||||
</td>
|
||||
<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>
|
||||
</tr>
|
||||
))
|
||||
@@ -149,7 +269,11 @@ export default function SchedulesPage() {
|
||||
return (
|
||||
<div key={hari} className="h-full">
|
||||
{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-[10px] font-semibold text-blue-500">{mtk.kode_mk}</span>
|
||||
</div>
|
||||
@@ -167,6 +291,81 @@ export default function SchedulesPage() {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -92,11 +92,23 @@ export default function AddBookingPage() {
|
||||
onChange={(e) => setSelectedRoomId(e.target.value)}
|
||||
>
|
||||
<option value="" disabled>-- Pilih Ruangan yang Tersedia --</option>
|
||||
{rooms.map(room => (
|
||||
<option key={room.room_id} value={room.room_id}>
|
||||
{room.name} (Kapasitas: {room.capacity} | {room.floor})
|
||||
|
||||
{/* PERBAIKAN: Sortir Ruangan & Disable yang berstatus Maintenance */}
|
||||
{[...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>
|
||||
))}
|
||||
|
||||
</select>
|
||||
</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"
|
||||
>
|
||||
<Clock size={18} />
|
||||
Lihat Riwayat Booking
|
||||
Riwayat Peminjaman
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user