From 8e7f5f539bed977eb3d63bf07faa0832ab0536bd Mon Sep 17 00:00:00 2001 From: "[Valentino Heman Budiarto]" <[hemanvalentino@gmail.com]> Date: Sat, 16 May 2026 15:30:23 +0700 Subject: [PATCH] update 16 mei urutan dan tampilan user admin --- backend/0 | 0 backend/controllers/bookingcontroller.go | 113 ++++++++++-------- backend/controllers/roomcontroller.go | 17 ++- backend/models/entity.go | 12 +- frontend/app/admin/approvals/page.tsx | 96 +++++++++++---- frontend/app/admin/page.tsx | 42 +++++-- frontend/app/admin/rooms/page.tsx | 6 +- .../app/dashboard/bookings/calendar/page.tsx | 55 +++++---- frontend/app/dashboard/page.tsx | 6 +- 9 files changed, 225 insertions(+), 122 deletions(-) create mode 100644 backend/0 diff --git a/backend/0 b/backend/0 new file mode 100644 index 0000000..e69de29 diff --git a/backend/controllers/bookingcontroller.go b/backend/controllers/bookingcontroller.go index 929403f..102e7d2 100644 --- a/backend/controllers/bookingcontroller.go +++ b/backend/controllers/bookingcontroller.go @@ -51,13 +51,16 @@ func CreateBooking(c *gin.Context) { return } - // (Overlap Check) +// (Overlap Check) var count int64 - config.DB.Model(&models.Booking{}).Where("room_id = ? AND status != 'Cancelled' AND ((start_time < ? AND end_time > ?) OR (start_time < ? AND end_time > ?) OR (start_time >= ? AND end_time <= ?))", - input.RoomID, input.EndTime, input.StartTime, input.EndTime, input.StartTime, input.StartTime, input.EndTime).Count(&count) + // KITA UBAH STATUSNYA MENJADI: status IN ('Pending', 'Approved') + config.DB.Model(&models.Booking{}).Where( + "room_id = ? AND status IN ('Pending', 'Approved') AND ((start_time < ? AND end_time > ?) OR (start_time < ? AND end_time > ?) OR (start_time >= ? AND end_time <= ?))", + input.RoomID, input.EndTime, input.StartTime, input.EndTime, input.StartTime, input.StartTime, input.EndTime, + ).Count(&count) if count > 0 { - c.JSON(http.StatusConflict, gin.H{"error": "Ruangan sudah dibooking pada jam tersebut!"}) + c.JSON(http.StatusConflict, gin.H{"error": "Ruangan sudah dibooking atau sedang dalam antrean persetujuan pada jam tersebut!"}) return } @@ -110,44 +113,56 @@ type UpdateStatusInput struct { // UPDATE STATUS (ADMIN) func UpdateBookingStatus(c *gin.Context) { - bookingID := c.Param("id") - var input UpdateStatusInput + bookingID := c.Param("id") - // 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": "Format data salah"}) + 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 - } + var booking models.Booking + // KUNCI PERBAIKAN: Gunakan Where("booking_id = ?") agar tepat sasaran + if err := config.DB.Where("booking_id = ?", bookingID).First(&booking).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Data booking tidak ditemukan di database"}) + return + } - // 3. Update Status - booking.Status = input.Status + // Gunakan Switch Case (Clean Code) yang sudah kita bahas sebelumnya + switch input.Status { + case "Approved": + if booking.RedeemCode == "" { + booking.RedeemCode = helpers.GenerateRedeemCode() + } + case "Rejected", "Cancelled": + booking.RedeemCode = "" + } - // 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 - } + booking.Status = input.Status - c.JSON(http.StatusOK, gin.H{ - "message": "Status berhasil diperbarui!", - "data": booking, - }) + if err := config.DB.Save(&booking).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Gagal menyimpan ke database"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Status booking berhasil diperbarui!", + "data": booking, + }) + + booking.Status = input.Status + + // KITA BUKA KEDOK ERROR DARI POSTGRESQL: + if err := config.DB.Save(&booking).Error; err != nil { + fmt.Println("🔥 ERROR DARI DATABASE:", err.Error()) // Cetak teks merah di terminal + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) // Tembak ke pop-up web + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Status booking berhasil diperbarui!", + "data": booking, + }) } // INPUT UNTUK ESP3 @@ -184,19 +199,17 @@ func VerifyRedeemCode(c *gin.Context) { // GET ALL BOOKINGS (Untuk Calendar View semua user) func GetAllBookings(c *gin.Context) { - var bookings []models.Booking + 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 - } + // 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, - }) + // 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 7b95c4a..b20a8fa 100644 --- a/backend/controllers/roomcontroller.go +++ b/backend/controllers/roomcontroller.go @@ -56,26 +56,25 @@ func UpdateRoomStatus(c *gin.Context) { } 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"}) + // Karena model sudah di-tag dengan benar, pencarian ini pasti akan berhasil + if err := config.DB.Where("room_id = ?", roomID).First(&room).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Ruangan tidak ditemukan"}) 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) +// UPDATE ROOM POWER (Menerima data Watt dari ESP32) func UpdateRoomPower(c *gin.Context) { var input EnergySensorInput @@ -85,14 +84,14 @@ func UpdateRoomPower(c *gin.Context) { } var room models.Room - if err := config.DB.First(&room, "room_id = ?", input.RoomID).Error; err != nil { + // Tetap gunakan room_id agar GORM tidak bingung + if err := config.DB.Where("room_id = ?", input.RoomID).First(&room).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"}) -} +} \ No newline at end of file diff --git a/backend/models/entity.go b/backend/models/entity.go index 399f5aa..2980e8e 100644 --- a/backend/models/entity.go +++ b/backend/models/entity.go @@ -19,12 +19,12 @@ type User struct { } type Room struct { - RoomID uint `gorm:"primaryKey" json:"room_id"` - Name string `gorm:"not null" json:"name"` - Category string `gorm:"not null" json:"category"` - Capacity int `gorm:"not null" json:"capacity"` - Floor string `gorm:"not null" json:"floor"` - Status string `gorm:"type:room_status;default:'Available'" json:"status"` + RoomID uint `gorm:"primaryKey;column:room_id" json:"room_id"` + Name string `json:"name"` + Category string `json:"category"` + Floor string `json:"floor"` + Capacity int `json:"capacity"` + Status string `json:"status"` PowerConsumption float64 `json:"power_consumption" gorm:"default:0"` } diff --git a/frontend/app/admin/approvals/page.tsx b/frontend/app/admin/approvals/page.tsx index 2fbbd25..1f201ad 100644 --- a/frontend/app/admin/approvals/page.tsx +++ b/frontend/app/admin/approvals/page.tsx @@ -1,22 +1,50 @@ "use client"; import { useState, useEffect } from "react"; -import { ShieldCheck, Check, X, Clock } from "lucide-react"; +import axios from "axios"; +import { ShieldCheck, Check, X } from "lucide-react"; export default function ApprovalsPage() { const [activeTab, setActiveTab] = useState("Pending"); const [bookings, setBookings] = useState([]); + // 1. FUNGSI UNTUK MENGAMBIL DATA ASLI DARI DATABASE + const fetchBookings = async () => { + try { + const token = localStorage.getItem("token"); + // Mengambil dari fungsi GetAllBookings di backend + const res = await axios.get("http://localhost:8080/api/bookings", { + headers: { Authorization: `Bearer ${token}` } + }); + setBookings(res.data.data); + } catch (err: any) { + console.error("Gagal mengambil data peminjaman:", err); + } + }; + 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" }, - ]); + fetchBookings(); }, []); + // 2. FUNGSI UNTUK MENGUBAH STATUS (APPROVE / REJECT) + const updateStatus = async (bookingId: string, newStatus: string) => { + try { + const token = localStorage.getItem("token"); + // Mengirim UUID booking_id ke backend Golang + await axios.put( + `http://localhost:8080/api/bookings/${bookingId}/status`, + { status: newStatus }, + { headers: { Authorization: `Bearer ${token}` } } + ); + + alert(`Peminjaman berhasil di-${newStatus}!`); + fetchBookings(); // Refresh tabel setelah update + } catch (err: any) { + console.error("Gagal update status:", err.response?.data || err.message); + alert("Error: " + (err.response?.data?.error || "Gagal mengubah status")); + } + }; + const filteredBookings = bookings.filter(b => b.status === activeTab); return ( @@ -56,34 +84,62 @@ export default function ApprovalsPage() { Peminjam Ruangan - Waktu Mulai + Tanggal & Waktu Keperluan Status / Aksi {filteredBookings.map((b) => ( - - {b.user?.full_name} - {b.room?.name} - {new Date(b.start_time).toLocaleString('id-ID')} + // Pastikan key menggunakan booking_id (UUID) + + {/* Gunakan huruf KECIL: b.user dan b.room sesuai JSON dari Golang */} + {b.user?.full_name || "Tanpa Nama"} + {b.room?.name || "Ruangan Tidak Diketahui"} + + {/* Menampilkan Waktu Mulai sampai Waktu Selesai */} + +
+ {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).toLocaleDateString('id-ID')} +
+ {b.purpose} {b.status === "Pending" ? (
- -
) : ( - - {b.status} - +
+ + {b.status} + + {/* Menampilkan Redeem Code jika sudah Approved */} + {b.status === 'Approved' && b.redeem_code && ( + + Code: {b.redeem_code} + + )} +
)} diff --git a/frontend/app/admin/page.tsx b/frontend/app/admin/page.tsx index 56762f4..2742a11 100644 --- a/frontend/app/admin/page.tsx +++ b/frontend/app/admin/page.tsx @@ -49,7 +49,8 @@ export default function AdminDashboard() { } }; -const handleAction = async (id: number, status: string) => { + // PERBAIKAN 1: Ubah tipe id menjadi 'string' karena kita menggunakan UUID + const handleAction = async (id: string, status: string) => { try { const token = localStorage.getItem("token"); // Mengirim status 'Approved' atau 'Rejected' ke backend @@ -59,7 +60,8 @@ const handleAction = async (id: number, status: string) => { ); alert(`Permintaan berhasil di-${status}`); fetchAdminData(); // Refresh data setelah aksi - } catch (err) { + } catch (err: any) { + console.error(err); alert("Gagal memproses pendaftaran."); } }; @@ -75,7 +77,7 @@ const handleAction = async (id: number, status: string) => {
{roomStats.map((room) => ( -
+
{room.name}
5 ? 'bg-orange-500 animate-pulse' : 'bg-gray-300'}`} /> @@ -104,26 +106,42 @@ const handleAction = async (id: number, status: string) => { Peminjam Ruangan - Waktu + Tanggal & Waktu Keperluan Tindakan {pendingBookings.map((b) => ( - - {b.user?.full_name} - {b.room?.name} - - {new Date(b.start_time).toLocaleString('id-ID')} + // PERBAIKAN 2: Gunakan booking_id sebagai key + + {b.user?.full_name || "Tanpa Nama"} + {b.room?.name || "Ruangan Tidak Diketahui"} + + {/* PERBAIKAN 3: Format Waktu Mulai dan Selesai yang Rapi */} + +
+ {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).toLocaleDateString('id-ID')} +
- {b.purpose} + + {b.purpose}
- -
diff --git a/frontend/app/admin/rooms/page.tsx b/frontend/app/admin/rooms/page.tsx index c4b126a..a7febc3 100644 --- a/frontend/app/admin/rooms/page.tsx +++ b/frontend/app/admin/rooms/page.tsx @@ -66,7 +66,10 @@ export default function ManageRoomsPage() {
- {rooms.map((room) => ( + {/* TAMBAHKAN [...rooms].sort(...) SEBELUM .map */} + {[...rooms] + .sort((a, b) => a.name.localeCompare(b.name)) + .map((room) => (
@@ -93,7 +96,6 @@ export default function ManageRoomsPage() { > Set to {room.status === 'Available' ? 'Maintenance' : 'Available'} - ID: {room.room_id}
))} diff --git a/frontend/app/dashboard/bookings/calendar/page.tsx b/frontend/app/dashboard/bookings/calendar/page.tsx index 8774ab6..23bc0b2 100644 --- a/frontend/app/dashboard/bookings/calendar/page.tsx +++ b/frontend/app/dashboard/bookings/calendar/page.tsx @@ -171,13 +171,17 @@ export default function CalendarViewPage() { {/* BODY TABEL: Sumbu Y (Ruangan) */} - {rooms.map((room) => ( - - - -
{room.name}
-
{room.category}
- +{/* --- DAFTAR RUANGAN YANG SUDAH DIURUTKAN --- */} + {[...rooms] + .sort((a, b) => a.name.localeCompare(b.name)) + .map((room) => ( + + +
{room.name}
+
{room.category}
+ + + {/* (Kodingan untuk kolom hari dan jadwal peminjamanmu biarkan tetap berada di bawah sini) */} {weekDates.map((date, idx) => { const dailyBookings = getBookingsForCell(room.room_id, date); @@ -186,23 +190,32 @@ export default function CalendarViewPage() { {dailyBookings.length > 0 ? (
- {dailyBookings.map((b, i) => ( -
b.status === "Pending" || b.status === "Approved") + // 2. SORT: Urutkan berdasarkan waktu mulai (dari pagi ke malam) + .sort((a, b) => new Date(a.start_time).getTime() - new Date(b.start_time).getTime()) + // 3. MAP: Render ke layar + .map((b) => ( +
-

- {b.purpose} -

-
- - {formatTime(b.start_time)} - {formatTime(b.end_time)} - - {b.status || 'Pending'} - -
+ 'bg-blue-50 border-blue-200 border-l-blue-500'}`} + > +

+ {b.purpose} +

+
+ + {formatTime(b.start_time)} - {formatTime(b.end_time)} + + {b.status || 'Pending'} +
- ))} +
+ ))}
) : (
diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx index bdddca8..511fe80 100644 --- a/frontend/app/dashboard/page.tsx +++ b/frontend/app/dashboard/page.tsx @@ -121,7 +121,10 @@ export default function Dashboard() { {/* Grid Daftar Kelas */}
- {rooms.map((room) => ( + {/* TAMBAHKAN PENGURUTAN ABJAD DI SINI */} + {[...rooms] + .sort((a, b) => a.name.localeCompare(b.name)) + .map((room) => (
@@ -130,7 +133,6 @@ export default function Dashboard() { }`}> {room.status} - {room.category}

{room.name}