update 15 mei
This commit is contained in:
@@ -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<any[]>([]);
|
||||
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="bg-blue-100 p-3 rounded-lg text-blue-600">
|
||||
<ShieldCheck size={28} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-800">Manajemen Persetujuan</h2>
|
||||
<p className="text-gray-500 text-sm mt-1">Kelola permohonan peminjaman ruangan S-CLASS.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TABS */}
|
||||
<div className="flex gap-4 border-b border-gray-200">
|
||||
{["Pending", "Approved", "Rejected"].map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`pb-3 px-4 font-bold text-sm transition-all ${
|
||||
activeTab === tab
|
||||
? "border-b-2 border-blue-600 text-blue-600"
|
||||
: "text-gray-400 hover:text-gray-600"
|
||||
}`}
|
||||
>
|
||||
{tab}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* TABEL */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead className="bg-gray-50 text-gray-500 text-xs font-bold uppercase">
|
||||
<tr>
|
||||
<th className="p-4">Peminjam</th>
|
||||
<th className="p-4">Ruangan</th>
|
||||
<th className="p-4">Waktu Mulai</th>
|
||||
<th className="p-4">Keperluan</th>
|
||||
<th className="p-4 text-center">Status / Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-50">
|
||||
{filteredBookings.map((b) => (
|
||||
<tr key={b.id} className="hover:bg-gray-50/50 transition-colors">
|
||||
<td className="p-4 font-bold text-gray-800 text-sm">{b.user?.full_name}</td>
|
||||
<td className="p-4 text-sm font-medium text-gray-600">{b.room?.name}</td>
|
||||
<td className="p-4 text-sm text-gray-600">{new Date(b.start_time).toLocaleString('id-ID')}</td>
|
||||
<td className="p-4 text-sm text-gray-600">{b.purpose}</td>
|
||||
<td className="p-4 text-center">
|
||||
{b.status === "Pending" ? (
|
||||
<div className="flex justify-center gap-2">
|
||||
<button className="flex items-center gap-1 px-3 py-1.5 bg-green-50 text-green-600 font-bold text-xs rounded-lg hover:bg-green-100 transition-colors">
|
||||
<Check size={14} /> Setujui
|
||||
</button>
|
||||
<button className="flex items-center gap-1 px-3 py-1.5 bg-red-50 text-red-600 font-bold text-xs rounded-lg hover:bg-red-100 transition-colors">
|
||||
<X size={14} /> Tolak
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-bold ${
|
||||
b.status === 'Approved' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
|
||||
}`}>
|
||||
{b.status}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{filteredBookings.length === 0 && (
|
||||
<tr><td colSpan={5} className="p-8 text-center text-gray-400 font-medium">Tidak ada data di kategori ini.</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<any>(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 (
|
||||
<div className="min-h-screen bg-white flex flex-col font-sans">
|
||||
|
||||
{/* HEADER ADMIN */}
|
||||
<header className="bg-[#e5e7eb] flex items-center justify-between px-6 py-4 shadow-sm z-20 relative">
|
||||
<div>
|
||||
<h2 className="text-blue-600 text-sm font-medium mb-1">Admin Panel,</h2>
|
||||
<h1 className="text-blue-600 text-3xl font-bold">S-CLASS</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-slate-800 text-white rounded-full h-10 w-10 flex items-center justify-center text-lg font-bold shadow-sm">
|
||||
{user?.full_name ? user.full_name.charAt(0).toUpperCase() : "A"}
|
||||
</div>
|
||||
<div className="hidden sm:flex flex-col">
|
||||
<span className="font-semibold text-gray-800 text-sm">{user?.full_name || "Administrator"}</span>
|
||||
<span className="font-medium text-blue-600 text-xs uppercase tracking-wider">System Controller</span>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={handleLogout} className="flex items-center gap-2 text-red-500 font-medium hover:text-red-600 transition-colors">
|
||||
Log Out <LogOut size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="flex flex-1 overflow-hidden relative">
|
||||
|
||||
{/* SIDEBAR ADMIN (Lebar Permanen: w-64) */}
|
||||
<aside className="bg-[#b0c4d9] w-64 flex flex-col py-10 shadow-inner z-10">
|
||||
<nav className="flex flex-col gap-6 px-4">
|
||||
|
||||
<Link href="/admin" className={`flex items-center gap-4 font-semibold transition-colors group ${pathname === '/admin' ? 'text-blue-700' : 'text-gray-800 hover:text-blue-700'}`}>
|
||||
<div className={`p-2 rounded-lg transition-colors ${pathname === '/admin' ? 'bg-white/60 shadow-sm' : 'bg-white/30 group-hover:bg-white/50'}`}>
|
||||
<LayoutDashboard size={22} />
|
||||
</div>
|
||||
<span>Dashboard</span>
|
||||
</Link>
|
||||
|
||||
<Link href="/admin/monitoring" className={`flex items-center gap-4 font-semibold transition-colors group ${pathname === '/admin/monitoring' ? 'text-blue-700' : 'text-gray-800 hover:text-blue-700'}`}>
|
||||
<div className={`p-2 rounded-lg transition-colors ${pathname === '/admin/monitoring' ? 'bg-white/60 shadow-sm' : 'bg-white/30 group-hover:bg-white/50'}`}>
|
||||
<Activity size={22} />
|
||||
</div>
|
||||
<span>Power Monitoring</span>
|
||||
</Link>
|
||||
|
||||
<Link href="/admin/approvals" className={`flex items-center gap-4 font-semibold transition-colors group ${pathname === '/admin/approvals' ? 'text-blue-700' : 'text-gray-800 hover:text-blue-700'}`}>
|
||||
<div className={`p-2 rounded-lg transition-colors ${pathname === '/admin/approvals' ? 'bg-white/60 shadow-sm' : 'bg-white/30 group-hover:bg-white/50'}`}>
|
||||
<ShieldCheck size={22} />
|
||||
</div>
|
||||
<span>Approvals</span>
|
||||
</Link>
|
||||
|
||||
<Link href="/admin/rooms" className={`flex items-center gap-4 font-semibold transition-colors group ${pathname === '/admin/rooms' ? 'text-blue-700' : 'text-gray-800 hover:text-blue-700'}`}>
|
||||
<div className={`p-2 rounded-lg transition-colors ${pathname === '/admin/rooms' ? 'bg-white/60 shadow-sm' : 'bg-white/30 group-hover:bg-white/50'}`}>
|
||||
<Settings2 size={22} />
|
||||
</div>
|
||||
<span>Manage Rooms</span>
|
||||
</Link>
|
||||
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{/* KONTEN UTAMA */}
|
||||
<main className="flex-1 overflow-y-auto bg-[#f8fafc] p-8">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<any[]>([]);
|
||||
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="bg-blue-100 p-3 rounded-lg text-blue-600">
|
||||
<Activity size={28} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-800">Power Monitoring & Control</h2>
|
||||
<p className="text-gray-500 text-sm mt-1">Pantau konsumsi daya kWh meter dan kendalikan *relay* sirkuit ruangan.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{rooms.map((room) => (
|
||||
<div key={room.id} className="bg-white p-6 rounded-xl border border-gray-100 shadow-sm relative overflow-hidden">
|
||||
|
||||
{/* Indikator Status di Pojok Kanan Atas */}
|
||||
<div className={`absolute top-0 right-0 px-4 py-1.5 text-[10px] font-black uppercase tracking-wider rounded-bl-xl
|
||||
${room.isRelayOn ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}
|
||||
>
|
||||
{room.isRelayOn ? 'Sirkuit Aktif' : 'Sirkuit Terputus'}
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-bold text-gray-800 mb-4">{room.name}</h3>
|
||||
|
||||
<div className="flex items-end gap-2 mb-6">
|
||||
<span className={`text-5xl font-black tracking-tight ${room.power > 1000 ? 'text-orange-500' : 'text-gray-800'}`}>
|
||||
{room.power}
|
||||
</span>
|
||||
<span className="text-gray-500 font-bold mb-1.5">Watts</span>
|
||||
</div>
|
||||
|
||||
{room.power > 1000 && (
|
||||
<div className="flex items-center gap-2 text-xs font-bold text-orange-600 bg-orange-50 p-2 rounded-lg mb-4">
|
||||
<AlertTriangle size={14} /> Beban Tinggi Terdeteksi!
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between mt-auto pt-4 border-t border-gray-100">
|
||||
<span className="text-xs text-gray-400 font-medium">Update: {room.lastUpdate}</span>
|
||||
|
||||
<button
|
||||
onClick={() => handleCutOff(room.name)}
|
||||
disabled={!room.isRelayOn}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-xs font-bold transition-all
|
||||
${room.isRelayOn
|
||||
? 'bg-red-50 text-red-600 hover:bg-red-600 hover:text-white border border-red-200 hover:border-red-600'
|
||||
: 'bg-gray-100 text-gray-400 cursor-not-allowed'}`}
|
||||
>
|
||||
{room.isRelayOn ? <><Power size={14} /> Cut Off Power</> : <><ZapOff size={14} /> Offline</>}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+103
-113
@@ -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<any>(null);
|
||||
const [bookings, setBookings] = useState<any[]>([]);
|
||||
const [pendingBookings, setPendingBookings] = useState<any[]>([]);
|
||||
const [roomStats, setRoomStats] = useState<any[]>([]);
|
||||
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 <div className="p-10 text-center">Memuat panel admin...</div>;
|
||||
if (loading) return <div className="p-10 font-medium text-gray-500">Memasuki Mode Admin...</div>;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100">
|
||||
{/* Navbar Admin */}
|
||||
<nav className="bg-slate-800 shadow-sm px-6 py-4 flex justify-between items-center text-white sticky top-0 z-10">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-blue-400">S-CLASS | ADMIN PANEL</h1>
|
||||
<p className="text-xs text-gray-300">Welcome, {admin?.full_name}</p>
|
||||
<div className="space-y-8">
|
||||
{/* SECTION 1: POWER MONITORING CARDS */}
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-800 mb-4 flex items-center gap-2">
|
||||
<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">
|
||||
{roomStats.map((room) => (
|
||||
<div key={room.id} className="bg-white p-5 rounded-xl border border-gray-100 shadow-sm">
|
||||
<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'}`} />
|
||||
</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-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>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center gap-2 text-sm bg-red-500 hover:bg-red-600 px-4 py-2 rounded-lg transition"
|
||||
>
|
||||
<LogOut size={16} /> Keluar
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Konten Utama */}
|
||||
<main className="max-w-6xl mx-auto p-6">
|
||||
<h2 className="text-2xl font-bold text-gray-800 mb-6">Manajemen Peminjaman Ruangan</h2>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-50 text-gray-600 text-sm border-b">
|
||||
<th className="p-4">Ruangan</th>
|
||||
{/* SECTION 2: APPROVAL TABLE */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div className="p-6 border-b border-gray-50 flex justify-between items-center">
|
||||
<h2 className="text-xl font-bold text-gray-800">Pending Approvals</h2>
|
||||
<span className="bg-blue-100 text-blue-700 px-3 py-1 rounded-full text-xs font-bold">
|
||||
{pendingBookings.length} Requests
|
||||
</span>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead className="bg-gray-50 text-gray-500 text-xs font-bold uppercase">
|
||||
<tr>
|
||||
<th className="p-4">Peminjam</th>
|
||||
<th className="p-4">Ruangan</th>
|
||||
<th className="p-4">Waktu</th>
|
||||
<th className="p-4">Keperluan</th>
|
||||
<th className="p-4">Status</th>
|
||||
<th className="p-4 text-center">Aksi</th>
|
||||
<th className="p-4 text-center">Tindakan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{bookings.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center p-8 text-gray-500">Belum ada pengajuan peminjaman.</td>
|
||||
<tbody className="divide-y divide-gray-50">
|
||||
{pendingBookings.map((b) => (
|
||||
<tr key={b.id} className="hover:bg-gray-50/50 transition-colors">
|
||||
<td className="p-4 font-bold text-gray-800 text-sm">{b.user?.full_name}</td>
|
||||
<td className="p-4 text-sm font-medium text-gray-600">{b.room?.name}</td>
|
||||
<td className="p-4 text-xs text-gray-500 leading-relaxed">
|
||||
{new Date(b.start_time).toLocaleString('id-ID')}
|
||||
</td>
|
||||
<td className="p-4 text-sm text-gray-600 truncate max-w-37.5">{b.purpose}</td>
|
||||
<td className="p-4">
|
||||
<div className="flex justify-center gap-2">
|
||||
<button onClick={() => handleAction(b.id, 'Approved')} className="p-2 bg-green-50 text-green-600 rounded-lg hover:bg-green-100 transition-colors">
|
||||
<Check size={18} />
|
||||
</button>
|
||||
<button onClick={() => handleAction(b.id, 'Rejected')} className="p-2 bg-red-50 text-red-600 rounded-lg hover:bg-red-100 transition-colors">
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{pendingBookings.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={5} className="p-10 text-center text-gray-400 text-sm italic">Tidak ada permintaan menunggu.</td>
|
||||
</tr>
|
||||
) : (
|
||||
bookings.map((b) => (
|
||||
<tr key={b.booking_id} className="border-b hover:bg-gray-50">
|
||||
<td className="p-4 font-semibold text-gray-800">{b.room?.name}</td>
|
||||
<td className="p-4 text-sm text-gray-600">ID User: {b.user_id.substring(0,8)}...</td>
|
||||
<td className="p-4 text-sm text-gray-600">
|
||||
<div>Mulai: {new Date(b.start_time).toLocaleString('id-ID')}</div>
|
||||
<div>Selesai: {new Date(b.end_time).toLocaleString('id-ID')}</div>
|
||||
</td>
|
||||
<td className="p-4 text-sm text-gray-600">{b.purpose}</td>
|
||||
<td className="p-4">
|
||||
<span className={`px-2 py-1 text-xs font-bold rounded-full ${
|
||||
b.status === 'Approved' ? 'bg-green-100 text-green-700' :
|
||||
b.status === 'Rejected' ? 'bg-red-100 text-red-700' :
|
||||
'bg-yellow-100 text-yellow-700'
|
||||
}`}>
|
||||
{b.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
{b.status === 'Pending' ? (
|
||||
<div className="flex justify-center gap-2">
|
||||
<button
|
||||
onClick={() => handleUpdateStatus(b.booking_id, 'Approved')}
|
||||
className="bg-green-500 hover:bg-green-600 text-white p-2 rounded-lg" title="Setujui">
|
||||
<CheckCircle size={18} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleUpdateStatus(b.booking_id, 'Rejected')}
|
||||
className="bg-red-500 hover:bg-red-600 text-white p-2 rounded-lg" title="Tolak">
|
||||
<XCircle size={18} />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-gray-400 text-xs">- Selesai -</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<any[]>([]);
|
||||
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 <div className="p-8 text-gray-500 font-medium text-center">Menghubungkan ke Database S-CLASS...</div>;
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto space-y-6">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-blue-100 p-3 rounded-lg text-blue-600">
|
||||
<Settings2 size={28} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-800">Manajemen Ruangan</h2>
|
||||
<p className="text-gray-500 text-sm mt-1">Atur ketersediaan operasional setiap ruangan di Gedung D.</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={fetchRooms} className="p-2 text-gray-400 hover:text-blue-600 transition-colors">
|
||||
<RefreshCcw size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{rooms.map((room) => (
|
||||
<div key={room.room_id} className={`bg-white p-6 rounded-2xl border transition-all shadow-sm flex items-center justify-between
|
||||
${room.status === 'Maintenance' ? 'border-orange-200 bg-orange-50/20' : 'border-gray-100'}`}>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2 text-gray-400 text-xs font-bold uppercase tracking-wider">
|
||||
{room.category} • {room.floor}
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-gray-800">{room.name}</h3>
|
||||
|
||||
<div className={`flex items-center gap-1.5 text-sm font-bold mt-2
|
||||
${room.status === 'Available' ? 'text-green-600' : 'text-orange-600'}`}>
|
||||
{room.status === 'Available' ? <CheckCircle2 size={16} /> : <AlertCircle size={16} />}
|
||||
{room.status}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-end gap-3">
|
||||
<button
|
||||
onClick={() => toggleRoomStatus(room.room_id, room.status)}
|
||||
className={`px-4 py-2 rounded-xl text-xs font-bold transition-all shadow-sm
|
||||
${room.status === 'Available'
|
||||
? 'bg-orange-100 text-orange-700 hover:bg-orange-600 hover:text-white'
|
||||
: 'bg-green-100 text-green-700 hover:bg-green-600 hover:text-white'}`}
|
||||
>
|
||||
Set to {room.status === 'Available' ? 'Maintenance' : 'Available'}
|
||||
</button>
|
||||
<span className="text-[10px] text-gray-400 font-medium italic">ID: {room.room_id}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user