| | 1 | = Advanced Reports = |
| | 2 | |
| | 3 | == 1. Resource Utilization and Demand Analysis == |
| | 4 | |
| | 5 | Quarterly report showing each resource's utilization rate, peak usage hours, busiest day, approval rate, and demand ranking. Helps administrators identify underutilized resources that could be repurposed and overdemanded resources that may need capacity expansion or alternatives. |
| | 6 | |
| | 7 | === Solution SQL === |
| | 8 | |
| | 9 | {{{ |
| | 10 | SET search_path TO project; |
| | 11 | |
| | 12 | WITH quarter_bounds AS ( |
| | 13 | SELECT DATE '2026-01-01' AS q_start, DATE '2026-03-31' AS q_end |
| | 14 | ), |
| | 15 | quarter_days AS ( |
| | 16 | SELECT d::DATE AS day, EXTRACT(ISODOW FROM d)::INT AS dow |
| | 17 | FROM quarter_bounds qb, generate_series(qb.q_start, qb.q_end, '1 day'::INTERVAL) AS d |
| | 18 | ), |
| | 19 | resource_availability AS ( |
| | 20 | SELECT |
| | 21 | r.resource_id, |
| | 22 | SUM(CASE |
| | 23 | WHEN qd.dow <= 5 OR r.available_weekends |
| | 24 | THEN EXTRACT(EPOCH FROM (r.available_to - r.available_from)) / 3600.0 |
| | 25 | ELSE 0 |
| | 26 | END) AS total_available_hours |
| | 27 | FROM resources r |
| | 28 | CROSS JOIN quarter_days qd |
| | 29 | GROUP BY r.resource_id |
| | 30 | ), |
| | 31 | reservation_stats AS ( |
| | 32 | SELECT |
| | 33 | rv.resource_id, |
| | 34 | COUNT(*) AS total_reservations, |
| | 35 | COUNT(*) FILTER (WHERE rv.status IN ('approved', 'completed')) AS approved_count, |
| | 36 | COUNT(*) FILTER (WHERE rv.status = 'rejected') AS rejected_count, |
| | 37 | COUNT(*) FILTER (WHERE rv.status = 'cancelled') AS cancelled_count, |
| | 38 | ROUND(SUM(EXTRACT(EPOCH FROM (rv.end_time - rv.start_time)) / 3600.0) |
| | 39 | FILTER (WHERE rv.status IN ('approved', 'completed')), 1) AS reserved_hours |
| | 40 | FROM reservations rv, quarter_bounds qb |
| | 41 | WHERE rv.start_time >= qb.q_start |
| | 42 | AND rv.start_time < qb.q_end + INTERVAL '1 day' |
| | 43 | GROUP BY rv.resource_id |
| | 44 | ), |
| | 45 | popular_day AS ( |
| | 46 | SELECT DISTINCT ON (rv.resource_id) |
| | 47 | rv.resource_id, |
| | 48 | TRIM(TO_CHAR(rv.start_time, 'Day')) AS busiest_day |
| | 49 | FROM reservations rv, quarter_bounds qb |
| | 50 | WHERE rv.start_time >= qb.q_start |
| | 51 | AND rv.start_time < qb.q_end + INTERVAL '1 day' |
| | 52 | AND rv.status IN ('approved', 'completed') |
| | 53 | GROUP BY rv.resource_id, TRIM(TO_CHAR(rv.start_time, 'Day')) |
| | 54 | ORDER BY rv.resource_id, COUNT(*) DESC |
| | 55 | ), |
| | 56 | peak_hour AS ( |
| | 57 | SELECT DISTINCT ON (rv.resource_id) |
| | 58 | rv.resource_id, |
| | 59 | EXTRACT(HOUR FROM rv.start_time)::INT AS peak_hour |
| | 60 | FROM reservations rv, quarter_bounds qb |
| | 61 | WHERE rv.start_time >= qb.q_start |
| | 62 | AND rv.start_time < qb.q_end + INTERVAL '1 day' |
| | 63 | AND rv.status IN ('approved', 'completed') |
| | 64 | GROUP BY rv.resource_id, EXTRACT(HOUR FROM rv.start_time) |
| | 65 | ORDER BY rv.resource_id, COUNT(*) DESC |
| | 66 | ) |
| | 67 | SELECT |
| | 68 | r.name AS resource_name, |
| | 69 | rt.type_name AS resource_type, |
| | 70 | COALESCE(l.building || ' ' || l.room, 'Digital') AS location, |
| | 71 | COALESCE(rs.total_reservations, 0) AS total_reservations, |
| | 72 | COALESCE(rs.approved_count, 0) AS approved, |
| | 73 | COALESCE(rs.rejected_count, 0) AS rejected, |
| | 74 | COALESCE(rs.cancelled_count, 0) AS cancelled, |
| | 75 | COALESCE(ROUND(rs.approved_count::NUMERIC / NULLIF(rs.total_reservations, 0) * 100, 1), 0) |
| | 76 | AS approval_rate_pct, |
| | 77 | COALESCE(rs.reserved_hours, 0) AS reserved_hours, |
| | 78 | ROUND(ra.total_available_hours, 0) AS available_hours, |
| | 79 | COALESCE(ROUND(rs.reserved_hours / NULLIF(ra.total_available_hours, 0) * 100, 2), 0) |
| | 80 | AS utilization_pct, |
| | 81 | COALESCE(pd.busiest_day, '-') AS busiest_day, |
| | 82 | COALESCE(ph.peak_hour || ':00', '-') AS peak_hour, |
| | 83 | RANK() OVER ( |
| | 84 | ORDER BY COALESCE(rs.reserved_hours, 0) |
| | 85 | / NULLIF(ra.total_available_hours, 0) DESC NULLS LAST |
| | 86 | ) AS demand_rank |
| | 87 | FROM resources r |
| | 88 | JOIN resource_types rt ON r.type_id = rt.type_id |
| | 89 | LEFT JOIN locations l ON r.location_id = l.location_id |
| | 90 | JOIN resource_availability ra ON r.resource_id = ra.resource_id |
| | 91 | LEFT JOIN reservation_stats rs ON r.resource_id = rs.resource_id |
| | 92 | LEFT JOIN popular_day pd ON r.resource_id = pd.resource_id |
| | 93 | LEFT JOIN peak_hour ph ON r.resource_id = ph.resource_id |
| | 94 | ORDER BY utilization_pct DESC; |
| | 95 | }}} |
| | 96 | |
| | 97 | === Solution Relational Algebra === |
| | 98 | |
| | 99 | {{{ |
| | 100 | QuarterDays <- π day, dow ( |
| | 101 | σ day >= '2026-01-01' ∧ day <= '2026-03-31' (CalendarDays) |
| | 102 | ) |
| | 103 | |
| | 104 | ResourceAvailability <- |
| | 105 | γ resource_id; total_available_hours := SUM(daily_hours) ( |
| | 106 | π resource_id, (CASE dow ≤ 5 ∨ available_weekends THEN hours ELSE 0) → daily_hours ( |
| | 107 | resources × QuarterDays |
| | 108 | ) |
| | 109 | ) |
| | 110 | |
| | 111 | ReservationStats <- |
| | 112 | γ resource_id; |
| | 113 | total_reservations := COUNT(*), |
| | 114 | approved_count := COUNT(σ status ∈ {'approved','completed'}), |
| | 115 | rejected_count := COUNT(σ status = 'rejected'), |
| | 116 | cancelled_count := COUNT(σ status = 'cancelled'), |
| | 117 | reserved_hours := SUM(duration_hours WHERE status ∈ {'approved','completed'}) ( |
| | 118 | σ start_time >= '2026-01-01' ∧ start_time < '2026-04-01' ( |
| | 119 | π resource_id, status, (end_time − start_time)/3600 → duration_hours (reservations) |
| | 120 | ) |
| | 121 | ) |
| | 122 | |
| | 123 | PopularDay <- |
| | 124 | γ resource_id; busiest_day := DAY_NAME(start_time) HAVING MAX(COUNT(*)) ( |
| | 125 | σ status ∈ {'approved','completed'} ∧ start_time ∈ Quarter (reservations) |
| | 126 | ) |
| | 127 | |
| | 128 | PeakHour <- |
| | 129 | γ resource_id; peak_hour := HOUR(start_time) HAVING MAX(COUNT(*)) ( |
| | 130 | σ status ∈ {'approved','completed'} ∧ start_time ∈ Quarter (reservations) |
| | 131 | ) |
| | 132 | |
| | 133 | Result <- |
| | 134 | π resource_name, resource_type, location, total_reservations, approved_count, |
| | 135 | rejected_count, cancelled_count, approval_rate_pct, reserved_hours, |
| | 136 | available_hours, utilization_pct, busiest_day, peak_hour, demand_rank ( |
| | 137 | ( |
| | 138 | ( |
| | 139 | ( |
| | 140 | (resources ⨝ resource_types) |
| | 141 | ⟕ locations |
| | 142 | ) |
| | 143 | ⨝ ResourceAvailability |
| | 144 | ) |
| | 145 | ⟕ ReservationStats |
| | 146 | ) |
| | 147 | ⟕ PopularDay |
| | 148 | ⟕ PeakHour |
| | 149 | ) |
| | 150 | }}} |
| | 151 | |
| | 152 | == 2. Monthly Reservation Trends with Cumulative Statistics == |
| | 153 | |
| | 154 | Monthly time-series report showing reservation counts, approval rates, month-over-month growth, cumulative totals, and the top 3 most demanded resources per month. Useful for identifying seasonal patterns and forecasting future demand at the semester or academic year level. |
| | 155 | |
| | 156 | === Solution SQL === |
| | 157 | |
| | 158 | {{{ |
| | 159 | SET search_path TO project; |
| | 160 | |
| | 161 | WITH monthly_overview AS ( |
| | 162 | SELECT |
| | 163 | DATE_TRUNC('month', rv.start_time) AS month, |
| | 164 | COUNT(*) AS total_reservations, |
| | 165 | COUNT(*) FILTER (WHERE rv.status IN ('approved', 'completed')) AS approved, |
| | 166 | COUNT(*) FILTER (WHERE rv.status = 'rejected') AS rejected, |
| | 167 | COUNT(*) FILTER (WHERE rv.status = 'cancelled') AS cancelled, |
| | 168 | COUNT(*) FILTER (WHERE rv.status = 'pending') AS pending, |
| | 169 | COUNT(DISTINCT rv.user_id) AS unique_users, |
| | 170 | COUNT(DISTINCT rv.resource_id) AS unique_resources, |
| | 171 | ROUND(AVG(EXTRACT(EPOCH FROM (rv.end_time - rv.start_time)) / 3600.0), 1) |
| | 172 | AS avg_duration_hours, |
| | 173 | ROUND(SUM(EXTRACT(EPOCH FROM (rv.end_time - rv.start_time)) / 3600.0) |
| | 174 | FILTER (WHERE rv.status IN ('approved', 'completed')), 1) |
| | 175 | AS total_approved_hours |
| | 176 | FROM reservations rv |
| | 177 | GROUP BY DATE_TRUNC('month', rv.start_time) |
| | 178 | ), |
| | 179 | monthly_trends AS ( |
| | 180 | SELECT |
| | 181 | TO_CHAR(month, 'YYYY-MM') AS month_label, |
| | 182 | month, |
| | 183 | total_reservations, |
| | 184 | approved, |
| | 185 | rejected, |
| | 186 | cancelled, |
| | 187 | pending, |
| | 188 | unique_users, |
| | 189 | unique_resources, |
| | 190 | avg_duration_hours, |
| | 191 | total_approved_hours, |
| | 192 | SUM(total_reservations) OVER (ORDER BY month) AS cumulative_total, |
| | 193 | ROUND(approved::NUMERIC / NULLIF(total_reservations, 0) * 100, 1) AS approval_rate, |
| | 194 | CASE |
| | 195 | WHEN LAG(total_reservations) OVER (ORDER BY month) IS NULL THEN NULL |
| | 196 | ELSE ROUND( |
| | 197 | (total_reservations - LAG(total_reservations) OVER (ORDER BY month))::NUMERIC |
| | 198 | / LAG(total_reservations) OVER (ORDER BY month) * 100, 1 |
| | 199 | ) |
| | 200 | END AS mom_change_pct |
| | 201 | FROM monthly_overview |
| | 202 | ), |
| | 203 | resource_demand_ranked AS ( |
| | 204 | SELECT |
| | 205 | TO_CHAR(DATE_TRUNC('month', rv.start_time), 'YYYY-MM') AS month_label, |
| | 206 | r.name AS resource_name, |
| | 207 | COUNT(*) AS demand_count, |
| | 208 | ROW_NUMBER() OVER ( |
| | 209 | PARTITION BY DATE_TRUNC('month', rv.start_time) |
| | 210 | ORDER BY COUNT(*) DESC |
| | 211 | ) AS rank |
| | 212 | FROM reservations rv |
| | 213 | JOIN resources r ON rv.resource_id = r.resource_id |
| | 214 | GROUP BY DATE_TRUNC('month', rv.start_time), r.name |
| | 215 | ), |
| | 216 | top_resources AS ( |
| | 217 | SELECT |
| | 218 | month_label, |
| | 219 | STRING_AGG(resource_name || ' (' || demand_count || ')', |
| | 220 | ', ' ORDER BY rank) AS top_3_resources |
| | 221 | FROM resource_demand_ranked |
| | 222 | WHERE rank <= 3 |
| | 223 | GROUP BY month_label |
| | 224 | ) |
| | 225 | SELECT |
| | 226 | mt.month_label, |
| | 227 | mt.total_reservations, |
| | 228 | mt.approved, |
| | 229 | mt.rejected, |
| | 230 | mt.cancelled, |
| | 231 | mt.pending, |
| | 232 | mt.approval_rate AS approval_rate_pct, |
| | 233 | COALESCE(mt.mom_change_pct || '%', 'N/A') AS month_over_month, |
| | 234 | mt.cumulative_total, |
| | 235 | mt.unique_users, |
| | 236 | mt.unique_resources, |
| | 237 | mt.avg_duration_hours, |
| | 238 | mt.total_approved_hours, |
| | 239 | COALESCE(tr.top_3_resources, '-') AS top_demanded_resources |
| | 240 | FROM monthly_trends mt |
| | 241 | LEFT JOIN top_resources tr ON mt.month_label = tr.month_label |
| | 242 | ORDER BY mt.month_label; |
| | 243 | }}} |
| | 244 | |
| | 245 | === Solution Relational Algebra === |
| | 246 | |
| | 247 | {{{ |
| | 248 | MonthlyOverview <- |
| | 249 | γ month := TRUNC_MONTH(start_time); |
| | 250 | total_reservations := COUNT(*), |
| | 251 | approved := COUNT(σ status ∈ {'approved','completed'}), |
| | 252 | rejected := COUNT(σ status = 'rejected'), |
| | 253 | cancelled := COUNT(σ status = 'cancelled'), |
| | 254 | pending := COUNT(σ status = 'pending'), |
| | 255 | unique_users := COUNT_DISTINCT(user_id), |
| | 256 | unique_resources := COUNT_DISTINCT(resource_id), |
| | 257 | avg_duration_hours := AVG(duration), |
| | 258 | total_approved_hours := SUM(duration WHERE status ∈ {'approved','completed'}) ( |
| | 259 | π start_time, status, user_id, resource_id, |
| | 260 | (end_time − start_time)/3600 → duration (reservations) |
| | 261 | ) |
| | 262 | |
| | 263 | MonthlyTrends <- |
| | 264 | π month_label, total_reservations, approved, rejected, cancelled, pending, |
| | 265 | approval_rate, cumulative_total, mom_change_pct ( |
| | 266 | ω ORDER BY month; |
| | 267 | cumulative_total := SUM(total_reservations), |
| | 268 | mom_change_pct := (total_reservations − LAG(total_reservations)) |
| | 269 | / LAG(total_reservations) * 100 ( |
| | 270 | MonthlyOverview |
| | 271 | ) |
| | 272 | ) |
| | 273 | |
| | 274 | ResourceDemandRanked <- |
| | 275 | ω PARTITION BY month ORDER BY demand_count DESC; |
| | 276 | rank := ROW_NUMBER() ( |
| | 277 | γ month := TRUNC_MONTH(start_time), resource_name := r.name; |
| | 278 | demand_count := COUNT(*) ( |
| | 279 | reservations rv ⨝ (rv.resource_id = r.resource_id) resources r |
| | 280 | ) |
| | 281 | ) |
| | 282 | |
| | 283 | TopResources <- |
| | 284 | γ month_label; top_3 := CONCAT(resource_name, demand_count) ( |
| | 285 | σ rank ≤ 3 (ResourceDemandRanked) |
| | 286 | ) |
| | 287 | |
| | 288 | Result <- |
| | 289 | π month_label, total_reservations, approved, rejected, cancelled, pending, |
| | 290 | approval_rate, mom_change_pct, cumulative_total, top_3 ( |
| | 291 | MonthlyTrends ⟕ (month_label) TopResources |
| | 292 | ) |
| | 293 | }}} |
| | 294 | |
| | 295 | == 3. User Activity and Behavior Analysis == |
| | 296 | |
| | 297 | Per-user report showing total reservations, approval and cancellation rates, most-used resource, total hours consumed, and activity ranking. Helps identify the most active users, detect problematic patterns (high cancellation rates), and understand how different user types (students vs. teaching staff) use the system. |
| | 298 | |
| | 299 | === Solution SQL === |
| | 300 | |
| | 301 | {{{ |
| | 302 | SET search_path TO project; |
| | 303 | |
| | 304 | WITH user_stats AS ( |
| | 305 | SELECT |
| | 306 | rv.user_id, |
| | 307 | COUNT(*) AS total_reservations, |
| | 308 | COUNT(*) FILTER (WHERE rv.status IN ('approved', 'completed')) AS approved, |
| | 309 | COUNT(*) FILTER (WHERE rv.status = 'rejected') AS rejected, |
| | 310 | COUNT(*) FILTER (WHERE rv.status = 'cancelled') AS cancelled, |
| | 311 | ROUND(COUNT(*) FILTER (WHERE rv.status = 'cancelled')::NUMERIC |
| | 312 | / NULLIF(COUNT(*), 0) * 100, 1) AS cancellation_rate, |
| | 313 | ROUND(AVG(EXTRACT(EPOCH FROM (rv.end_time - rv.start_time)) / 3600.0), 1) |
| | 314 | AS avg_duration_hours, |
| | 315 | ROUND(SUM(EXTRACT(EPOCH FROM (rv.end_time - rv.start_time)) / 3600.0) |
| | 316 | FILTER (WHERE rv.status IN ('approved', 'completed')), 1) AS total_hours_used, |
| | 317 | COUNT(DISTINCT rv.resource_id) AS distinct_resources_used |
| | 318 | FROM reservations rv |
| | 319 | GROUP BY rv.user_id |
| | 320 | ), |
| | 321 | favorite_resource AS ( |
| | 322 | SELECT DISTINCT ON (rv.user_id) |
| | 323 | rv.user_id, |
| | 324 | r.name AS favorite_resource, |
| | 325 | COUNT(*) AS use_count |
| | 326 | FROM reservations rv |
| | 327 | JOIN resources r ON rv.resource_id = r.resource_id |
| | 328 | WHERE rv.status IN ('approved', 'completed') |
| | 329 | GROUP BY rv.user_id, r.name |
| | 330 | ORDER BY rv.user_id, COUNT(*) DESC |
| | 331 | ) |
| | 332 | SELECT |
| | 333 | u.first_name || ' ' || u.last_name AS user_name, |
| | 334 | ut.type_name AS user_type, |
| | 335 | COALESCE(us.total_reservations, 0) AS total_reservations, |
| | 336 | COALESCE(us.approved, 0) AS approved, |
| | 337 | COALESCE(us.rejected, 0) AS rejected, |
| | 338 | COALESCE(us.cancelled, 0) AS cancelled, |
| | 339 | COALESCE(us.cancellation_rate, 0) AS cancellation_rate_pct, |
| | 340 | COALESCE(us.avg_duration_hours, 0) AS avg_duration_hours, |
| | 341 | COALESCE(us.total_hours_used, 0) AS total_hours_used, |
| | 342 | COALESCE(us.distinct_resources_used, 0) AS distinct_resources, |
| | 343 | COALESCE(fr.favorite_resource, '-') AS most_used_resource, |
| | 344 | COALESCE(fr.use_count, 0) AS most_used_count, |
| | 345 | RANK() OVER (ORDER BY COALESCE(us.total_reservations, 0) DESC) AS activity_rank |
| | 346 | FROM users u |
| | 347 | JOIN user_types ut ON u.type_id = ut.type_id |
| | 348 | LEFT JOIN user_stats us ON u.user_id = us.user_id |
| | 349 | LEFT JOIN favorite_resource fr ON u.user_id = fr.user_id |
| | 350 | ORDER BY activity_rank; |
| | 351 | }}} |
| | 352 | |
| | 353 | === Solution Relational Algebra === |
| | 354 | |
| | 355 | {{{ |
| | 356 | UserStats <- |
| | 357 | γ user_id; |
| | 358 | total_reservations := COUNT(*), |
| | 359 | approved := COUNT(σ status ∈ {'approved','completed'}), |
| | 360 | rejected := COUNT(σ status = 'rejected'), |
| | 361 | cancelled := COUNT(σ status = 'cancelled'), |
| | 362 | cancellation_rate := COUNT(σ status='cancelled') / COUNT(*) * 100, |
| | 363 | avg_duration_hours := AVG(duration), |
| | 364 | total_hours_used := SUM(duration WHERE status ∈ {'approved','completed'}), |
| | 365 | distinct_resources := COUNT_DISTINCT(resource_id) ( |
| | 366 | π user_id, status, resource_id, |
| | 367 | (end_time − start_time)/3600 → duration (reservations) |
| | 368 | ) |
| | 369 | |
| | 370 | FavoriteResource <- |
| | 371 | γ user_id; favorite_resource := r.name HAVING MAX(use_count) ( |
| | 372 | γ user_id, r.name; use_count := COUNT(*) ( |
| | 373 | σ status ∈ {'approved','completed'} ( |
| | 374 | reservations rv ⨝ (rv.resource_id = r.resource_id) resources r |
| | 375 | ) |
| | 376 | ) |
| | 377 | ) |
| | 378 | |
| | 379 | Result <- |
| | 380 | π user_name, user_type, total_reservations, approved, rejected, cancelled, |
| | 381 | cancellation_rate, avg_duration_hours, total_hours_used, |
| | 382 | distinct_resources, favorite_resource, activity_rank ( |
| | 383 | ( |
| | 384 | (users u ⨝ (u.type_id = ut.type_id) user_types ut) |
| | 385 | ⟕ (u.user_id = us.user_id) UserStats us |
| | 386 | ) |
| | 387 | ⟕ (u.user_id = fr.user_id) FavoriteResource fr |
| | 388 | ) |
| | 389 | }}} |
| | 390 | |
| | 391 | == 4. Administrator Approval Workload and Bottleneck Analysis == |
| | 392 | |
| | 393 | Shows each administrator's approval workload, approval/rejection rates, workload share percentage, and system-wide pending reservation wait times. Helps management identify if approval responsibilities are evenly distributed and whether pending reservations are being handled in a timely manner. |
| | 394 | |
| | 395 | === Solution SQL === |
| | 396 | |
| | 397 | {{{ |
| | 398 | SET search_path TO project; |
| | 399 | |
| | 400 | WITH admin_stats AS ( |
| | 401 | SELECT |
| | 402 | rv.approved_by AS admin_id, |
| | 403 | COUNT(*) AS total_reviewed, |
| | 404 | COUNT(*) FILTER (WHERE rv.status IN ('approved', 'completed')) AS approved_count, |
| | 405 | COUNT(*) FILTER (WHERE rv.status = 'rejected') AS rejected_count, |
| | 406 | ROUND(COUNT(*) FILTER (WHERE rv.status IN ('approved', 'completed'))::NUMERIC |
| | 407 | / NULLIF(COUNT(*), 0) * 100, 1) AS approval_rate, |
| | 408 | COUNT(DISTINCT rv.resource_id) AS distinct_resources_handled, |
| | 409 | COUNT(DISTINCT rv.user_id) AS distinct_users_served |
| | 410 | FROM reservations rv |
| | 411 | WHERE rv.approved_by IS NOT NULL |
| | 412 | GROUP BY rv.approved_by |
| | 413 | ), |
| | 414 | pending_stats AS ( |
| | 415 | SELECT |
| | 416 | COUNT(*) AS total_pending, |
| | 417 | ROUND(AVG(EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - rv.created_at)) |
| | 418 | / 86400.0), 1) AS avg_wait_days, |
| | 419 | MAX(EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - rv.created_at)) |
| | 420 | / 86400.0)::INT AS max_wait_days |
| | 421 | FROM reservations rv |
| | 422 | WHERE rv.status = 'pending' |
| | 423 | ), |
| | 424 | workload_share AS ( |
| | 425 | SELECT |
| | 426 | admin_id, |
| | 427 | total_reviewed, |
| | 428 | ROUND(total_reviewed::NUMERIC / SUM(total_reviewed) OVER () * 100, 1) |
| | 429 | AS workload_share_pct |
| | 430 | FROM admin_stats |
| | 431 | ) |
| | 432 | SELECT |
| | 433 | u.first_name || ' ' || u.last_name AS administrator, |
| | 434 | COALESCE(ast.total_reviewed, 0) AS total_reviewed, |
| | 435 | COALESCE(ast.approved_count, 0) AS approved, |
| | 436 | COALESCE(ast.rejected_count, 0) AS rejected, |
| | 437 | COALESCE(ast.approval_rate, 0) AS approval_rate_pct, |
| | 438 | COALESCE(ws.workload_share_pct, 0) AS workload_share_pct, |
| | 439 | COALESCE(ast.distinct_resources_handled, 0) AS resources_handled, |
| | 440 | COALESCE(ast.distinct_users_served, 0) AS users_served, |
| | 441 | ps.total_pending AS system_pending_count, |
| | 442 | ps.avg_wait_days AS pending_avg_wait_days, |
| | 443 | ps.max_wait_days AS pending_max_wait_days |
| | 444 | FROM users u |
| | 445 | JOIN user_types ut ON u.type_id = ut.type_id |
| | 446 | LEFT JOIN admin_stats ast ON u.user_id = ast.admin_id |
| | 447 | LEFT JOIN workload_share ws ON u.user_id = ws.admin_id |
| | 448 | CROSS JOIN pending_stats ps |
| | 449 | WHERE ut.type_name = 'Administrator' |
| | 450 | ORDER BY total_reviewed DESC; |
| | 451 | }}} |
| | 452 | |
| | 453 | === Solution Relational Algebra === |
| | 454 | |
| | 455 | {{{ |
| | 456 | AdminStats <- |
| | 457 | γ admin_id := approved_by; |
| | 458 | total_reviewed := COUNT(*), |
| | 459 | approved_count := COUNT(σ status ∈ {'approved','completed'}), |
| | 460 | rejected_count := COUNT(σ status = 'rejected'), |
| | 461 | approval_rate := COUNT(σ status ∈ {'approved','completed'}) / COUNT(*) * 100, |
| | 462 | distinct_resources := COUNT_DISTINCT(resource_id), |
| | 463 | distinct_users := COUNT_DISTINCT(user_id) ( |
| | 464 | σ approved_by IS NOT NULL (reservations) |
| | 465 | ) |
| | 466 | |
| | 467 | PendingStats <- |
| | 468 | γ total_pending := COUNT(*), |
| | 469 | avg_wait_days := AVG((NOW() − created_at) / 86400), |
| | 470 | max_wait_days := MAX((NOW() − created_at) / 86400) ( |
| | 471 | σ status = 'pending' (reservations) |
| | 472 | ) |
| | 473 | |
| | 474 | WorkloadShare <- |
| | 475 | π admin_id, total_reviewed, |
| | 476 | total_reviewed / SUM(total_reviewed) * 100 → workload_share_pct ( |
| | 477 | AdminStats |
| | 478 | ) |
| | 479 | |
| | 480 | Result <- |
| | 481 | π administrator, total_reviewed, approved, rejected, approval_rate, |
| | 482 | workload_share_pct, resources_handled, users_served, |
| | 483 | total_pending, avg_wait_days, max_wait_days ( |
| | 484 | ( |
| | 485 | ( |
| | 486 | σ type_name = 'Administrator' (users u ⨝ user_types ut) |
| | 487 | ⟕ (u.user_id = ast.admin_id) AdminStats ast |
| | 488 | ) |
| | 489 | ⟕ (u.user_id = ws.admin_id) WorkloadShare ws |
| | 490 | ) |
| | 491 | × PendingStats ps |
| | 492 | ) |
| | 493 | }}} |
| | 494 | |
| | 495 | == 5. Resource Recommendation Based on Similar Users == |
| | 496 | |
| | 497 | Collaborative filtering report: given a target user, finds other users with similar reservation patterns (shared resources), then recommends resources those similar users have used that the target user has not tried yet. Useful for suggesting relevant resources to users and improving resource discovery. |
| | 498 | |
| | 499 | === Solution SQL === |
| | 500 | |
| | 501 | {{{ |
| | 502 | SET search_path TO project; |
| | 503 | |
| | 504 | WITH target_user AS ( |
| | 505 | SELECT 5 AS user_id -- parameterize per user |
| | 506 | ), |
| | 507 | target_resources AS ( |
| | 508 | SELECT DISTINCT rv.resource_id |
| | 509 | FROM reservations rv, target_user tu |
| | 510 | WHERE rv.user_id = tu.user_id |
| | 511 | AND rv.status IN ('approved', 'completed') |
| | 512 | ), |
| | 513 | similar_users AS ( |
| | 514 | SELECT |
| | 515 | rv.user_id AS similar_user_id, |
| | 516 | COUNT(DISTINCT rv.resource_id) AS shared_resources, |
| | 517 | RANK() OVER (ORDER BY COUNT(DISTINCT rv.resource_id) DESC) AS similarity_rank |
| | 518 | FROM reservations rv |
| | 519 | JOIN target_resources tr ON rv.resource_id = tr.resource_id |
| | 520 | CROSS JOIN target_user tu |
| | 521 | WHERE rv.user_id != tu.user_id |
| | 522 | AND rv.status IN ('approved', 'completed') |
| | 523 | GROUP BY rv.user_id |
| | 524 | ), |
| | 525 | recommended_resources AS ( |
| | 526 | SELECT |
| | 527 | rv.resource_id, |
| | 528 | r.name AS resource_name, |
| | 529 | rt.type_name AS resource_type, |
| | 530 | COALESCE(l.building || ' ' || l.room, 'Digital') AS location, |
| | 531 | COUNT(DISTINCT rv.user_id) AS recommended_by_count, |
| | 532 | ROUND(AVG(EXTRACT(EPOCH FROM (rv.end_time - rv.start_time)) / 3600.0), 1) |
| | 533 | AS avg_usage_hours |
| | 534 | FROM reservations rv |
| | 535 | JOIN similar_users su ON rv.user_id = su.similar_user_id |
| | 536 | JOIN resources r ON rv.resource_id = r.resource_id |
| | 537 | JOIN resource_types rt ON r.type_id = rt.type_id |
| | 538 | LEFT JOIN locations l ON r.location_id = l.location_id |
| | 539 | WHERE rv.status IN ('approved', 'completed') |
| | 540 | AND su.similarity_rank <= 5 |
| | 541 | AND rv.resource_id NOT IN (SELECT resource_id FROM target_resources) |
| | 542 | GROUP BY rv.resource_id, r.name, rt.type_name, l.building, l.room |
| | 543 | ) |
| | 544 | SELECT |
| | 545 | u.first_name || ' ' || u.last_name AS target_user, |
| | 546 | rr.resource_name, |
| | 547 | rr.resource_type, |
| | 548 | rr.location, |
| | 549 | rr.recommended_by_count AS similar_users_use_it, |
| | 550 | rr.avg_usage_hours, |
| | 551 | RANK() OVER (ORDER BY rr.recommended_by_count DESC, |
| | 552 | rr.avg_usage_hours DESC) AS recommendation_rank |
| | 553 | FROM recommended_resources rr |
| | 554 | CROSS JOIN target_user tu |
| | 555 | JOIN users u ON tu.user_id = u.user_id |
| | 556 | ORDER BY recommendation_rank; |
| | 557 | }}} |
| | 558 | |
| | 559 | === Solution Relational Algebra === |
| | 560 | |
| | 561 | {{{ |
| | 562 | TargetUser <- {(user_id: 5)} |
| | 563 | |
| | 564 | TargetResources <- |
| | 565 | π resource_id ( |
| | 566 | σ status ∈ {'approved','completed'} ( |
| | 567 | reservations ⨝ (user_id) TargetUser |
| | 568 | ) |
| | 569 | ) |
| | 570 | |
| | 571 | SimilarUsers <- |
| | 572 | ω ORDER BY shared_resources DESC; similarity_rank := RANK() ( |
| | 573 | γ similar_user_id := rv.user_id; |
| | 574 | shared_resources := COUNT_DISTINCT(rv.resource_id) ( |
| | 575 | σ rv.user_id ≠ tu.user_id ∧ rv.status ∈ {'approved','completed'} ( |
| | 576 | (reservations rv ⨝ (rv.resource_id = tr.resource_id) TargetResources tr) |
| | 577 | × TargetUser tu |
| | 578 | ) |
| | 579 | ) |
| | 580 | ) |
| | 581 | |
| | 582 | RecommendedResources <- |
| | 583 | γ resource_id, resource_name, resource_type, location; |
| | 584 | recommended_by := COUNT_DISTINCT(user_id), |
| | 585 | avg_usage_hours := AVG(duration) ( |
| | 586 | π rv.resource_id, r.name → resource_name, rt.type_name → resource_type, |
| | 587 | l.building || l.room → location, rv.user_id, |
| | 588 | (rv.end_time − rv.start_time)/3600 → duration ( |
| | 589 | σ rv.status ∈ {'approved','completed'} ∧ su.similarity_rank ≤ 5 ( |
| | 590 | ( |
| | 591 | ( |
| | 592 | reservations rv |
| | 593 | ⨝ (rv.user_id = su.similar_user_id) σ similarity_rank ≤ 5 (SimilarUsers su) |
| | 594 | ) |
| | 595 | ⨝ (rv.resource_id = r.resource_id) resources r |
| | 596 | ⨝ (r.type_id = rt.type_id) resource_types rt |
| | 597 | ⟕ (r.location_id = l.location_id) locations l |
| | 598 | ) |
| | 599 | − (π * (reservations rv ⨝ TargetResources)) |
| | 600 | ) |
| | 601 | ) |
| | 602 | ) |
| | 603 | |
| | 604 | Result <- |
| | 605 | π target_user, resource_name, resource_type, location, |
| | 606 | recommended_by, avg_usage_hours, recommendation_rank ( |
| | 607 | ω ORDER BY recommended_by DESC, avg_usage_hours DESC; |
| | 608 | recommendation_rank := RANK() ( |
| | 609 | RecommendedResources × (TargetUser ⨝ users) |
| | 610 | ) |
| | 611 | ) |
| | 612 | }}} |