source: frontend/src/pages/ReportsPage.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: 10.8 KB
RevLine 
[09e02d7]1import { useState } from "react";
2import toast from "react-hot-toast";
3import {
4 ArrowRight,
5 BarChart3,
6 Boxes,
7 CheckCircle2,
8 DoorOpen,
9 Loader2,
10 Sparkles,
11 Table2,
12 TrendingUp,
13} from "lucide-react";
14import { getEquipmentDemandReport, getRoomUtilizationReport } from "../api.js";
15
16export default function ReportsPage() {
17 const [roomReport, setRoomReport] = useState([]);
18 const [equipmentReport, setEquipmentReport] = useState([]);
19 const [activeReport, setActiveReport] = useState(null);
20 const [loadingRoomReport, setLoadingRoomReport] = useState(false);
21 const [loadingEquipmentReport, setLoadingEquipmentReport] = useState(false);
22
23 async function loadRoomReport() {
24 setLoadingRoomReport(true);
25
26 try {
27 const data = await getRoomUtilizationReport();
28 setRoomReport(Array.isArray(data) ? data : []);
29 setActiveReport("rooms");
30 toast.success("Room utilization report loaded");
31 } catch (err) {
32 toast.error(err.message || "Room utilization report could not be loaded.");
33 } finally {
34 setLoadingRoomReport(false);
35 }
36 }
37
38 async function loadEquipmentReport() {
39 setLoadingEquipmentReport(true);
40
41 try {
42 const data = await getEquipmentDemandReport();
43 setEquipmentReport(Array.isArray(data) ? data : []);
44 setActiveReport("equipment");
45 toast.success("Equipment demand report loaded");
46 } catch (err) {
47 toast.error(err.message || "Equipment demand report could not be loaded.");
48 } finally {
49 setLoadingEquipmentReport(false);
50 }
51 }
52
53 return (
54 <section className="reports-page-pro">
55 <div className="reports-hero-pro">
56 <div>
57 <div className="dashboard-eyebrow">
58 <BarChart3 size={16} />
59 Analytics center
60 </div>
61
62 <h1>Reports & Insights</h1>
63
64 <p>
65 Review room utilization, equipment demand and operational trends
66 using live data from the project database.
67 </p>
68 </div>
69
70 <div className="reports-hero-metrics">
71 <div>
72 <strong>2</strong>
73 <span>Advanced reports</span>
74 </div>
75
76 <div>
77 <strong>Live</strong>
78 <span>PostgreSQL data</span>
79 </div>
80
81 <div>
82 <strong>SQL</strong>
83 <span>Grouped analytics</span>
84 </div>
85 </div>
86 </div>
87
88 <div className="reports-command-grid">
89 <ReportCard
90 icon={DoorOpen}
91 tag="Quarterly analysis"
92 title="Room utilization"
93 description="Analyze how rooms are used across quarters, including reservation volume, approval status, approved hours and utilization ranking."
94 points={[
95 "Grouped by quarter, building and room",
96 "Approved, rejected, cancelled and pending counts",
97 "Room ranking by utilization level",
98 ]}
99 loading={loadingRoomReport}
100 onClick={loadRoomReport}
101 />
102
103 <ReportCard
104 icon={Boxes}
105 tag="Stock risk analysis"
106 title="Equipment demand"
107 description="Track requested equipment quantities and compare demand with registered stock and assigned room equipment."
108 points={[
109 "Demand by equipment type and quarter",
110 "Registered stock versus requested quantity",
111 "Demand ranking and stock risk level",
112 ]}
113 loading={loadingEquipmentReport}
114 onClick={loadEquipmentReport}
115 />
116 </div>
117
118 {!activeReport && (
119 <div className="reports-empty-panel">
120 <div className="reports-empty-icon">
121 <Table2 size={28} />
122 </div>
123
124 <div>
125 <h2>Select a report to view results</h2>
126 <p>
127 Choose one of the analytical reports above. Results will be loaded
128 from the live database and displayed in a structured table.
129 </p>
130 </div>
131 </div>
132 )}
133
134 {activeReport === "rooms" && roomReport.length > 0 && (
135 <PremiumTable
136 title="Room utilization report"
137 subtitle="Quarterly utilization grouped by building, room, reservation status and approved hours."
138 columns={[
139 "Quarter",
140 "Building",
141 "Room",
142 "Type",
143 "Total",
144 "Approved",
145 "Hours",
146 "Rank",
147 "Level",
148 ]}
149 rows={roomReport.map((row) => [
150 row.quarterStart,
151 row.buildingName,
152 row.roomCode,
153 row.type,
154 row.totalRoomReservations,
155 row.approvedReservations,
156 row.approvedHours,
157 row.utilizationRank,
158 row.utilizationLevel,
159 ])}
160 />
161 )}
162
163 {activeReport === "equipment" && equipmentReport.length > 0 && (
164 <PremiumTable
165 title="Equipment demand report"
166 subtitle="Demand, registered quantity, assigned equipment, stock risk and ranking by equipment type."
167 columns={[
168 "Quarter",
169 "Equipment",
170 "Stock",
171 "Assigned",
172 "Registered",
173 "Requested",
174 "Demand %",
175 "Rank",
176 "Level",
177 ]}
178 rows={equipmentReport.map((row) => [
179 row.quarterStart,
180 row.equipmentName,
181 row.stockQuantity,
182 row.assignedRoomQuantity,
183 row.totalRegisteredQuantity,
184 row.totalRequestedQuantity,
185 row.demandToRegisteredPercent,
186 row.demandRank,
187 row.demandLevel,
188 ])}
189 />
190 )}
191
192 {activeReport === "rooms" && !loadingRoomReport && roomReport.length === 0 && (
193 <EmptyResult title="No room utilization rows were returned." />
194 )}
195
196 {activeReport === "equipment" &&
197 !loadingEquipmentReport &&
198 equipmentReport.length === 0 && (
199 <EmptyResult title="No equipment demand rows were returned." />
200 )}
201 </section>
202 );
203}
204
205function ReportCard({
206 icon: Icon,
207 tag,
208 title,
209 description,
210 points,
211 loading,
212 onClick,
213 }) {
214 return (
215 <button
216 className="report-command-card"
217 type="button"
218 onClick={onClick}
219 disabled={loading}
220 >
221 <div className="report-command-top">
222 <div className="report-command-icon">
223 <Icon size={25} />
224 </div>
225
226 <span>{tag}</span>
227 </div>
228
229 <h2>{title}</h2>
230
231 <p>{description}</p>
232
233 <div className="report-point-list">
234 {points.map((point) => (
235 <div key={point}>
236 <CheckCircle2 size={17} />
237 {point}
238 </div>
239 ))}
240 </div>
241
242 <div className="report-command-footer">
243 <span>
244 {loading ? (
245 <>
246 Loading report
247 <Loader2 size={17} className="spin-icon" />
248 </>
249 ) : (
250 <>
251 Open report
252 <ArrowRight size={17} />
253 </>
254 )}
255 </span>
256
257 <TrendingUp size={20} />
258 </div>
259 </button>
260 );
261}
262
263function PremiumTable({ title, subtitle, columns, rows }) {
264 return (
265 <div className="panel table-panel report-output-panel">
266 <div className="panel-header report-table-header">
267 <div>
268 <div className="dashboard-eyebrow">
269 <Sparkles size={15} />
270 Report output
271 </div>
272
273 <h3>{title}</h3>
274 <p>{subtitle}</p>
275 </div>
276
277 <div className="report-row-count">
278 <strong>{rows.length}</strong>
279 <span>rows loaded</span>
280 </div>
281 </div>
282
283 <div className="table-wrapper">
284 <table>
285 <thead>
286 <tr>
287 {columns.map((column) => (
288 <th key={column}>{column}</th>
289 ))}
290 </tr>
291 </thead>
292
293 <tbody>
294 {rows.map((row, index) => (
295 <tr key={index}>
296 {row.map((cell, cellIndex) => (
297 <td key={cellIndex}>{String(cell ?? "")}</td>
298 ))}
299 </tr>
300 ))}
301 </tbody>
302 </table>
303 </div>
304 </div>
305 );
306}
307
308function EmptyResult({ title }) {
309 return (
310 <div className="reports-empty-panel">
311 <div className="reports-empty-icon">
312 <Table2 size={28} />
313 </div>
314
315 <div>
316 <h2>{title}</h2>
317 <p>
318 The query executed successfully, but there is no matching data for the
319 current dataset.
320 </p>
321 </div>
322 </div>
323 );
324}
Note: See TracBrowser for help on using the repository browser.