diff --git a/backend/cmd/main.go b/backend/cmd/main.go index b8feca2..ba8c3c4 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -50,10 +50,16 @@ func main() { // Bookings protected.POST("/bookings", controllers.CreateBooking) - protected.GET("/bookings", controllers.GetUserBookings) - protected.PATCH("/bookings/:id", controllers.UpdateBookingStatus) + protected.GET("/bookings", controllers.GetAllBookings) + protected.PUT("/bookings/:id/status", controllers.UpdateBookingStatus) // <-- Cukup tulis satu kali saja + + // Admin (Manage Rooms) + protected.PUT("/admin/rooms/:id/status", controllers.UpdateRoomStatus) // <-- Pastikan baris ini ada } + // 5. Jalur IoT ESP32 (Di luar protected) + r.POST("/api/sensor/energy", controllers.UpdateRoomPower) + r.Run(":8080") } @@ -70,4 +76,4 @@ func CORSMiddleware() gin.HandlerFunc { } c.Next() } -} \ No newline at end of file +} diff --git a/backend/controllers/bookingcontroller.go b/backend/controllers/bookingcontroller.go index d68ce3e..929403f 100644 --- a/backend/controllers/bookingcontroller.go +++ b/backend/controllers/bookingcontroller.go @@ -110,37 +110,44 @@ type UpdateStatusInput struct { // UPDATE STATUS (ADMIN) func UpdateBookingStatus(c *gin.Context) { - bookingID := c.Param("id") + bookingID := c.Param("id") + var input UpdateStatusInput - var booking models.Booking - if err := config.DB.First(&booking, "booking_id = ?", bookingID).Error; err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "Booking tidak ditemukan"}) - return - } + // 1. Validasi Input JSON + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Format data tidak valid"}) + return + } - var input UpdateStatusInput - if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } + var booking models.Booking + // 2. Gunakan "id = ?" jika primary key di DB adalah 'id' + // Jika di model kamu pakai 'RoomID', pastikan konsisten + if err := config.DB.Where("id = ?", bookingID).First(&booking).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Data booking tidak ditemukan di database"}) + return + } - booking.Status = input.Status + // 3. Update Status + booking.Status = input.Status - //REDEEM CODE - if input.Status == "Approved" { - if booking.RedeemCode == "" { - booking.RedeemCode = helpers.GenerateRedeemCode() - } - } else if input.Status == "Rejected" || input.Status == "Cancelled" { - booking.RedeemCode = "" - } + // REVISI CLEAN CODE: Menggunakan Switch + switch input.Status { + case "Approved": + if booking.RedeemCode == "" { + booking.RedeemCode = helpers.GenerateRedeemCode() + } + case "Rejected", "Cancelled": + booking.RedeemCode = "" + } + if err := config.DB.Save(&booking).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Gagal menyimpan perubahan ke database"}) + return + } - config.DB.Save(&booking) - - c.JSON(http.StatusOK, gin.H{ - "message": "Status booking berhasil diperbarui!", - "data": booking, - }) + c.JSON(http.StatusOK, gin.H{ + "message": "Status berhasil diperbarui!", + "data": booking, + }) } // INPUT UNTUK ESP3 @@ -174,3 +181,22 @@ func VerifyRedeemCode(c *gin.Context) { "status": "success", }) } + +// GET ALL BOOKINGS (Untuk Calendar View semua user) +func GetAllBookings(c *gin.Context) { + var bookings []models.Booking + + // Preload digunakan untuk mengambil data Relasi (Nama Ruangan & Nama Peminjam) + if err := config.DB.Preload("Room").Preload("User").Order("created_at desc").Find(&bookings).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Perhatikan: responsenya menggunakan "data: bookings" agar cocok dengan Frontend React-mu + c.JSON(http.StatusOK, gin.H{ + "status": "success", + "data": bookings, + }) +} + + diff --git a/backend/controllers/roomcontroller.go b/backend/controllers/roomcontroller.go index 537296e..7b95c4a 100644 --- a/backend/controllers/roomcontroller.go +++ b/backend/controllers/roomcontroller.go @@ -30,4 +30,69 @@ func CreateRoom(c *gin.Context) { return } c.JSON(http.StatusOK, gin.H{"message": "Ruangan berhasil ditambahkan!", "data": input}) -} \ No newline at end of file +} + +// --- STRUKTUR DATA UNTUK MENERIMA JSON --- + +type UpdateRoomStatusInput struct { + Status string `json:"status" binding:"required"` +} + +type EnergySensorInput struct { + RoomID int `json:"room_id" binding:"required"` + Power float64 `json:"power" binding:"required"` +} + +// --- FUNGSI HANDLER --- + +// UPDATE ROOM STATUS (Admin: Available <-> Maintenance) +func UpdateRoomStatus(c *gin.Context) { + roomID := c.Param("id") + var input UpdateRoomStatusInput + + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + var room models.Room + // KUNCI PERBAIKANNYA: Cukup masukkan `roomID` langsung. + // GORM akan otomatis mencari berdasarkan Primary Key tanpa peduli nama kolomnya. + if err := config.DB.First(&room, roomID).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Ruangan tidak ditemukan di tabel"}) + return + } + + // Ubah status dan simpan + room.Status = input.Status + if err := config.DB.Save(&room).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Gagal menyimpan perubahan"}) + return + } + c.JSON(http.StatusOK, gin.H{ + "message": "Status ruangan berhasil diperbarui", + "data": room, + }) +} + +// UPDATE ROOM POWER (Menerima data Watt dari ESP32 / Tuya) +func UpdateRoomPower(c *gin.Context) { + var input EnergySensorInput + + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Format data sensor salah"}) + return + } + + var room models.Room + if err := config.DB.First(&room, "room_id = ?", input.RoomID).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Ruangan tidak ditemukan"}) + return + } + + // Pastikan kolom ini sesuai dengan nama di models.Room milikmu (misal: PowerConsumption) + room.PowerConsumption = input.Power + config.DB.Save(&room) + + c.JSON(http.StatusOK, gin.H{"message": "Data daya ESP32 berhasil disimpan"}) +} diff --git a/backend/models/entity.go b/backend/models/entity.go index 120fd9c..399f5aa 100644 --- a/backend/models/entity.go +++ b/backend/models/entity.go @@ -25,6 +25,7 @@ type Room struct { Capacity int `gorm:"not null" json:"capacity"` Floor string `gorm:"not null" json:"floor"` Status string `gorm:"type:room_status;default:'Available'" json:"status"` + PowerConsumption float64 `json:"power_consumption" gorm:"default:0"` } type Booking struct { diff --git a/frontend/app/admin/approvals/page.tsx b/frontend/app/admin/approvals/page.tsx new file mode 100644 index 0000000..2fbbd25 --- /dev/null +++ b/frontend/app/admin/approvals/page.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { ShieldCheck, Check, X, Clock } from "lucide-react"; + +export default function ApprovalsPage() { + const [activeTab, setActiveTab] = useState("Pending"); + const [bookings, setBookings] = useState([]); + + useEffect(() => { + // Data Dummy Sementara + setBookings([ + { id: 1, user: { full_name: "Andreas Budi" }, room: { name: "Kelas D101" }, start_time: new Date().toISOString(), purpose: "Rapat Evaluasi BEM", status: "Pending" }, + { id: 2, user: { full_name: "Siska Saraswati" }, room: { name: "Kelas D105" }, start_time: new Date().toISOString(), purpose: "Sidang Skripsi", status: "Pending" }, + { id: 3, user: { full_name: "Bima Arya" }, room: { name: "Kelas D102" }, start_time: new Date().toISOString(), purpose: "Kelas Pengganti", status: "Approved" }, + { id: 4, user: { full_name: "Citra Kirana" }, room: { name: "Kelas D104" }, start_time: new Date().toISOString(), purpose: "Latihan Band", status: "Rejected" }, + ]); + }, []); + + const filteredBookings = bookings.filter(b => b.status === activeTab); + + return ( +
+
+
+ +
+
+

Manajemen Persetujuan

+

Kelola permohonan peminjaman ruangan S-CLASS.

+
+
+ + {/* TABS */} +
+ {["Pending", "Approved", "Rejected"].map((tab) => ( + + ))} +
+ + {/* TABEL */} +
+
+ + + + + + + + + + + + {filteredBookings.map((b) => ( + + + + + + + + ))} + {filteredBookings.length === 0 && ( + + )} + +
PeminjamRuanganWaktu MulaiKeperluanStatus / Aksi
{b.user?.full_name}{b.room?.name}{new Date(b.start_time).toLocaleString('id-ID')}{b.purpose} + {b.status === "Pending" ? ( +
+ + +
+ ) : ( + + {b.status} + + )} +
Tidak ada data di kategori ini.
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/app/admin/layout.tsx b/frontend/app/admin/layout.tsx new file mode 100644 index 0000000..2186d6b --- /dev/null +++ b/frontend/app/admin/layout.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter, usePathname } from "next/navigation"; +import Link from "next/link"; +import { + LogOut, + LayoutDashboard, + Activity, + ShieldCheck, + Settings2 +} from "lucide-react"; + +export default function AdminLayout({ children }: { children: React.ReactNode }) { + const router = useRouter(); + const pathname = usePathname(); + const [user, setUser] = useState(null); + + // State isSidebarOpen sudah kita buang sepenuhnya + + useEffect(() => { + const userData = localStorage.getItem("user"); + if (userData) { + const parsed = JSON.parse(userData); + if (parsed.role !== 'admin') { + router.push("/dashboard"); + } else { + setUser(parsed); + } + } else { + router.push("/login"); + } + }, [router]); + + const handleLogout = () => { + localStorage.clear(); + router.push("/login"); + }; + + return ( +
+ + {/* HEADER ADMIN */} +
+
+

Admin Panel,

+

S-CLASS

+
+
+
+
+ {user?.full_name ? user.full_name.charAt(0).toUpperCase() : "A"} +
+
+ {user?.full_name || "Administrator"} + System Controller +
+
+ +
+
+ +
+ + {/* SIDEBAR ADMIN (Lebar Permanen: w-64) */} + + + {/* KONTEN UTAMA */} +
+ {children} +
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/app/admin/monitoring/page.tsx b/frontend/app/admin/monitoring/page.tsx new file mode 100644 index 0000000..798b8f0 --- /dev/null +++ b/frontend/app/admin/monitoring/page.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Activity, Power, ZapOff, AlertTriangle } from "lucide-react"; + +export default function PowerMonitoringPage() { + const [rooms, setRooms] = useState([]); + + useEffect(() => { + // Simulasi Data Ruangan dari IoT + const dummyRooms = [ + { id: 1, name: "Kelas D101", power: 1250, isRelayOn: true, lastUpdate: "Baru saja" }, + { id: 2, name: "Kelas D102", power: 0, isRelayOn: false, lastUpdate: "2 mnt lalu" }, + { id: 3, name: "Kelas D103", power: 45, isRelayOn: true, lastUpdate: "Baru saja" }, + { id: 4, name: "Kelas D104", power: 0, isRelayOn: false, lastUpdate: "10 mnt lalu" }, + ]; + setRooms(dummyRooms); + }, []); + + const handleCutOff = (roomName: string) => { + if(confirm(`PERINGATAN: Anda yakin ingin mematikan daya secara paksa di ${roomName}?`)) { + alert(`Sinyal pemutusan daya dikirim ke Relay ${roomName}.`); + // Nanti di sini kamu pasang axios.post ke Golang -> MQTT -> ESP32 + } + }; + + return ( +
+
+
+ +
+
+

Power Monitoring & Control

+

Pantau konsumsi daya kWh meter dan kendalikan *relay* sirkuit ruangan.

+
+
+ +
+ {rooms.map((room) => ( +
+ + {/* Indikator Status di Pojok Kanan Atas */} +
+ {room.isRelayOn ? 'Sirkuit Aktif' : 'Sirkuit Terputus'} +
+ +

{room.name}

+ +
+ 1000 ? 'text-orange-500' : 'text-gray-800'}`}> + {room.power} + + Watts +
+ + {room.power > 1000 && ( +
+ Beban Tinggi Terdeteksi! +
+ )} + +
+ Update: {room.lastUpdate} + + +
+
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/app/admin/page.tsx b/frontend/app/admin/page.tsx index 71b3c39..56762f4 100644 --- a/frontend/app/admin/page.tsx +++ b/frontend/app/admin/page.tsx @@ -2,153 +2,143 @@ import { useEffect, useState } from "react"; import axios from "axios"; -import { useRouter } from "next/navigation"; -import { LogOut, CheckCircle, XCircle, Clock } from "lucide-react"; +import { Zap, Check, X, Info, Activity } from "lucide-react"; export default function AdminDashboard() { - const router = useRouter(); - const [admin, setAdmin] = useState(null); - const [bookings, setBookings] = useState([]); + const [pendingBookings, setPendingBookings] = useState([]); + const [roomStats, setRoomStats] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { - const token = localStorage.getItem("token"); - const userData = localStorage.getItem("user"); - - if (!token || !userData) { - router.push("/login"); - return; - } - - const userObj = JSON.parse(userData); - // Tendang kalau bukan admin yang masuk - if (userObj.role !== "admin") { - router.push("/dashboard"); - return; - } - - setAdmin(userObj); - fetchBookings(token); + fetchAdminData(); + // Refresh data konsumsi daya setiap 10 detik agar terlihat real-time + const interval = setInterval(fetchAdminData, 10000); + return () => clearInterval(interval); }, []); - const fetchBookings = async (token: string) => { + const fetchAdminData = async () => { try { - const response = await axios.get("http://localhost:8080/api/bookings", { - headers: { Authorization: `Bearer ${token}` }, + const token = localStorage.getItem("token"); + + // 1. Ambil SEMUA booking pakai endpoint yang sudah ada, lalu filter di Frontend + const bookRes = await axios.get("http://localhost:8080/api/bookings", { + headers: { Authorization: `Bearer ${token}` } }); - setBookings(response.data.data || []); - } catch (error) { - console.error("Gagal ambil data booking:", error); + + const allBookings = bookRes.data.data || []; + // Saring hanya yang statusnya "Pending" atau kosong + const pendingOnly = allBookings.filter((b: any) => b.status === "Pending" || !b.status); + setPendingBookings(pendingOnly); + + // 2. Ambil daftar ruangan biasa, lalu kita sisipkan "Dummy Power" untuk UI + const roomRes = await axios.get("http://localhost:8080/api/rooms", { + headers: { Authorization: `Bearer ${token}` } + }); + + const roomsWithDummyPower = (roomRes.data.data || []).map((room: any) => ({ + ...room, + // Simulasi daya acak antara 0 sampai 15 Watt untuk keperluan UI + power_consumption: Math.floor(Math.random() * 15) + })); + setRoomStats(roomsWithDummyPower); + + } catch (err) { + console.error("Gagal ambil data admin", err); } finally { setLoading(false); } }; - const handleUpdateStatus = async (bookingId: string, newStatus: string) => { - const token = localStorage.getItem("token"); +const handleAction = async (id: number, status: string) => { try { - await axios.patch( - `http://localhost:8080/api/bookings/${bookingId}`, - { status: newStatus }, + const token = localStorage.getItem("token"); + // Mengirim status 'Approved' atau 'Rejected' ke backend + await axios.put(`http://localhost:8080/api/bookings/${id}/status`, + { status: status }, { headers: { Authorization: `Bearer ${token}` } } ); - alert(`Booking berhasil di-${newStatus}!`); - fetchBookings(token as string); // Refresh data setelah update - } catch (error) { - alert("Gagal mengubah status booking."); - console.error(error); + alert(`Permintaan berhasil di-${status}`); + fetchAdminData(); // Refresh data setelah aksi + } catch (err) { + alert("Gagal memproses pendaftaran."); } }; - const handleLogout = () => { - localStorage.removeItem("token"); - localStorage.removeItem("user"); - router.push("/login"); - }; - - if (loading) return
Memuat panel admin...
; + if (loading) return
Memasuki Mode Admin...
; return ( -
- {/* Navbar Admin */} - +
- {/* Konten Utama */} -
-

Manajemen Peminjaman Ruangan

- -
- - - - + {/* SECTION 2: APPROVAL TABLE */} +
+
+

Pending Approvals

+ + {pendingBookings.length} Requests + +
+
+
Ruangan
+ + + - - + - - {bookings.length === 0 ? ( - - + + {pendingBookings.map((b) => ( + + + + + + + + ))} + {pendingBookings.length === 0 && ( + + - ) : ( - bookings.map((b) => ( - - - - - - - - - )) )}
PeminjamRuangan Waktu KeperluanStatusAksiTindakan
Belum ada pengajuan peminjaman.
{b.user?.full_name}{b.room?.name} + {new Date(b.start_time).toLocaleString('id-ID')} + {b.purpose} +
+ + +
+
Tidak ada permintaan menunggu.
{b.room?.name}ID User: {b.user_id.substring(0,8)}... -
Mulai: {new Date(b.start_time).toLocaleString('id-ID')}
-
Selesai: {new Date(b.end_time).toLocaleString('id-ID')}
-
{b.purpose} - - {b.status} - - - {b.status === 'Pending' ? ( -
- - -
- ) : ( -
- Selesai -
- )} -
-
+ ); } \ No newline at end of file diff --git a/frontend/app/admin/rooms/page.tsx b/frontend/app/admin/rooms/page.tsx new file mode 100644 index 0000000..c4b126a --- /dev/null +++ b/frontend/app/admin/rooms/page.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { useEffect, useState } from "react"; +import axios from "axios"; +import { Settings2, RefreshCcw, AlertCircle, CheckCircle2 } from "lucide-react"; + +export default function ManageRoomsPage() { + const [rooms, setRooms] = useState([]); + const [loading, setLoading] = useState(true); + + const fetchRooms = async () => { + try { + const token = localStorage.getItem("token"); + const res = await axios.get("http://localhost:8080/api/rooms", { + headers: { Authorization: `Bearer ${token}` } + }); + setRooms(res.data.data); + } catch (err: any) { + console.error("Error Detail:", err.response?.data || err.message); + alert("Error dari Server saat ambil ruangan: " + (err.response?.data?.error || err.message)); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchRooms(); + }, []); + + // FUNGSI UTAMA: Mengubah status ruangan (Available <-> Maintenance) + const toggleRoomStatus = async (roomId: number, currentStatus: string) => { + const newStatus = currentStatus === "Available" ? "Maintenance" : "Available"; + + try { + const token = localStorage.getItem("token"); + await axios.put(`http://localhost:8080/api/admin/rooms/${roomId}/status`, + { status: newStatus }, + { headers: { Authorization: `Bearer ${token}` } } + ); + + alert(`Status Ruangan berhasil diubah menjadi ${newStatus}`); + fetchRooms(); // Refresh data agar UI terupdate + } catch (err: any) { + console.error("Gagal update:", err.response?.data || err.message); + alert("Gagal memperbarui status ruangan: " + (err.response?.data?.error || err.message)); + } + }; + + if (loading) return
Menghubungkan ke Database S-CLASS...
; + + return ( +
+
+
+
+ +
+
+

Manajemen Ruangan

+

Atur ketersediaan operasional setiap ruangan di Gedung D.

+
+
+ +
+ +
+ {rooms.map((room) => ( +
+ +
+
+ {room.category} • {room.floor} +
+

{room.name}

+ +
+ {room.status === 'Available' ? : } + {room.status} +
+
+ +
+ + ID: {room.room_id} +
+
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/app/dashboard/bookings/add/page.tsx b/frontend/app/dashboard/bookings/add/page.tsx index 539c6fd..15530b6 100644 --- a/frontend/app/dashboard/bookings/add/page.tsx +++ b/frontend/app/dashboard/bookings/add/page.tsx @@ -1,245 +1,149 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; +import axios from "axios"; import { useRouter } from "next/navigation"; -import { Calendar, Clock, Save, X, AlertTriangle } from "lucide-react"; +import { CalendarPlus, Clock, FileText, MapPin, CheckCircle2 } from "lucide-react"; export default function AddBookingPage() { const router = useRouter(); - const [isLoading, setIsLoading] = useState(false); + const [rooms, setRooms] = useState([]); + const [loading, setLoading] = useState(true); + const [submitLoading, setSubmitLoading] = useState(false); - // State untuk form input - const [formData, setFormData] = useState({ - room: "Ruang T-301", - date: "", - startTime: "", - endTime: "", - purpose: "", - }); + // State Form + const [selectedRoomId, setSelectedRoomId] = useState(""); + const [startTime, setStartTime] = useState(""); + const [endTime, setEndTime] = useState(""); + const [purpose, setPurpose] = useState(""); - const handleChange = (e: React.ChangeEvent) => { - setFormData({ ...formData, [e.target.name]: e.target.value }); - }; - - // --- LOGIKA VALIDASI (Business Logic) --- - const validateBookingRules = () => { - // 1. Cek kelengkapan data dasar - if (!formData.date || !formData.startTime || !formData.endTime) { - alert("Mohon lengkapi Tanggal dan Jam terlebih dahulu."); - return false; - } - - // 2. Ambil Jam Mulai - const startHour = parseInt(formData.startTime.split(":")[0]); - - // 3. Cek Jam Khusus (Diatas 21:00 ATAU Sebelum 06:00) - const isSpecialTime = startHour >= 21 || startHour < 6; - - if (isSpecialTime) { - const bookingDate = new Date(formData.date); - const today = new Date(); - - // Reset jam agar perhitungan murni berdasarkan tanggal - bookingDate.setHours(0, 0, 0, 0); - today.setHours(0, 0, 0, 0); - - // Hitung selisih hari - const diffTime = bookingDate.getTime() - today.getTime(); - const diffDays = diffTime / (1000 * 3600 * 24); - - // ATURAN: Harus H-3 - if (diffDays < 3) { - alert( - `Gagal! Peminjaman di jam khusus (${formData.startTime}) harus dilakukan minimal 3 hari sebelumnya.\n\n` + - `Jarak peminjaman Anda: ${diffDays} hari dari sekarang.` - ); - return false; + // Ambil daftar ruangan untuk Dropdown + useEffect(() => { + const fetchRooms = async () => { + try { + const token = localStorage.getItem("token"); + const res = await axios.get("http://localhost:8080/api/rooms", { + headers: { Authorization: `Bearer ${token}` }, + }); + setRooms(res.data.data); + } catch (error) { + console.error("Gagal memuat ruangan", error); + } finally { + setLoading(false); } - } + }; + fetchRooms(); + }, []); - return true; - }; - - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + if (!selectedRoomId) return alert("Pilih ruangan terlebih dahulu!"); - // Jalankan validasi - if (!validateBookingRules()) return; + setSubmitLoading(true); + const token = localStorage.getItem("token"); - setIsLoading(true); + try { + const startISO = new Date(startTime).toISOString(); + const endISO = new Date(endTime).toISOString(); - // --- MOCK API CALL --- - console.log("Mengirim Data:", formData); + await axios.post("http://localhost:8080/api/bookings", { + room_id: parseInt(selectedRoomId), + start_time: startISO, + end_time: endISO, + purpose: purpose, + }, { + headers: { Authorization: `Bearer ${token}` }, + }); - setTimeout(() => { - alert("Berhasil! Jadwal telah disimpan."); - setIsLoading(false); - // Redirect kembali ke halaman List Booking - router.push("/dashboard/bookings"); - }, 1500); + alert("Booking Berhasil Diajukan!"); + router.push("/dashboard/bookings/calendar"); // Arahkan ke jadwal setelah sukses + + } catch (error: any) { + alert("GAGAL: " + (error.response?.data?.error || "Terjadi kesalahan")); + } finally { + setSubmitLoading(false); + } }; + if (loading) return
Memuat formulir...
; + return ( -
- {/* Header Halaman */} -
-

- Buat Peminjaman Baru -

- -
- - {/* Form Container */} -
-
-

- Formulir Pengajuan Jadwal -

+
+
+
+ +
+
+

Buat Booking Baru

+

Isi detail di bawah ini untuk memesan ruangan kelas S-CLASS.

- -
-
- - {/* --- Baris 1: Ruangan & Tanggal --- */} -
- {/* Input Ruangan */} -
- -
- - - - -
-
- - {/* Input Tanggal */} -
- -
- - -
-
-
- - {/* --- Baris 2: Waktu Mulai & Selesai --- */} -
-
- -
- - -
- {/* Info Validasi */} -

- - Peminjaman jam 21:00 - 06:00 wajib H-3. -

-
- -
- -
- - -
-
-
- - {/* --- Baris 3: Keperluan --- */} -
- - -
- - {/* --- Tombol Aksi --- */} -
- - - -
- -
-
+ +
+ {/* Pilih Ruangan */} +
+ +
+ + +
+
+ + {/* Waktu Mulai & Selesai (Grid 2 Kolom) */} +
+
+ +
+ + setStartTime(e.target.value)} + /> +
+
+
+ +
+ + setEndTime(e.target.value)} + /> +
+
+
+ + {/* Keperluan */} +
+ +
+ +