koprogo_api/infrastructure/web/handlers/
resource_booking_handlers.rs1use crate::application::dto::{CreateResourceBookingDto, UpdateResourceBookingDto};
2use crate::domain::entities::{BookingStatus, ResourceType};
3use crate::infrastructure::web::app_state::AppState;
4use crate::infrastructure::web::middleware::AuthenticatedUser;
5use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
6use chrono::{DateTime, Utc};
7use serde::Deserialize;
8use uuid::Uuid;
9
10#[post("/resource-bookings")]
31pub async fn create_booking(
32 data: web::Data<AppState>,
33 auth: AuthenticatedUser,
34 request: web::Json<CreateResourceBookingDto>,
35) -> impl Responder {
36 let org_id = match auth.require_organization() {
37 Ok(id) => id,
38 Err(e) => {
39 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
40 }
41 };
42 match data
43 .resource_booking_use_cases
44 .create_booking(auth.user_id, org_id, request.into_inner())
45 .await
46 {
47 Ok(booking) => HttpResponse::Created().json(booking),
48 Err(e) => {
49 if e.contains("conflicts with") {
50 HttpResponse::Conflict().json(serde_json::json!({"error": e}))
51 } else if e.contains("not found") {
52 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
53 } else {
54 HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
55 }
56 }
57 }
58}
59
60#[get("/resource-bookings/{id}")]
68pub async fn get_booking(
69 data: web::Data<AppState>,
70 _auth: AuthenticatedUser,
71 id: web::Path<Uuid>,
72) -> impl Responder {
73 match data
74 .resource_booking_use_cases
75 .get_booking(id.into_inner())
76 .await
77 {
78 Ok(booking) => HttpResponse::Ok().json(booking),
79 Err(e) => HttpResponse::NotFound().json(serde_json::json!({"error": e})),
80 }
81}
82
83#[get("/buildings/{building_id}/resource-bookings")]
90pub async fn list_building_bookings(
91 data: web::Data<AppState>,
92 _auth: AuthenticatedUser,
93 building_id: web::Path<Uuid>,
94) -> impl Responder {
95 match data
96 .resource_booking_use_cases
97 .list_building_bookings(building_id.into_inner())
98 .await
99 {
100 Ok(bookings) => HttpResponse::Ok().json(bookings),
101 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
102 }
103}
104
105#[get("/buildings/{building_id}/resource-bookings/type/{resource_type}")]
112pub async fn list_by_resource_type(
113 data: web::Data<AppState>,
114 _auth: AuthenticatedUser,
115 path: web::Path<(Uuid, String)>,
116) -> impl Responder {
117 let (building_id, resource_type_str) = path.into_inner();
118
119 let resource_type: ResourceType =
121 match serde_json::from_str(&format!("\"{}\"", resource_type_str)) {
122 Ok(rt) => rt,
123 Err(_) => {
124 return HttpResponse::BadRequest().json(serde_json::json!({
125 "error": format!("Invalid resource type: {}", resource_type_str)
126 }))
127 }
128 };
129
130 match data
131 .resource_booking_use_cases
132 .list_by_resource_type(building_id, resource_type)
133 .await
134 {
135 Ok(bookings) => HttpResponse::Ok().json(bookings),
136 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
137 }
138}
139
140#[get("/buildings/{building_id}/resource-bookings/resource/{resource_type}/{resource_name}")]
147pub async fn list_by_resource(
148 data: web::Data<AppState>,
149 _auth: AuthenticatedUser,
150 path: web::Path<(Uuid, String, String)>,
151) -> impl Responder {
152 let (building_id, resource_type_str, resource_name) = path.into_inner();
153
154 let resource_type: ResourceType =
156 match serde_json::from_str(&format!("\"{}\"", resource_type_str)) {
157 Ok(rt) => rt,
158 Err(_) => {
159 return HttpResponse::BadRequest().json(serde_json::json!({
160 "error": format!("Invalid resource type: {}", resource_type_str)
161 }))
162 }
163 };
164
165 match data
166 .resource_booking_use_cases
167 .list_by_resource(building_id, resource_type, resource_name)
168 .await
169 {
170 Ok(bookings) => HttpResponse::Ok().json(bookings),
171 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
172 }
173}
174
175#[get("/resource-bookings/my")]
182pub async fn list_my_bookings(
183 data: web::Data<AppState>,
184 auth: AuthenticatedUser,
185) -> impl Responder {
186 let org_id = match auth.require_organization() {
187 Ok(id) => id,
188 Err(e) => {
189 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
190 }
191 };
192 match data
193 .resource_booking_use_cases
194 .list_user_bookings(auth.user_id, org_id)
195 .await
196 {
197 Ok(bookings) => HttpResponse::Ok().json(bookings),
198 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
199 }
200}
201
202#[get("/resource-bookings/my/status/{status}")]
209pub async fn list_my_bookings_by_status(
210 data: web::Data<AppState>,
211 auth: AuthenticatedUser,
212 status: web::Path<String>,
213) -> impl Responder {
214 let booking_status: BookingStatus =
216 match serde_json::from_str(&format!("\"{}\"", status.into_inner())) {
217 Ok(s) => s,
218 Err(_) => {
219 return HttpResponse::BadRequest()
220 .json(serde_json::json!({"error": "Invalid status"}))
221 }
222 };
223
224 let org_id = match auth.require_organization() {
225 Ok(id) => id,
226 Err(e) => {
227 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
228 }
229 };
230 match data
231 .resource_booking_use_cases
232 .list_user_bookings_by_status(auth.user_id, org_id, booking_status)
233 .await
234 {
235 Ok(bookings) => HttpResponse::Ok().json(bookings),
236 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
237 }
238}
239
240#[get("/buildings/{building_id}/resource-bookings/status/{status}")]
247pub async fn list_building_bookings_by_status(
248 data: web::Data<AppState>,
249 _auth: AuthenticatedUser,
250 path: web::Path<(Uuid, String)>,
251) -> impl Responder {
252 let (building_id, status_str) = path.into_inner();
253
254 let booking_status: BookingStatus = match serde_json::from_str(&format!("\"{}\"", status_str)) {
256 Ok(s) => s,
257 Err(_) => {
258 return HttpResponse::BadRequest().json(serde_json::json!({
259 "error": format!("Invalid status: {}", status_str)
260 }))
261 }
262 };
263
264 match data
265 .resource_booking_use_cases
266 .list_building_bookings_by_status(building_id, booking_status)
267 .await
268 {
269 Ok(bookings) => HttpResponse::Ok().json(bookings),
270 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
271 }
272}
273
274#[derive(Deserialize)]
275pub struct UpcomingQuery {
276 limit: Option<i64>,
277}
278
279#[get("/buildings/{building_id}/resource-bookings/upcoming")]
286pub async fn list_upcoming_bookings(
287 data: web::Data<AppState>,
288 _auth: AuthenticatedUser,
289 building_id: web::Path<Uuid>,
290 query: web::Query<UpcomingQuery>,
291) -> impl Responder {
292 match data
293 .resource_booking_use_cases
294 .list_upcoming_bookings(building_id.into_inner(), query.limit)
295 .await
296 {
297 Ok(bookings) => HttpResponse::Ok().json(bookings),
298 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
299 }
300}
301
302#[get("/buildings/{building_id}/resource-bookings/active")]
309pub async fn list_active_bookings(
310 data: web::Data<AppState>,
311 _auth: AuthenticatedUser,
312 building_id: web::Path<Uuid>,
313) -> impl Responder {
314 match data
315 .resource_booking_use_cases
316 .list_active_bookings(building_id.into_inner())
317 .await
318 {
319 Ok(bookings) => HttpResponse::Ok().json(bookings),
320 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
321 }
322}
323
324#[derive(Deserialize)]
325pub struct PastQuery {
326 limit: Option<i64>,
327}
328
329#[get("/buildings/{building_id}/resource-bookings/past")]
336pub async fn list_past_bookings(
337 data: web::Data<AppState>,
338 _auth: AuthenticatedUser,
339 building_id: web::Path<Uuid>,
340 query: web::Query<PastQuery>,
341) -> impl Responder {
342 match data
343 .resource_booking_use_cases
344 .list_past_bookings(building_id.into_inner(), query.limit)
345 .await
346 {
347 Ok(bookings) => HttpResponse::Ok().json(bookings),
348 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
349 }
350}
351
352#[put("/resource-bookings/{id}")]
366pub async fn update_booking(
367 data: web::Data<AppState>,
368 auth: AuthenticatedUser,
369 id: web::Path<Uuid>,
370 request: web::Json<UpdateResourceBookingDto>,
371) -> impl Responder {
372 let org_id = match auth.require_organization() {
373 Ok(id) => id,
374 Err(e) => {
375 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
376 }
377 };
378 match data
379 .resource_booking_use_cases
380 .update_booking(id.into_inner(), auth.user_id, org_id, request.into_inner())
381 .await
382 {
383 Ok(booking) => HttpResponse::Ok().json(booking),
384 Err(e) => {
385 if e.contains("Only the booking owner") {
386 HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
387 } else if e.contains("not found") {
388 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
389 } else {
390 HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
391 }
392 }
393 }
394}
395
396#[post("/resource-bookings/{id}/cancel")]
406pub async fn cancel_booking(
407 data: web::Data<AppState>,
408 auth: AuthenticatedUser,
409 id: web::Path<Uuid>,
410) -> impl Responder {
411 let org_id = match auth.require_organization() {
412 Ok(id) => id,
413 Err(e) => {
414 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
415 }
416 };
417 match data
418 .resource_booking_use_cases
419 .cancel_booking(id.into_inner(), auth.user_id, org_id)
420 .await
421 {
422 Ok(booking) => HttpResponse::Ok().json(booking),
423 Err(e) => {
424 if e.contains("Only the booking owner") {
425 HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
426 } else if e.contains("not found") {
427 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
428 } else {
429 HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
430 }
431 }
432 }
433}
434
435#[post("/resource-bookings/{id}/complete")]
444pub async fn complete_booking(
445 data: web::Data<AppState>,
446 _auth: AuthenticatedUser,
447 id: web::Path<Uuid>,
448) -> impl Responder {
449 match data
450 .resource_booking_use_cases
451 .complete_booking(id.into_inner())
452 .await
453 {
454 Ok(booking) => HttpResponse::Ok().json(booking),
455 Err(e) => {
456 if e.contains("not found") {
457 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
458 } else {
459 HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
460 }
461 }
462 }
463}
464
465#[post("/resource-bookings/{id}/no-show")]
474pub async fn mark_no_show(
475 data: web::Data<AppState>,
476 _auth: AuthenticatedUser,
477 id: web::Path<Uuid>,
478) -> impl Responder {
479 match data
480 .resource_booking_use_cases
481 .mark_no_show(id.into_inner())
482 .await
483 {
484 Ok(booking) => HttpResponse::Ok().json(booking),
485 Err(e) => {
486 if e.contains("not found") {
487 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
488 } else {
489 HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
490 }
491 }
492 }
493}
494
495#[post("/resource-bookings/{id}/confirm")]
504pub async fn confirm_booking(
505 data: web::Data<AppState>,
506 _auth: AuthenticatedUser,
507 id: web::Path<Uuid>,
508) -> impl Responder {
509 match data
510 .resource_booking_use_cases
511 .confirm_booking(id.into_inner())
512 .await
513 {
514 Ok(booking) => HttpResponse::Ok().json(booking),
515 Err(e) => {
516 if e.contains("not found") {
517 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
518 } else {
519 HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
520 }
521 }
522 }
523}
524
525#[delete("/resource-bookings/{id}")]
534pub async fn delete_booking(
535 data: web::Data<AppState>,
536 auth: AuthenticatedUser,
537 id: web::Path<Uuid>,
538) -> impl Responder {
539 let org_id = match auth.require_organization() {
540 Ok(id) => id,
541 Err(e) => {
542 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
543 }
544 };
545 match data
546 .resource_booking_use_cases
547 .delete_booking(id.into_inner(), auth.user_id, org_id)
548 .await
549 {
550 Ok(()) => HttpResponse::NoContent().finish(),
551 Err(e) => {
552 if e.contains("Only the booking owner") {
553 HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
554 } else if e.contains("not found") {
555 HttpResponse::NotFound().json(serde_json::json!({"error": e}))
556 } else {
557 HttpResponse::InternalServerError().json(serde_json::json!({"error": e}))
558 }
559 }
560 }
561}
562
563#[derive(Deserialize)]
564pub struct CheckConflictsQuery {
565 pub building_id: Uuid,
566 pub resource_type: String,
567 pub resource_name: String,
568 pub start_time: DateTime<Utc>,
569 pub end_time: DateTime<Utc>,
570 pub exclude_booking_id: Option<Uuid>,
571}
572
573#[get("/resource-bookings/check-conflicts")]
588pub async fn check_conflicts(
589 data: web::Data<AppState>,
590 _auth: AuthenticatedUser,
591 query: web::Query<CheckConflictsQuery>,
592) -> impl Responder {
593 let resource_type: ResourceType =
595 match serde_json::from_str(&format!("\"{}\"", query.resource_type)) {
596 Ok(rt) => rt,
597 Err(_) => {
598 return HttpResponse::BadRequest().json(serde_json::json!({
599 "error": format!("Invalid resource type: {}", query.resource_type)
600 }))
601 }
602 };
603
604 match data
605 .resource_booking_use_cases
606 .check_conflicts(
607 query.building_id,
608 resource_type,
609 query.resource_name.clone(),
610 query.start_time,
611 query.end_time,
612 query.exclude_booking_id,
613 )
614 .await
615 {
616 Ok(conflicts) => HttpResponse::Ok().json(conflicts),
617 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
618 }
619}
620
621#[get("/buildings/{building_id}/resource-bookings/statistics")]
628pub async fn get_booking_statistics(
629 data: web::Data<AppState>,
630 _auth: AuthenticatedUser,
631 building_id: web::Path<Uuid>,
632) -> impl Responder {
633 match data
634 .resource_booking_use_cases
635 .get_statistics(building_id.into_inner())
636 .await
637 {
638 Ok(stats) => HttpResponse::Ok().json(stats),
639 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
640 }
641}