source: frontend/src/pages/ApprovalsPage.jsx

Last change on this file was 09e02d7, checked in by Nikola Sarafimov <sarafimov.nikola12345@…>, 4 days ago

Final room reservation system implementation

  • Property mode set to 100644
File size: 4.6 KB
Line 
1import { useEffect, useState } from "react";
2import { motion } from "framer-motion";
3import toast from "react-hot-toast";
4import { CheckCircle2, ClipboardCheck, XCircle } from "lucide-react";
5import { approveReservation, getPendingReservations } from "../api.js";
6
7export default function ApprovalsPage({ activeUser }) {
8 const [pending, setPending] = useState([]);
9 const [loading, setLoading] = useState(true);
10
11 function loadData() {
12 setLoading(true);
13
14 getPendingReservations()
15 .then(setPending)
16 .catch((err) => toast.error(err.message))
17 .finally(() => setLoading(false));
18 }
19
20 useEffect(() => {
21 if (canApprove) {
22 loadData();
23 }
24 }, [canApprove]);
25
26 const canApprove = activeUser.role === "approver" || activeUser.role === "admin";
27
28 async function saveDecision(reservationId, decision) {
29 try {
30 await approveReservation({
31 reservationId,
32 approverId: activeUser.userId,
33 decision,
34 note: `${decision} through React UI.`,
35 });
36
37 toast.success(`Reservation ${decision}`);
38 loadData();
39 } catch (err) {
40 toast.error(err.message);
41 }
42 }
43
44 if (!canApprove) {
45 return (
46 <section>
47 <Hero
48 eyebrow="Access denied"
49 title="Review Pending Reservations"
50 subtitle="This module is available only for approver and administrator users."
51 />
52 </section>
53 );
54 }
55
56 return (
57 <section>
58 <Hero
59 eyebrow="Approval center"
60 title="Review Pending Reservations"
61 subtitle="Approve or reject pending requests through the PostgreSQL stored function."
62 />
63
64 {loading && <div className="empty-state">Loading pending reservations...</div>}
65
66 {!loading && pending.length === 0 && (
67 <div className="empty-state">No pending reservations at the moment.</div>
68 )}
69
70 {!loading && pending.length > 0 && (
71 <div className="approval-grid">
72 {pending.map((item) => (
73 <motion.div className="approval-card" key={item.reservationId} whileHover={{ y: -4 }}>
74 <div className="approval-card-top">
75 <Badge value={item.status} />
76 <span>#{item.reservationId}</span>
77 </div>
78
79 <h3>{item.requesterName}</h3>
80 <p>{item.reservationDate} · {item.startTime} - {item.endTime}</p>
81
82 <div className="approval-meta">
83 <span>Room</span>
84 <strong>{item.roomCode || "No room"}</strong>
85 </div>
86
87 <div className="approval-meta">
88 <span>Building</span>
89 <strong>{item.buildingName || "No building"}</strong>
90 </div>
91
92 <div className="approval-meta column">
93 <span>Equipment</span>
94 <strong>{item.requestedEquipment || "No equipment"}</strong>
95 </div>
96
97 <div className="approval-actions">
98 <button className="success-button" onClick={() => saveDecision(item.reservationId, "approved")}>
99 <CheckCircle2 size={18} />
100 Approve
101 </button>
102
103 <button className="danger-button" onClick={() => saveDecision(item.reservationId, "rejected")}>
104 <XCircle size={18} />
105 Reject
106 </button>
107 </div>
108 </motion.div>
109 ))}
110 </div>
111 )}
112 </section>
113 );
114}
115
116function Hero({ eyebrow, title, subtitle }) {
117 return (
118 <div className="hero">
119 <div>
120 <div className="eyebrow">
121 <ClipboardCheck size={16} />
122 {eyebrow}
123 </div>
124 <h1>{title}</h1>
125 <p>{subtitle}</p>
126 </div>
127 </div>
128 );
129}
130
131function Badge({ value }) {
132 return <span className={`badge ${String(value || "").toLowerCase()}`}>{value}</span>;
133}
Note: See TracBrowser for help on using the repository browser.