6 maret 2026

This commit is contained in:
[Valentino Heman Budiarto] 2026-03-06 13:53:39 +07:00
parent bad2d21fc2
commit 2a3440ebe9
3 changed files with 69 additions and 21 deletions

View File

@ -5,7 +5,7 @@ import (
"s-class-backend/config" "s-class-backend/config"
"s-class-backend/controllers" "s-class-backend/controllers"
"s-class-backend/middleware" "s-class-backend/middleware"
"s-class-backend/models" // <--- Pastikan ini ada! "s-class-backend/models"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -15,7 +15,6 @@ func main() {
config.ConnectDatabase() config.ConnectDatabase()
// 2. AutoMigrate (Membuat tabel jika belum ada) // 2. AutoMigrate (Membuat tabel jika belum ada)
// Kita masukkan semua model di sini agar relasi terbentuk rapi
config.DB.AutoMigrate(&models.User{}, &models.Room{}, &models.Booking{}) config.DB.AutoMigrate(&models.User{}, &models.Room{}, &models.Booking{})
r := gin.Default() r := gin.Default()
@ -34,8 +33,10 @@ func main() {
auth.POST("/login", controllers.Login) auth.POST("/login", controllers.Login)
} }
r.POST("/api/verify-code", controllers.VerifyRedeemCode)
protected := r.Group("/api") protected := r.Group("/api")
protected.Use(middleware.AuthMiddleware()) protected.Use(middleware.AuthMiddleware())
{ {
protected.GET("/profile", func(c *gin.Context) { protected.GET("/profile", func(c *gin.Context) {
userID, _ := c.Get("user_id") userID, _ := c.Get("user_id")
@ -48,9 +49,9 @@ func main() {
protected.POST("/rooms", controllers.CreateRoom) protected.POST("/rooms", controllers.CreateRoom)
// Bookings // Bookings
protected.POST("/bookings", controllers.CreateBooking) protected.POST("/bookings", controllers.CreateBooking)
protected.GET("/bookings", controllers.GetUserBookings) protected.GET("/bookings", controllers.GetUserBookings)
protected.PATCH("/bookings/:id", controllers.UpdateBookingStatus) protected.PATCH("/bookings/:id", controllers.UpdateBookingStatus)
} }
r.Run(":8080") r.Run(":8080")

View File

@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"s-class-backend/config" "s-class-backend/config"
"s-class-backend/helpers" // Import helper pembuat kode acak "s-class-backend/helpers"
"s-class-backend/models" "s-class-backend/models"
"time" "time"
@ -26,7 +26,7 @@ func CreateBooking(c *gin.Context) {
return return
} }
// --- Handle UUID --- //Handle UUID
userIDInterface, exists := c.Get("user_id") userIDInterface, exists := c.Get("user_id")
if !exists { if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User ID tidak ditemukan"}) c.JSON(http.StatusUnauthorized, gin.H{"error": "User ID tidak ditemukan"})
@ -51,7 +51,7 @@ func CreateBooking(c *gin.Context) {
return return
} }
// CEK BENTROK (Overlap Check) // (Overlap Check)
var count int64 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 <= ?))", 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) input.RoomID, input.EndTime, input.StartTime, input.EndTime, input.StartTime, input.StartTime, input.EndTime).Count(&count)
@ -105,10 +105,10 @@ func GetUserBookings(c *gin.Context) {
} }
type UpdateStatusInput struct { type UpdateStatusInput struct {
Status string `json:"status" binding:"required"` Status string `json:"status" binding:"required"`
} }
// --- FUNGSI UPDATE STATUS (ADMIN) --- //UPDATE STATUS (ADMIN)
func UpdateBookingStatus(c *gin.Context) { func UpdateBookingStatus(c *gin.Context) {
bookingID := c.Param("id") bookingID := c.Param("id")
@ -126,14 +126,12 @@ func UpdateBookingStatus(c *gin.Context) {
booking.Status = input.Status booking.Status = input.Status
// 👇 LOGIKA PEMBUATAN REDEEM CODE 👇 //REDEEM CODE
if input.Status == "Approved" { if input.Status == "Approved" {
// Buat kode dari helper jika sebelumnya belum punya kode
if booking.RedeemCode == "" { if booking.RedeemCode == "" {
booking.RedeemCode = helpers.GenerateRedeemCode() booking.RedeemCode = helpers.GenerateRedeemCode()
} }
} else if input.Status == "Rejected" || input.Status == "Cancelled" { } else if input.Status == "Rejected" || input.Status == "Cancelled" {
// Kosongkan kode jika peminjaman ditolak/dibatalkan
booking.RedeemCode = "" booking.RedeemCode = ""
} }
@ -143,4 +141,36 @@ func UpdateBookingStatus(c *gin.Context) {
"message": "Status booking berhasil diperbarui!", "message": "Status booking berhasil diperbarui!",
"data": booking, "data": booking,
}) })
} }
//INPUT UNTUK ESP3
type RedeemInput struct {
RedeemCode string `json:"redeem_code" binding:"required"`
}
//VERIFIKASI KODE DARI ESP32
func VerifyRedeemCode(c *gin.Context) {
var input RedeemInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Format data salah"})
return
}
var booking models.Booking
if err := config.DB.Preload("Room").First(&booking, "redeem_code = ? AND status = 'Approved'", input.RedeemCode).Error; err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Kode tidak valid, belum disetujui, atau sudah digunakan!"})
return
}
booking.Status = "Completed"
booking.RedeemCode = ""
config.DB.Save(&booking)
c.JSON(http.StatusOK, gin.H{
"message": "Kode valid!",
"room": booking.Room.Name,
"status": "success",
})
}

View File

@ -3,7 +3,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import axios from "axios"; import axios from "axios";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { ArrowLeft, Calendar, Clock, MapPin, AlertCircle, CheckCircle, XCircle } from "lucide-react"; import { ArrowLeft, Calendar, Clock, MapPin, AlertCircle, CheckCircle, XCircle, Key } from "lucide-react";
interface Booking { interface Booking {
booking_id: string; booking_id: string;
@ -15,6 +15,7 @@ interface Booking {
end_time: string; end_time: string;
purpose: string; purpose: string;
status: string; status: string;
redeem_code: string;
created_at: string; created_at: string;
} }
@ -45,7 +46,6 @@ export default function HistoryPage() {
} }
}; };
// Fungsi untuk memformat tanggal agar enak dibaca (Contoh: 10 Feb 2026, 08:00)
const formatDate = (isoString: string) => { const formatDate = (isoString: string) => {
const date = new Date(isoString); const date = new Date(isoString);
return date.toLocaleDateString("id-ID", { return date.toLocaleDateString("id-ID", {
@ -54,23 +54,21 @@ export default function HistoryPage() {
}); });
}; };
// Fungsi menentukan warna status
const getStatusBadge = (status: string) => { const getStatusBadge = (status: string) => {
switch (status) { switch (status) {
case "Approved": case "Approved":
return <span className="flex items-center gap-1 bg-green-100 text-green-700 px-3 py-1 rounded-full text-xs font-bold"><CheckCircle size={14}/> Disetujui</span>; return <span className="flex items-center gap-1 bg-green-100 text-green-700 px-3 py-1 rounded-full text-xs font-bold"><CheckCircle size={14}/> Disetujui</span>;
case "Rejected": case "Rejected":
return <span className="flex items-center gap-1 bg-red-100 text-red-700 px-3 py-1 rounded-full text-xs font-bold"><XCircle size={14}/> Ditolak</span>; return <span className="flex items-center gap-1 bg-red-100 text-red-700 px-3 py-1 rounded-full text-xs font-bold"><XCircle size={14}/> Ditolak</span>;
default: // Pending default:
return <span className="flex items-center gap-1 bg-yellow-100 text-yellow-700 px-3 py-1 rounded-full text-xs font-bold"><AlertCircle size={14}/> Menunggu</span>; return <span className="flex items-center gap-1 bg-yellow-100 text-yellow-700 px-3 py-1 rounded-full text-xs font-bold"><AlertCircle size={14}/> Menunggu</span>;
} }
}; };
if (loading) return <div className="p-10 text-center">Memuat riwayat...</div>; if (loading) return <div className="p-10 text-center font-bold text-gray-500">Memuat riwayat...</div>;
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
{/* Navbar Header */}
<nav className="bg-white shadow-sm px-6 py-4 sticky top-0 z-10"> <nav className="bg-white shadow-sm px-6 py-4 sticky top-0 z-10">
<div className="max-w-4xl mx-auto flex items-center gap-4"> <div className="max-w-4xl mx-auto flex items-center gap-4">
<button <button
@ -96,6 +94,7 @@ export default function HistoryPage() {
<div className="space-y-4"> <div className="space-y-4">
{bookings.map((item) => ( {bookings.map((item) => (
<div key={item.booking_id} className="bg-white p-5 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition"> <div key={item.booking_id} className="bg-white p-5 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition">
<div className="flex justify-between items-start mb-4"> <div className="flex justify-between items-start mb-4">
<div> <div>
<h3 className="text-lg font-bold text-gray-800">{item.room.name}</h3> <h3 className="text-lg font-bold text-gray-800">{item.room.name}</h3>
@ -127,6 +126,24 @@ export default function HistoryPage() {
<p className="text-xs text-gray-400">Keperluan:</p> <p className="text-xs text-gray-400">Keperluan:</p>
<p className="text-sm text-gray-800 font-medium">{item.purpose}</p> <p className="text-sm text-gray-800 font-medium">{item.purpose}</p>
</div> </div>
{item.status === "Approved" && item.redeem_code && (
<div className="mt-4 bg-indigo-50 border border-indigo-200 rounded-lg p-4">
<div className="flex items-center gap-2 text-indigo-800 mb-2">
<Key size={16} />
<p className="text-sm font-bold">Kode Akses Ruangan</p>
</div>
<div className="bg-white border border-indigo-100 rounded text-center py-2 shadow-inner">
<p className="text-2xl font-mono font-bold text-indigo-700 tracking-[0.2em]">
{item.redeem_code}
</p>
</div>
<p className="text-xs text-indigo-500 mt-2 text-center">
*Masukkan kode ini pada layar ruangan.
</p>
</div>
)}
</div> </div>
))} ))}
</div> </div>