1use crate::application::dto::BookingStatisticsDto;
2use crate::application::ports::ResourceBookingRepository;
3use crate::domain::entities::{BookingStatus, RecurringPattern, ResourceBooking, ResourceType};
4use crate::infrastructure::pool::DbPool;
5use async_trait::async_trait;
6use chrono::{DateTime, Utc};
7use sqlx::Row;
8use uuid::Uuid;
9
10pub struct PostgresResourceBookingRepository {
11 pool: DbPool,
12}
13
14impl PostgresResourceBookingRepository {
15 pub fn new(pool: DbPool) -> Self {
16 Self { pool }
17 }
18
19 fn row_to_entity(row: &sqlx::postgres::PgRow) -> Result<ResourceBooking, String> {
21 let resource_type_str: String = row
23 .try_get("res_type")
24 .map_err(|e| format!("Failed to get resource_type: {}", e))?;
25 let resource_type: ResourceType =
26 serde_json::from_str(&format!("\"{}\"", resource_type_str))
27 .map_err(|e| format!("Failed to parse resource_type: {}", e))?;
28
29 let status_str: String = row
30 .try_get("status")
31 .map_err(|e| format!("Failed to get status: {}", e))?;
32 let status: BookingStatus = serde_json::from_str(&format!("\"{}\"", status_str))
33 .map_err(|e| format!("Failed to parse status: {}", e))?;
34
35 let recurring_pattern_str: String = row
36 .try_get("recurring_pattern")
37 .map_err(|e| format!("Failed to get recurring_pattern: {}", e))?;
38 let recurring_pattern: RecurringPattern =
39 serde_json::from_str(&format!("\"{}\"", recurring_pattern_str))
40 .map_err(|e| format!("Failed to parse recurring_pattern: {}", e))?;
41
42 Ok(ResourceBooking {
43 id: row
44 .try_get("id")
45 .map_err(|e| format!("Failed to get id: {}", e))?,
46 building_id: row
47 .try_get("building_id")
48 .map_err(|e| format!("Failed to get building_id: {}", e))?,
49 resource_type,
50 resource_name: row
51 .try_get("resource_name")
52 .map_err(|e| format!("Failed to get resource_name: {}", e))?,
53 booked_by: row
54 .try_get("booked_by")
55 .map_err(|e| format!("Failed to get booked_by: {}", e))?,
56 booked_by_user_id: row
58 .try_get("booked_by_user_id")
59 .map_err(|e| format!("Failed to get booked_by_user_id: {}", e))?,
60 on_behalf_of_acp: row
61 .try_get("on_behalf_of_acp")
62 .map_err(|e| format!("Failed to get on_behalf_of_acp: {}", e))?,
63 motif: row
64 .try_get("motif")
65 .map_err(|e| format!("Failed to get motif: {}", e))?,
66 start_time: row
67 .try_get("start_time")
68 .map_err(|e| format!("Failed to get start_time: {}", e))?,
69 end_time: row
70 .try_get("end_time")
71 .map_err(|e| format!("Failed to get end_time: {}", e))?,
72 status,
73 notes: row
74 .try_get("notes")
75 .map_err(|e| format!("Failed to get notes: {}", e))?,
76 recurring_pattern,
77 recurrence_end_date: row
78 .try_get("recurrence_end_date")
79 .map_err(|e| format!("Failed to get recurrence_end_date: {}", e))?,
80 created_at: row
81 .try_get("created_at")
82 .map_err(|e| format!("Failed to get created_at: {}", e))?,
83 updated_at: row
84 .try_get("updated_at")
85 .map_err(|e| format!("Failed to get updated_at: {}", e))?,
86 })
87 }
88}
89
90#[async_trait]
91impl ResourceBookingRepository for PostgresResourceBookingRepository {
92 async fn create(&self, booking: &ResourceBooking) -> Result<ResourceBooking, String> {
93 let resource_type_str = serde_json::to_string(&booking.resource_type)
95 .map_err(|e| format!("Failed to serialize resource_type: {}", e))?
96 .trim_matches('"')
97 .to_string();
98
99 let status_str = serde_json::to_string(&booking.status)
100 .map_err(|e| format!("Failed to serialize status: {}", e))?
101 .trim_matches('"')
102 .to_string();
103
104 let recurring_pattern_str = serde_json::to_string(&booking.recurring_pattern)
105 .map_err(|e| format!("Failed to serialize recurring_pattern: {}", e))?
106 .trim_matches('"')
107 .to_string();
108
109 sqlx::query(
110 r#"
111 INSERT INTO resource_bookings (
112 id, building_id, resource_type, resource_name, booked_by,
113 start_time, end_time, status, notes, recurring_pattern,
114 recurrence_end_date, created_at, updated_at, booked_by_user_id, on_behalf_of_acp, motif
115 )
116 VALUES ($1, $2, $3::resource_type, $4, $5, $6, $7, $8::booking_status, $9,
117 $10::recurring_pattern, $11, $12, $13, $14, $15, $16)
118 "#,
119 )
120 .bind(booking.id)
121 .bind(booking.building_id)
122 .bind(&resource_type_str)
123 .bind(&booking.resource_name)
124 .bind(booking.booked_by)
125 .bind(booking.start_time)
126 .bind(booking.end_time)
127 .bind(&status_str)
128 .bind(&booking.notes)
129 .bind(&recurring_pattern_str)
130 .bind(booking.recurrence_end_date)
131 .bind(booking.created_at)
132 .bind(booking.updated_at)
133 .bind(booking.booked_by_user_id)
134 .bind(booking.on_behalf_of_acp)
135 .bind(&booking.motif)
136 .execute(&self.pool)
137 .await
138 .map_err(|e| format!("Failed to create resource booking: {}", e))?;
139
140 Ok(booking.clone())
141 }
142
143 async fn find_by_id(&self, id: Uuid) -> Result<Option<ResourceBooking>, String> {
144 let row = sqlx::query(
145 r#"
146 SELECT id, building_id, resource_type::text AS res_type, resource_name, booked_by,
147 start_time, end_time, status::text AS status, notes, recurring_pattern::text AS recurring_pattern,
148 recurrence_end_date, created_at, updated_at, booked_by_user_id, on_behalf_of_acp, motif
149 FROM resource_bookings
150 WHERE id = $1
151 "#,
152 )
153 .bind(id)
154 .fetch_optional(&self.pool)
155 .await
156 .map_err(|e| format!("Failed to find resource booking: {}", e))?;
157
158 match row {
159 Some(r) => Ok(Some(Self::row_to_entity(&r)?)),
160 None => Ok(None),
161 }
162 }
163
164 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<ResourceBooking>, String> {
165 let rows = sqlx::query(
166 r#"
167 SELECT id, building_id, resource_type::text AS res_type, resource_name, booked_by,
168 start_time, end_time, status::text AS status, notes, recurring_pattern::text AS recurring_pattern,
169 recurrence_end_date, created_at, updated_at, booked_by_user_id, on_behalf_of_acp, motif
170 FROM resource_bookings
171 WHERE building_id = $1
172 ORDER BY start_time ASC
173 "#,
174 )
175 .bind(building_id)
176 .fetch_all(&self.pool)
177 .await
178 .map_err(|e| format!("Failed to find bookings by building: {}", e))?;
179
180 rows.iter().map(Self::row_to_entity).collect()
181 }
182
183 async fn find_by_building_and_resource_type(
184 &self,
185 building_id: Uuid,
186 resource_type: ResourceType,
187 ) -> Result<Vec<ResourceBooking>, String> {
188 let resource_type_str = serde_json::to_string(&resource_type)
189 .map_err(|e| format!("Failed to serialize resource_type: {}", e))?
190 .trim_matches('"')
191 .to_string();
192
193 let rows = sqlx::query(
194 r#"
195 SELECT id, building_id, resource_type::text AS res_type, resource_name, booked_by,
196 start_time, end_time, status::text AS status, notes, recurring_pattern::text AS recurring_pattern,
197 recurrence_end_date, created_at, updated_at, booked_by_user_id, on_behalf_of_acp, motif
198 FROM resource_bookings
199 WHERE building_id = $1 AND resource_type = $2::resource_type
200 ORDER BY start_time ASC
201 "#,
202 )
203 .bind(building_id)
204 .bind(&resource_type_str)
205 .fetch_all(&self.pool)
206 .await
207 .map_err(|e| {
208 format!(
209 "Failed to find bookings by building and resource type: {}",
210 e
211 )
212 })?;
213
214 rows.iter().map(Self::row_to_entity).collect()
215 }
216
217 async fn find_by_resource(
218 &self,
219 building_id: Uuid,
220 resource_type: ResourceType,
221 resource_name: &str,
222 ) -> Result<Vec<ResourceBooking>, String> {
223 let resource_type_str = serde_json::to_string(&resource_type)
224 .map_err(|e| format!("Failed to serialize resource_type: {}", e))?
225 .trim_matches('"')
226 .to_string();
227
228 let rows = sqlx::query(
229 r#"
230 SELECT id, building_id, resource_type::text AS res_type, resource_name, booked_by,
231 start_time, end_time, status::text AS status, notes, recurring_pattern::text AS recurring_pattern,
232 recurrence_end_date, created_at, updated_at, booked_by_user_id, on_behalf_of_acp, motif
233 FROM resource_bookings
234 WHERE building_id = $1 AND resource_type = $2::resource_type AND resource_name = $3
235 ORDER BY start_time ASC
236 "#,
237 )
238 .bind(building_id)
239 .bind(&resource_type_str)
240 .bind(resource_name)
241 .fetch_all(&self.pool)
242 .await
243 .map_err(|e| format!("Failed to find bookings by resource: {}", e))?;
244
245 rows.iter().map(Self::row_to_entity).collect()
246 }
247
248 async fn find_by_user(&self, user_id: Uuid) -> Result<Vec<ResourceBooking>, String> {
249 let rows = sqlx::query(
250 r#"
251 SELECT id, building_id, resource_type::text AS res_type, resource_name, booked_by,
252 start_time, end_time, status::text AS status, notes, recurring_pattern::text AS recurring_pattern,
253 recurrence_end_date, created_at, updated_at, booked_by_user_id, on_behalf_of_acp, motif
254 FROM resource_bookings
255 WHERE booked_by = $1
256 ORDER BY start_time DESC
257 "#,
258 )
259 .bind(user_id)
260 .fetch_all(&self.pool)
261 .await
262 .map_err(|e| format!("Failed to find bookings by user: {}", e))?;
263
264 rows.iter().map(Self::row_to_entity).collect()
265 }
266
267 async fn find_by_user_and_status(
268 &self,
269 user_id: Uuid,
270 status: BookingStatus,
271 ) -> Result<Vec<ResourceBooking>, String> {
272 let status_str = serde_json::to_string(&status)
273 .map_err(|e| format!("Failed to serialize status: {}", e))?
274 .trim_matches('"')
275 .to_string();
276
277 let rows = sqlx::query(
278 r#"
279 SELECT id, building_id, resource_type::text AS res_type, resource_name, booked_by,
280 start_time, end_time, status::text AS status, notes, recurring_pattern::text AS recurring_pattern,
281 recurrence_end_date, created_at, updated_at, booked_by_user_id, on_behalf_of_acp, motif
282 FROM resource_bookings
283 WHERE booked_by = $1 AND status = $2::booking_status
284 ORDER BY start_time DESC
285 "#,
286 )
287 .bind(user_id)
288 .bind(&status_str)
289 .fetch_all(&self.pool)
290 .await
291 .map_err(|e| format!("Failed to find bookings by user and status: {}", e))?;
292
293 rows.iter().map(Self::row_to_entity).collect()
294 }
295
296 async fn find_by_building_and_status(
297 &self,
298 building_id: Uuid,
299 status: BookingStatus,
300 ) -> Result<Vec<ResourceBooking>, String> {
301 let status_str = serde_json::to_string(&status)
302 .map_err(|e| format!("Failed to serialize status: {}", e))?
303 .trim_matches('"')
304 .to_string();
305
306 let rows = sqlx::query(
307 r#"
308 SELECT id, building_id, resource_type::text AS res_type, resource_name, booked_by,
309 start_time, end_time, status::text AS status, notes, recurring_pattern::text AS recurring_pattern,
310 recurrence_end_date, created_at, updated_at, booked_by_user_id, on_behalf_of_acp, motif
311 FROM resource_bookings
312 WHERE building_id = $1 AND status = $2::booking_status
313 ORDER BY start_time ASC
314 "#,
315 )
316 .bind(building_id)
317 .bind(&status_str)
318 .fetch_all(&self.pool)
319 .await
320 .map_err(|e| format!("Failed to find bookings by building and status: {}", e))?;
321
322 rows.iter().map(Self::row_to_entity).collect()
323 }
324
325 async fn find_upcoming(
326 &self,
327 building_id: Uuid,
328 limit: Option<i64>,
329 ) -> Result<Vec<ResourceBooking>, String> {
330 let limit_val = limit.unwrap_or(50);
331
332 let rows = sqlx::query(
333 r#"
334 SELECT id, building_id, resource_type::text AS res_type, resource_name, booked_by,
335 start_time, end_time, status::text AS status, notes, recurring_pattern::text AS recurring_pattern,
336 recurrence_end_date, created_at, updated_at, booked_by_user_id, on_behalf_of_acp, motif
337 FROM resource_bookings
338 WHERE building_id = $1
339 AND start_time > NOW()
340 AND status IN ('Confirmed', 'Pending')
341 ORDER BY start_time ASC
342 LIMIT $2
343 "#,
344 )
345 .bind(building_id)
346 .bind(limit_val)
347 .fetch_all(&self.pool)
348 .await
349 .map_err(|e| format!("Failed to find upcoming bookings: {}", e))?;
350
351 rows.iter().map(Self::row_to_entity).collect()
352 }
353
354 async fn find_active(&self, building_id: Uuid) -> Result<Vec<ResourceBooking>, String> {
355 let rows = sqlx::query(
356 r#"
357 SELECT id, building_id, resource_type::text AS res_type, resource_name, booked_by,
358 start_time, end_time, status::text AS status, notes, recurring_pattern::text AS recurring_pattern,
359 recurrence_end_date, created_at, updated_at, booked_by_user_id, on_behalf_of_acp, motif
360 FROM resource_bookings
361 WHERE building_id = $1
362 AND status = 'Confirmed'
363 AND start_time <= NOW()
364 AND end_time > NOW()
365 ORDER BY start_time ASC
366 "#,
367 )
368 .bind(building_id)
369 .fetch_all(&self.pool)
370 .await
371 .map_err(|e| format!("Failed to find active bookings: {}", e))?;
372
373 rows.iter().map(Self::row_to_entity).collect()
374 }
375
376 async fn find_past(
377 &self,
378 building_id: Uuid,
379 limit: Option<i64>,
380 ) -> Result<Vec<ResourceBooking>, String> {
381 let limit_val = limit.unwrap_or(50);
382
383 let rows = sqlx::query(
384 r#"
385 SELECT id, building_id, resource_type::text AS res_type, resource_name, booked_by,
386 start_time, end_time, status::text AS status, notes, recurring_pattern::text AS recurring_pattern,
387 recurrence_end_date, created_at, updated_at, booked_by_user_id, on_behalf_of_acp, motif
388 FROM resource_bookings
389 WHERE building_id = $1
390 AND end_time < NOW()
391 ORDER BY start_time DESC
392 LIMIT $2
393 "#,
394 )
395 .bind(building_id)
396 .bind(limit_val)
397 .fetch_all(&self.pool)
398 .await
399 .map_err(|e| format!("Failed to find past bookings: {}", e))?;
400
401 rows.iter().map(Self::row_to_entity).collect()
402 }
403
404 async fn find_conflicts(
405 &self,
406 building_id: Uuid,
407 resource_type: ResourceType,
408 resource_name: &str,
409 start_time: DateTime<Utc>,
410 end_time: DateTime<Utc>,
411 exclude_booking_id: Option<Uuid>,
412 ) -> Result<Vec<ResourceBooking>, String> {
413 let resource_type_str = serde_json::to_string(&resource_type)
414 .map_err(|e| format!("Failed to serialize resource_type: {}", e))?
415 .trim_matches('"')
416 .to_string();
417
418 let rows = if let Some(exclude_id) = exclude_booking_id {
421 sqlx::query(
422 r#"
423 SELECT id, building_id, resource_type::text AS res_type, resource_name, booked_by,
424 start_time, end_time, status::text AS status, notes, recurring_pattern::text AS recurring_pattern,
425 recurrence_end_date, created_at, updated_at, booked_by_user_id, on_behalf_of_acp, motif
426 FROM resource_bookings
427 WHERE building_id = $1
428 AND resource_type = $2::resource_type
429 AND resource_name = $3
430 AND status IN ('Pending', 'Confirmed')
431 AND start_time < $5
432 AND end_time > $4
433 AND id != $6
434 ORDER BY start_time ASC
435 "#,
436 )
437 .bind(building_id)
438 .bind(&resource_type_str)
439 .bind(resource_name)
440 .bind(start_time)
441 .bind(end_time)
442 .bind(exclude_id)
443 .fetch_all(&self.pool)
444 .await
445 } else {
446 sqlx::query(
447 r#"
448 SELECT id, building_id, resource_type::text AS res_type, resource_name, booked_by,
449 start_time, end_time, status::text AS status, notes, recurring_pattern::text AS recurring_pattern,
450 recurrence_end_date, created_at, updated_at, booked_by_user_id, on_behalf_of_acp, motif
451 FROM resource_bookings
452 WHERE building_id = $1
453 AND resource_type = $2::resource_type
454 AND resource_name = $3
455 AND status IN ('Pending', 'Confirmed')
456 AND start_time < $5
457 AND end_time > $4
458 ORDER BY start_time ASC
459 "#,
460 )
461 .bind(building_id)
462 .bind(&resource_type_str)
463 .bind(resource_name)
464 .bind(start_time)
465 .bind(end_time)
466 .fetch_all(&self.pool)
467 .await
468 }
469 .map_err(|e| format!("Failed to find conflicting bookings: {}", e))?;
470
471 rows.iter().map(Self::row_to_entity).collect()
472 }
473
474 async fn update(&self, booking: &ResourceBooking) -> Result<ResourceBooking, String> {
475 let resource_type_str = serde_json::to_string(&booking.resource_type)
476 .map_err(|e| format!("Failed to serialize resource_type: {}", e))?
477 .trim_matches('"')
478 .to_string();
479
480 let status_str = serde_json::to_string(&booking.status)
481 .map_err(|e| format!("Failed to serialize status: {}", e))?
482 .trim_matches('"')
483 .to_string();
484
485 let recurring_pattern_str = serde_json::to_string(&booking.recurring_pattern)
486 .map_err(|e| format!("Failed to serialize recurring_pattern: {}", e))?
487 .trim_matches('"')
488 .to_string();
489
490 let result = sqlx::query(
491 r#"
492 UPDATE resource_bookings
493 SET resource_type = $2::resource_type,
494 resource_name = $3,
495 start_time = $4,
496 end_time = $5,
497 status = $6::booking_status,
498 notes = $7,
499 recurring_pattern = $8::recurring_pattern,
500 recurrence_end_date = $9,
501 updated_at = $10
502 WHERE id = $1
503 "#,
504 )
505 .bind(booking.id)
506 .bind(&resource_type_str)
507 .bind(&booking.resource_name)
508 .bind(booking.start_time)
509 .bind(booking.end_time)
510 .bind(&status_str)
511 .bind(&booking.notes)
512 .bind(&recurring_pattern_str)
513 .bind(booking.recurrence_end_date)
514 .bind(booking.updated_at)
515 .execute(&self.pool)
516 .await
517 .map_err(|e| format!("Failed to update resource booking: {}", e))?;
518
519 if result.rows_affected() == 0 {
520 return Err("Resource booking not found".to_string());
521 }
522
523 Ok(booking.clone())
524 }
525
526 async fn delete(&self, id: Uuid) -> Result<(), String> {
527 let result = sqlx::query(
528 r#"
529 DELETE FROM resource_bookings
530 WHERE id = $1
531 "#,
532 )
533 .bind(id)
534 .execute(&self.pool)
535 .await
536 .map_err(|e| format!("Failed to delete resource booking: {}", e))?;
537
538 if result.rows_affected() == 0 {
539 return Err("Resource booking not found".to_string());
540 }
541
542 Ok(())
543 }
544
545 async fn count_by_building(&self, building_id: Uuid) -> Result<i64, String> {
546 let row = sqlx::query(
547 r#"
548 SELECT COUNT(*) as count
549 FROM resource_bookings
550 WHERE building_id = $1
551 "#,
552 )
553 .bind(building_id)
554 .fetch_one(&self.pool)
555 .await
556 .map_err(|e| format!("Failed to count bookings by building: {}", e))?;
557
558 let count: i64 = row
559 .try_get("count")
560 .map_err(|e| format!("Failed to get count: {}", e))?;
561 Ok(count)
562 }
563
564 async fn count_by_building_and_status(
565 &self,
566 building_id: Uuid,
567 status: BookingStatus,
568 ) -> Result<i64, String> {
569 let status_str = serde_json::to_string(&status)
570 .map_err(|e| format!("Failed to serialize status: {}", e))?
571 .trim_matches('"')
572 .to_string();
573
574 let row = sqlx::query(
575 r#"
576 SELECT COUNT(*) as count
577 FROM resource_bookings
578 WHERE building_id = $1 AND status = $2::booking_status
579 "#,
580 )
581 .bind(building_id)
582 .bind(&status_str)
583 .fetch_one(&self.pool)
584 .await
585 .map_err(|e| format!("Failed to count bookings by building and status: {}", e))?;
586
587 let count: i64 = row
588 .try_get("count")
589 .map_err(|e| format!("Failed to get count: {}", e))?;
590 Ok(count)
591 }
592
593 async fn count_by_resource(
594 &self,
595 building_id: Uuid,
596 resource_type: ResourceType,
597 resource_name: &str,
598 ) -> Result<i64, String> {
599 let resource_type_str = serde_json::to_string(&resource_type)
600 .map_err(|e| format!("Failed to serialize resource_type: {}", e))?
601 .trim_matches('"')
602 .to_string();
603
604 let row = sqlx::query(
605 r#"
606 SELECT COUNT(*) as count
607 FROM resource_bookings
608 WHERE building_id = $1 AND resource_type = $2::resource_type AND resource_name = $3
609 "#,
610 )
611 .bind(building_id)
612 .bind(&resource_type_str)
613 .bind(resource_name)
614 .fetch_one(&self.pool)
615 .await
616 .map_err(|e| format!("Failed to count bookings by resource: {}", e))?;
617
618 let count: i64 = row
619 .try_get("count")
620 .map_err(|e| format!("Failed to get count: {}", e))?;
621 Ok(count)
622 }
623
624 async fn get_statistics(&self, building_id: Uuid) -> Result<BookingStatisticsDto, String> {
625 let total = self.count_by_building(building_id).await?;
627 let confirmed = self
628 .count_by_building_and_status(building_id, BookingStatus::Confirmed)
629 .await?;
630 let pending = self
631 .count_by_building_and_status(building_id, BookingStatus::Pending)
632 .await?;
633 let completed = self
634 .count_by_building_and_status(building_id, BookingStatus::Completed)
635 .await?;
636 let cancelled = self
637 .count_by_building_and_status(building_id, BookingStatus::Cancelled)
638 .await?;
639 let no_show = self
640 .count_by_building_and_status(building_id, BookingStatus::NoShow)
641 .await?;
642
643 let active_bookings = self.find_active(building_id).await?.len() as i64;
645
646 let upcoming_row = sqlx::query(
648 r#"
649 SELECT COUNT(*) as count
650 FROM resource_bookings
651 WHERE building_id = $1
652 AND start_time > NOW()
653 AND status IN ('Confirmed', 'Pending')
654 "#,
655 )
656 .bind(building_id)
657 .fetch_one(&self.pool)
658 .await
659 .map_err(|e| format!("Failed to count upcoming bookings: {}", e))?;
660
661 let upcoming_bookings: i64 = upcoming_row
662 .try_get("count")
663 .map_err(|e| format!("Failed to get upcoming count: {}", e))?;
664
665 let hours_row = sqlx::query(
667 r#"
668 SELECT COALESCE(SUM(EXTRACT(EPOCH FROM (end_time - start_time)) / 3600), 0) as total_hours
669 FROM resource_bookings
670 WHERE building_id = $1
671 AND status IN ('Confirmed', 'Completed')
672 "#,
673 )
674 .bind(building_id)
675 .fetch_one(&self.pool)
676 .await
677 .map_err(|e| format!("Failed to calculate total hours booked: {}", e))?;
678
679 let total_hours_booked: f64 = hours_row
680 .try_get("total_hours")
681 .map_err(|e| format!("Failed to get total_hours: {}", e))?;
682
683 let popular_row = sqlx::query(
685 r#"
686 SELECT resource_name, COUNT(*) as booking_count
687 FROM resource_bookings
688 WHERE building_id = $1
689 GROUP BY resource_name
690 ORDER BY booking_count DESC
691 LIMIT 1
692 "#,
693 )
694 .bind(building_id)
695 .fetch_optional(&self.pool)
696 .await
697 .map_err(|e| format!("Failed to find most popular resource: {}", e))?;
698
699 let most_popular_resource = popular_row.map(|row| {
700 row.try_get::<String, _>("resource_name")
701 .unwrap_or_default()
702 });
703
704 Ok(BookingStatisticsDto {
705 building_id,
706 total_bookings: total,
707 confirmed_bookings: confirmed,
708 pending_bookings: pending,
709 completed_bookings: completed,
710 cancelled_bookings: cancelled,
711 no_show_bookings: no_show,
712 active_bookings,
713 upcoming_bookings,
714 total_hours_booked,
715 most_popular_resource,
716 })
717 }
718}