source: frontend/src/pages/CreateReservationPage.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: 11.7 KB
Line 
1import { useEffect, useState } from "react";
2import toast from "react-hot-toast";
3import { ArrowRight, PlusCircle, RefreshCcw, Search, UserRound, Wrench } from "lucide-react";
4import { createReservation, getEquipment, searchRooms } from "../api.js";
5
6export default function CreateReservationPage({ activeUser }) {
7 const [equipment, setEquipment] = useState([]);
8 const [rooms, setRooms] = useState([]);
9
10 const [form, setForm] = useState({
11 reservationDate: "2026-07-20",
12 startTime: "09:00",
13 endTime: "10:00",
14 minimumCapacity: 1,
15 roomType: "",
16 requiredEquipmentName: "",
17 });
18
19 const [selectedRoomId, setSelectedRoomId] = useState("");
20 const [equipmentId, setEquipmentId] = useState("");
21 const [quantity, setQuantity] = useState(1);
22 const [selectedEquipment, setSelectedEquipment] = useState([]);
23 const [created, setCreated] = useState(null);
24 const [loading, setLoading] = useState(false);
25
26 useEffect(() => {
27 getEquipment()
28 .then(setEquipment)
29 .catch((err) => toast.error(err.message));
30 }, []);
31
32 function updateField(field, value) {
33 setForm((prev) => ({ ...prev, [field]: value }));
34 }
35
36 async function loadAvailableRooms(e) {
37 e.preventDefault();
38 setCreated(null);
39
40 if (!form.reservationDate || !form.startTime || !form.endTime) {
41 toast.error("Reservation date, start time and end time are required.");
42 return;
43 }
44
45 if (form.endTime <= form.startTime) {
46 toast.error("End time must be after start time.");
47 return;
48 }
49
50 if (Number(form.minimumCapacity) < 1) {
51 toast.error("Minimum capacity must be greater than 0.");
52 return;
53 }
54
55 try {
56 const result = await searchRooms({
57 ...form,
58 minimumCapacity: Number(form.minimumCapacity),
59 });
60
61 setRooms(result);
62 setSelectedRoomId("");
63 toast.success(`${result.length} rooms loaded`);
64 } catch (err) {
65 setRooms([]);
66 toast.error(err.message);
67 }
68 }
69
70 function addEquipment() {
71 if (!equipmentId) {
72 toast.error("Select equipment first.");
73 return;
74 }
75
76 if (Number(quantity) <= 0) {
77 toast.error("Quantity must be greater than 0.");
78 return;
79 }
80
81 const selected = equipment.find((item) => item.equipmentId === Number(equipmentId));
82
83 setSelectedEquipment((prev) => {
84 const existingItem = prev.find((item) => item.equipmentId === Number(equipmentId));
85
86 if (existingItem) {
87 return prev.map((item) =>
88 item.equipmentId === Number(equipmentId)
89 ? {
90 ...item,
91 requestedQuantity: item.requestedQuantity + Number(quantity),
92 }
93 : item
94 );
95 }
96
97 return [
98 ...prev,
99 {
100 equipmentId: Number(equipmentId),
101 requestedQuantity: Number(quantity),
102 name: selected?.name || `Equipment ${equipmentId}`,
103 },
104 ];
105 });
106
107 setEquipmentId("");
108 setQuantity(1);
109 toast.success("Equipment added");
110 }
111
112 function removeEquipment(index) {
113 setSelectedEquipment((prev) => prev.filter((_, i) => i !== index));
114 }
115
116 async function submitReservation(e) {
117 e.preventDefault();
118 setCreated(null);
119
120 if (!activeUser?.userId) {
121 toast.error("Logged-in requester is missing.");
122 return;
123 }
124
125 if (!form.reservationDate || !form.startTime || !form.endTime) {
126 toast.error("Reservation date, start time and end time are required.");
127 return;
128 }
129
130 if (form.endTime <= form.startTime) {
131 toast.error("End time must be after start time.");
132 return;
133 }
134
135 const hasRoom = Boolean(selectedRoomId);
136 const hasEquipment = selectedEquipment.length > 0;
137
138 if (!hasRoom && !hasEquipment) {
139 toast.error("Please select a room or add equipment before creating a reservation.");
140 return;
141 }
142
143 setLoading(true);
144
145 try {
146 const result = await createReservation({
147 userId: activeUser.userId,
148 reservationDate: form.reservationDate,
149 startTime: form.startTime,
150 endTime: form.endTime,
151 roomId: selectedRoomId ? Number(selectedRoomId) : null,
152 equipment: selectedEquipment.map((item) => ({
153 equipmentId: item.equipmentId,
154 requestedQuantity: item.requestedQuantity,
155 })),
156 });
157
158 setCreated(result);
159 toast.success(`Reservation #${result.reservationId} created`);
160 } catch (err) {
161 toast.error(err.message);
162 } finally {
163 setLoading(false);
164 }
165 }
166
167 return (
168 <section>
169 <Hero
170 eyebrow="Reservation workflow"
171 title="Create Reservation Request"
172 subtitle="Create a room or equipment reservation request and submit it for approval."
173 />
174
175 <div className="wizard-steps">
176 <Step number="01" title="Define interval" />
177 <Step number="02" title="Choose room" />
178 <Step number="03" title="Add equipment" />
179 <Step number="04" title="Submit request" />
180 </div>
181
182 <div className="workspace-grid">
183 <form className="panel page-form" onSubmit={loadAvailableRooms}>
184 <SectionTitle
185 icon={Search}
186 title="1. Load available rooms"
187 subtitle="Search the database before choosing a room."
188 />
189
190 <div className="form-grid">
191 <Input label="Reservation date" type="date" value={form.reservationDate} onChange={(v) => updateField("reservationDate", v)} />
192 <Input label="Start time" type="time" value={form.startTime} onChange={(v) => updateField("startTime", v)} />
193 <Input label="End time" type="time" value={form.endTime} onChange={(v) => updateField("endTime", v)} />
194 <Input label="Minimum capacity" type="number" value={form.minimumCapacity} onChange={(v) => updateField("minimumCapacity", v)} />
195 </div>
196
197 <button className="secondary full">
198 Load available rooms
199 <RefreshCcw size={18} />
200 </button>
201 </form>
202
203 <form className="panel page-form" onSubmit={submitReservation}>
204 <SectionTitle
205 icon={PlusCircle}
206 title="2. Submit reservation"
207 subtitle="The requester is the currently logged-in database user."
208 />
209
210 <div className="requester-card">
211 <div className="avatar">{getInitials(activeUser.fullName)}</div>
212 <div>
213 <span>Requester</span>
214 <strong>{activeUser.fullName}</strong>
215 </div>
216 <Badge value={activeUser.role} />
217 </div>
218
219 <label>
220 Selected room
221 <select value={selectedRoomId} onChange={(e) => setSelectedRoomId(e.target.value)}>
222 <option value="">No room / equipment-only reservation</option>
223 {rooms.map((room) => (
224 <option key={room.roomId} value={room.roomId}>
225 {room.roomCode} | {room.type} | capacity {room.capacity}
226 </option>
227 ))}
228 </select>
229 </label>
230
231 <div className="equipment-box">
232 <h4>Optional equipment</h4>
233
234 <label>
235 Equipment
236 <select value={equipmentId} onChange={(e) => setEquipmentId(e.target.value)}>
237 <option value="">Select equipment</option>
238 {equipment.map((item) => (
239 <option key={item.equipmentId} value={item.equipmentId}>
240 {item.name} | stock {item.stockQuantity}
241 </option>
242 ))}
243 </select>
244 </label>
245
246 <Input label="Quantity" type="number" value={quantity} onChange={setQuantity} />
247
248 <button type="button" className="secondary full" onClick={addEquipment}>
249 Add equipment
250 <Wrench size={18} />
251 </button>
252
253 {selectedEquipment.length > 0 && (
254 <ul className="selected-list">
255 {selectedEquipment.map((item, index) => (
256 <li key={`${item.equipmentId}-${index}`}>
257 <span>{item.name} x{item.requestedQuantity}</span>
258 <button type="button" onClick={() => removeEquipment(index)}>
259 Remove
260 </button>
261 </li>
262 ))}
263 </ul>
264 )}
265 </div>
266
267 <button className="primary full" disabled={loading}>
268 {loading ? "Creating..." : "Create reservation"}
269 <ArrowRight size={18} />
270 </button>
271 </form>
272 </div>
273
274 {created && (
275 <div className="result-card">
276 <h3>Reservation created successfully</h3>
277 <pre>{JSON.stringify(created, null, 2)}</pre>
278 </div>
279 )}
280 </section>
281 );
282}
283
284function Hero({ eyebrow, title, subtitle }) {
285 return (
286 <div className="hero">
287 <div>
288 <div className="eyebrow">
289 <PlusCircle size={16} />
290 {eyebrow}
291 </div>
292 <h1>{title}</h1>
293 <p>{subtitle}</p>
294 </div>
295 </div>
296 );
297}
298
299function SectionTitle({ icon: Icon, title, subtitle }) {
300 return (
301 <div className="section-title">
302 <Icon size={21} />
303 <div>
304 <h3>{title}</h3>
305 <p>{subtitle}</p>
306 </div>
307 </div>
308 );
309}
310
311function Step({ number, title }) {
312 return (
313 <div className="step">
314 <span>{number}</span>
315 <strong>{title}</strong>
316 </div>
317 );
318}
319
320function Input({ label, type = "text", value, onChange }) {
321 return (
322 <label>
323 {label}
324 <input type={type} value={value} onChange={(e) => onChange(e.target.value)} />
325 </label>
326 );
327}
328
329function Badge({ value }) {
330 return <span className={`badge ${String(value || "").toLowerCase()}`}>{value}</span>;
331}
332
333function getInitials(name) {
334 return String(name || "U")
335 .split(" ")
336 .map((part) => part[0])
337 .slice(0, 2)
338 .join("")
339 .toUpperCase();
340}
Note: See TracBrowser for help on using the repository browser.