diff --git a/frontend b/frontend deleted file mode 160000 index 557ce3b..0000000 --- a/frontend +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 557ce3b43798549993dbd59b8afa3d053356e57d diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/frontend/app/admin/page.tsx b/frontend/app/admin/page.tsx new file mode 100644 index 0000000..71b3c39 --- /dev/null +++ b/frontend/app/admin/page.tsx @@ -0,0 +1,154 @@ +"use client"; + +import { useEffect, useState } from "react"; +import axios from "axios"; +import { useRouter } from "next/navigation"; +import { LogOut, CheckCircle, XCircle, Clock } from "lucide-react"; + +export default function AdminDashboard() { + const router = useRouter(); + const [admin, setAdmin] = useState(null); + const [bookings, setBookings] = 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); + }, []); + + const fetchBookings = async (token: string) => { + try { + const response = 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); + } finally { + setLoading(false); + } + }; + + const handleUpdateStatus = async (bookingId: string, newStatus: string) => { + const token = localStorage.getItem("token"); + try { + await axios.patch( + `http://localhost:8080/api/bookings/${bookingId}`, + { status: newStatus }, + { 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); + } + }; + + const handleLogout = () => { + localStorage.removeItem("token"); + localStorage.removeItem("user"); + router.push("/login"); + }; + + if (loading) return
Memuat panel admin...
; + + return ( +
+ {/* Navbar Admin */} + + + {/* Konten Utama */} +
+

Manajemen Peminjaman Ruangan

+ +
+ + + + + + + + + + + + + {bookings.length === 0 ? ( + + + + ) : ( + bookings.map((b) => ( + + + + + + + + + )) + )} + +
RuanganPeminjamWaktuKeperluanStatusAksi
Belum ada pengajuan peminjaman.
{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/dashboard/bookings/add/page.tsx b/frontend/app/dashboard/bookings/add/page.tsx new file mode 100644 index 0000000..539c6fd --- /dev/null +++ b/frontend/app/dashboard/bookings/add/page.tsx @@ -0,0 +1,245 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { Calendar, Clock, Save, X, AlertTriangle } from "lucide-react"; + +export default function AddBookingPage() { + const router = useRouter(); + const [isLoading, setIsLoading] = useState(false); + + // State untuk form input + const [formData, setFormData] = useState({ + room: "Ruang T-301", + date: "", + startTime: "", + endTime: "", + purpose: "", + }); + + 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; + } + } + + return true; + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + // Jalankan validasi + if (!validateBookingRules()) return; + + setIsLoading(true); + + // --- MOCK API CALL --- + console.log("Mengirim Data:", formData); + + setTimeout(() => { + alert("Berhasil! Jadwal telah disimpan."); + setIsLoading(false); + // Redirect kembali ke halaman List Booking + router.push("/dashboard/bookings"); + }, 1500); + }; + + return ( +
+ {/* Header Halaman */} +
+

+ Buat Peminjaman Baru +

+ +
+ + {/* Form Container */} +
+
+

+ Formulir Pengajuan Jadwal +

+
+ +
+
+ + {/* --- 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 --- */} +
+ + + +
+ +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/app/dashboard/bookings/calendar/page.tsx b/frontend/app/dashboard/bookings/calendar/page.tsx new file mode 100644 index 0000000..e7590d0 --- /dev/null +++ b/frontend/app/dashboard/bookings/calendar/page.tsx @@ -0,0 +1,247 @@ +"use client"; + +import { useState } from "react"; +import { ChevronLeft, ChevronRight, Calendar as CalendarIcon, Clock, MapPin } from "lucide-react"; + +// --- Tipe Data Mock --- +type Booking = { + id: number; + room: string; + date: string; // Format YYYY-MM-DD + startTime: string; + endTime: string; + purpose: string; + status: "Approved" | "Pending"; +}; + +// --- Data Dummy (Mock Data) --- +const mockBookings: Booking[] = [ + { + id: 1, + room: "Ruang T-301", + date: "2026-02-05", + startTime: "07:30", + endTime: "10:00", + purpose: "Kuliah Sistem Kendali", + status: "Approved", + }, + { + id: 2, + room: "Lab IoT", + date: "2026-02-05", // Tanggal sama dengan atas + startTime: "13:00", + endTime: "15:00", + purpose: "Praktikum Embedded System", + status: "Approved", + }, + { + id: 3, + room: "Ruang T-302", + date: "2026-02-12", + startTime: "09:00", + endTime: "11:30", + purpose: "Seminar Proposal", + status: "Pending", + }, + { + id: 4, + room: "Lab Kendali", + date: "2026-02-20", + startTime: "10:00", + endTime: "12:00", + purpose: "Riset Skripsi", + status: "Approved", + }, +]; + +export default function CalendarPage() { + const [currentDate, setCurrentDate] = useState(new Date(2026, 1)); // Februari 2026 + const [selectedDate, setSelectedDate] = useState(null); + + // --- Helper Functions --- + const getDaysInMonth = (year: number, month: number) => new Date(year, month + 1, 0).getDate(); + const getFirstDayOfMonth = (year: number, month: number) => new Date(year, month, 1).getDay(); + + const handlePrevMonth = () => { + setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1)); + setSelectedDate(null); + }; + + const handleNextMonth = () => { + setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1)); + setSelectedDate(null); + }; + + const handleDateClick = (day: number) => { + const year = currentDate.getFullYear(); + const month = String(currentDate.getMonth() + 1).padStart(2, "0"); + const dateStr = String(day).padStart(2, "0"); + const fullDate = `${year}-${month}-${dateStr}`; + + // Toggle seleksi: Jika diklik lagi, tutup detailnya + if (selectedDate === fullDate) { + setSelectedDate(null); + } else { + setSelectedDate(fullDate); + } + }; + + // --- Generate Calendar Grid --- + const year = currentDate.getFullYear(); + const month = currentDate.getMonth(); + const daysInMonth = getDaysInMonth(year, month); + const firstDay = getFirstDayOfMonth(year, month); + + // Array kosong untuk padding hari sebelum tanggal 1 + const emptyDays = Array.from({ length: firstDay }, (_, i) => i); + // Array tanggal 1 sampai akhir bulan + const days = Array.from({ length: daysInMonth }, (_, i) => i + 1); + + // Nama Bulan + const monthNames = [ + "Januari", "Februari", "Maret", "April", "Mei", "Juni", + "Juli", "Agustus", "September", "Oktober", "November", "Desember" + ]; + + // Filter booking untuk tanggal yang dipilih + const selectedBookings = mockBookings.filter((b) => b.date === selectedDate); + + return ( +
+ + {/* --- KIRI: KALENDER --- */} +
+ {/* Header Kalender */} +
+
+
+ +
+
+

+ {monthNames[month]} {year} +

+

Pilih tanggal untuk melihat detail.

+
+
+ +
+ + +
+
+ + {/* Grid Kalender */} +
+ {/* Nama Hari */} +
+ {["Min", "Sen", "Sel", "Rab", "Kam", "Jum", "Sab"].map((d) => ( + + {d} + + ))} +
+ + {/* Tanggal */} +
+ {/* Render Empty Cells */} + {emptyDays.map((_, i) => ( +
+ ))} + + {/* Render Date Cells */} + {days.map((day) => { + // Format tanggal hari ini untuk pengecekan data + const currentDayStr = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`; + const hasBooking = mockBookings.some((b) => b.date === currentDayStr); + const isSelected = selectedDate === currentDayStr; + + return ( +
handleDateClick(day)} + className={`relative flex h-24 sm:h-32 cursor-pointer flex-col justify-between rounded border p-2 transition hover:bg-gray-50 dark:hover:bg-meta-4 + ${isSelected + ? "border-yellow-500 bg-yellow-50 dark:bg-slate-800" + : "border-stroke border-gray-200 bg-white dark:bg-boxdark" + }`} + > + + {day} + + + {/* Indikator Booking (Dot / Bar) */} + {hasBooking && ( +
+ {mockBookings + .filter(b => b.date === currentDayStr) + .slice(0, 2) // Batasi tampilan max 2 baris di grid + .map((b, idx) => ( +
+ {b.startTime} - {b.room} +
+ ))} + {mockBookings.filter(b => b.date === currentDayStr).length > 2 && ( + + Lainnya + )} +
+ )} +
+ ); + })} +
+
+
+ + {/* --- KANAN: DETAIL SIDEBAR --- */} + {selectedDate && ( +
+
+

+ Jadwal {selectedDate} +

+ + {selectedBookings.length > 0 ? ( +
+ {selectedBookings.map((booking) => ( +
+
+ + {booking.status} + +
+ +

{booking.purpose}

+ +
+ {booking.room} +
+
+ {booking.startTime} - {booking.endTime} +
+
+ ))} +
+ ) : ( +
+

Tidak ada jadwal pada tanggal ini.

+ +
+ )} +
+
+ )} + +
+ ); +} \ No newline at end of file diff --git a/frontend/app/dashboard/bookings/page.tsx b/frontend/app/dashboard/bookings/page.tsx new file mode 100644 index 0000000..b36fd9b --- /dev/null +++ b/frontend/app/dashboard/bookings/page.tsx @@ -0,0 +1,189 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +export default function BookingPage() { + const router = useRouter(); + const [isLoading, setIsLoading] = useState(false); + + // State untuk form input + const [formData, setFormData] = useState({ + room: "Ruang T-301", + date: "", + startTime: "", + endTime: "", + purpose: "", + }); + + const handleChange = (e: React.ChangeEvent) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); + }; + + // --- LOGIKA VALIDASI KHUSUS --- + const validateBookingRules = () => { + // 1. Pastikan tanggal dan jam sudah diisi + if (!formData.date || !formData.startTime) { + alert("Mohon lengkapi Tanggal dan Jam Mulai terlebih dahulu."); + return false; + } + + // 2. Ambil Jam dari input (Format "HH:mm", kita ambil HH nya saja) + const startHour = parseInt(formData.startTime.split(":")[0]); + + // 3. Cek apakah jam termasuk "Jam Khusus" (Diatas 21:00 ATAU Sebelum 06:00) + // Note: startHour >= 21 artinya jam 21:00 ke atas. startHour < 6 artinya jam 00:00 - 05:59 + const isSpecialTime = startHour >= 21 || startHour < 6; + + if (isSpecialTime) { + // Hitung selisih hari antara HARI INI dan TANGGAL BOOKING + const bookingDate = new Date(formData.date); + const today = new Date(); + + // Reset jam ke 00:00:00 agar perhitungan murni berdasarkan tanggal + bookingDate.setHours(0, 0, 0, 0); + today.setHours(0, 0, 0, 0); + + // Hitung selisih waktu dalam milidetik + const diffTime = bookingDate.getTime() - today.getTime(); + // Konversi milidetik ke hari (1 hari = 1000ms * 3600detik * 24jam) + const diffDays = diffTime / (1000 * 3600 * 24); + + // ATURAN: Jika selisih kurang dari 3 hari, tolak! + 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; // Validasi Gagal + } + } + + return true; // Validasi Berhasil + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + // Jalankan validasi sebelum proses loading + const isValid = validateBookingRules(); + if (!isValid) { + return; // Berhenti di sini jika validasi gagal + } + + setIsLoading(true); + + // --- LOGIKA SEMENTARA (MOCK) --- + console.log("Data Booking dikirim:", formData); + + setTimeout(() => { + alert("Peminjaman Berhasil! Token akses Anda telah dibuat."); + setIsLoading(false); + router.push("/dashboard"); + }, 1500); + }; + + return ( +
+
+ + {/* Header Form */} +
+

Formulir Peminjaman Ruang

+ +
+ +
+ + {/* Pilihan Ruangan */} +
+ + +
+ + {/* Tanggal */} +
+ + +
+ + {/* Waktu Mulai & Selesai */} +
+
+ + +

*Jam 21:00-06:00 butuh H-3

+
+
+ + +
+
+ + {/* Keperluan */} +
+ + +
+ + {/* Tombol Submit */} + + +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/app/dashboard/layout.tsx b/frontend/app/dashboard/layout.tsx new file mode 100644 index 0000000..ed4799b --- /dev/null +++ b/frontend/app/dashboard/layout.tsx @@ -0,0 +1,27 @@ +import Header from "@/components/Header"; +import Sidebar from "@/components/Sidebar"; + +export default function DashboardLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+
+ {/* Sidebar akan tampil di sini */} + + + {/* Area Konten Utama */} +
+
+
+
+ {children} +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx new file mode 100644 index 0000000..029216f --- /dev/null +++ b/frontend/app/dashboard/page.tsx @@ -0,0 +1,266 @@ +"use client"; + +import { useEffect, useState } from "react"; +import axios from "axios"; +import { useRouter } from "next/navigation"; +import { LogOut, MapPin, Users, Calendar, X, Clock, FileText } from "lucide-react"; + +// Tipe Data +interface Room { + room_id: number; + name: string; + category: string; + capacity: number; + floor: string; + status: string; +} + +export default function Dashboard() { + const router = useRouter(); + const [user, setUser] = useState(null); + const [rooms, setRooms] = useState([]); + const [loading, setLoading] = useState(true); + + // --- STATE UNTUK MODAL BOOKING --- + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedRoom, setSelectedRoom] = useState(null); + const [startTime, setStartTime] = useState(""); + const [endTime, setEndTime] = useState(""); + const [purpose, setPurpose] = useState(""); + const [submitLoading, setSubmitLoading] = useState(false); + // ---------------------------------- + + useEffect(() => { + const token = localStorage.getItem("token"); + const userData = localStorage.getItem("user"); + + if (!token || !userData) { + router.push("/login"); + return; + } + + setUser(JSON.parse(userData)); + fetchRooms(token); + }, []); + + const fetchRooms = async (token: string) => { + try { + const response = await axios.get("http://localhost:8080/api/rooms", { + headers: { Authorization: `Bearer ${token}` }, + }); + setRooms(response.data.data); + } catch (error) { + console.error("Error:", error); + router.push("/login"); + } finally { + setLoading(false); + } + }; + + const handleLogout = () => { + localStorage.removeItem("token"); + localStorage.removeItem("user"); + router.push("/login"); + }; + + // --- BUKA MODAL --- + const openBookingModal = (room: Room) => { + setSelectedRoom(room); + setIsModalOpen(true); + // Reset form + setStartTime(""); + setEndTime(""); + setPurpose(""); + }; + + // --- SUBMIT BOOKING --- + const handleBookingSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setSubmitLoading(true); + + const token = localStorage.getItem("token"); + + try { + // Perlu convert string datetime-local ke Format ISO string (RFC3339) buat Golang + // Contoh input: "2026-02-06T14:30" -> Output: "2026-02-06T14:30:00.000Z" + const startISO = new Date(startTime).toISOString(); + const endISO = new Date(endTime).toISOString(); + + await axios.post( + "http://localhost:8080/api/bookings", + { + room_id: selectedRoom?.room_id, + start_time: startISO, + end_time: endISO, + purpose: purpose, + }, + { + headers: { Authorization: `Bearer ${token}` }, + } + ); + + alert("Booking Berhasil Diajukan! Menunggu persetujuan."); + setIsModalOpen(false); // Tutup modal + + } catch (error: any) { + // Tangkap error dari Backend (Misal: Bentrok / 409 Conflict) + const errorMsg = error.response?.data?.error || "Gagal melakukan booking."; + alert("GAGAL: " + errorMsg); + } finally { + setSubmitLoading(false); + } + }; + + if (loading) return
Memuat data...
; + + return ( +
+ {/* Navbar */} + + + {/* Konten Utama */} +
+

Pilih Ruangan

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

{room.name}

+
+
+ {room.floor} +
+
+ Kapasitas: {room.capacity} +
+
+
+ + +
+ ))} +
+
+ + {/* --- MODAL POPUP FORMULIR --- */} + {isModalOpen && selectedRoom && ( +
+
+ + {/* Header Modal */} +
+

Form Peminjaman

+ +
+ + {/* Body Form */} +
+
+

Ruangan: {selectedRoom.name}

+

{selectedRoom.floor}

+
+ +
+ +
+ + setStartTime(e.target.value)} + /> +
+
+ +
+ +
+ + setEndTime(e.target.value)} + /> +
+
+ +
+ +
+ +