6 maret 2026
This commit is contained in:
parent
bad2d21fc2
commit
2a3440ebe9
@ -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,6 +33,8 @@ 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())
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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)
|
||||||
@ -108,7 +108,7 @@ 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 = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,3 +142,35 @@ func UpdateBookingStatus(c *gin.Context) {
|
|||||||
"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",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user