1use crate::application::dto::{
2 AssignTicketRequest, CancelTicketRequest, CreateTicketRequest, ReopenTicketRequest,
3 ResolveTicketRequest,
4};
5use crate::domain::entities::TicketStatus;
6use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
7use crate::infrastructure::web::{AppState, AuthenticatedUser};
8use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
9use uuid::Uuid;
10
11#[utoipa::path(
14 post,
15 path = "/tickets",
16 tag = "Tickets",
17 summary = "Create a new maintenance ticket",
18 request_body = CreateTicketRequest,
19 responses(
20 (status = 201, description = "Ticket created"),
21 (status = 400, description = "Bad request"),
22 (status = 401, description = "Unauthorized"),
23 (status = 500, description = "Internal server error"),
24 ),
25 security(("bearer_auth" = []))
26)]
27#[post("/tickets")]
28pub async fn create_ticket(
29 state: web::Data<AppState>,
30 user: AuthenticatedUser,
31 request: web::Json<CreateTicketRequest>,
32) -> impl Responder {
33 let organization_id = match user.require_organization() {
34 Ok(org_id) => org_id,
35 Err(e) => {
36 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
37 }
38 };
39
40 let created_by = user.user_id;
42
43 match state
44 .ticket_use_cases
45 .create_ticket(organization_id, created_by, request.into_inner())
46 .await
47 {
48 Ok(ticket) => {
49 AuditLogEntry::new(
50 AuditEventType::TicketCreated,
51 Some(user.user_id),
52 Some(organization_id),
53 )
54 .with_resource("Ticket", ticket.id)
55 .log();
56
57 HttpResponse::Created().json(ticket)
58 }
59 Err(err) => {
60 AuditLogEntry::new(
61 AuditEventType::TicketCreated,
62 Some(user.user_id),
63 Some(organization_id),
64 )
65 .with_error(err.clone())
66 .log();
67
68 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
69 }
70 }
71}
72
73#[utoipa::path(
74 get,
75 path = "/tickets/{id}",
76 tag = "Tickets",
77 summary = "Get a ticket by ID",
78 params(
79 ("id" = Uuid, Path, description = "Ticket ID")
80 ),
81 responses(
82 (status = 200, description = "Ticket found"),
83 (status = 404, description = "Ticket not found"),
84 (status = 500, description = "Internal server error"),
85 ),
86 security(("bearer_auth" = []))
87)]
88#[get("/tickets/{id}")]
89pub async fn get_ticket(
90 state: web::Data<AppState>,
91 user: AuthenticatedUser,
92 id: web::Path<Uuid>,
93) -> impl Responder {
94 match state.ticket_use_cases.get_ticket(*id).await {
95 Ok(Some(ticket)) => {
96 if let Err(e) = user.verify_org_access(ticket.organization_id) {
98 return HttpResponse::Forbidden().json(serde_json::json!({ "error": e }));
99 }
100 HttpResponse::Ok().json(ticket)
101 }
102 Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
103 "error": "Ticket not found"
104 })),
105 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
106 }
107}
108
109#[utoipa::path(
110 get,
111 path = "/buildings/{building_id}/tickets",
112 tag = "Tickets",
113 summary = "List all tickets for a building",
114 params(
115 ("building_id" = Uuid, Path, description = "Building ID")
116 ),
117 responses(
118 (status = 200, description = "List of tickets"),
119 (status = 500, description = "Internal server error"),
120 ),
121 security(("bearer_auth" = []))
122)]
123#[get("/buildings/{building_id}/tickets")]
124pub async fn list_building_tickets(
125 state: web::Data<AppState>,
126 building_id: web::Path<Uuid>,
127) -> impl Responder {
128 match state
129 .ticket_use_cases
130 .list_tickets_by_building(*building_id)
131 .await
132 {
133 Ok(tickets) => HttpResponse::Ok().json(tickets),
134 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
135 }
136}
137
138#[utoipa::path(
139 get,
140 path = "/organizations/{organization_id}/tickets",
141 tag = "Tickets",
142 summary = "List all tickets for an organization",
143 params(
144 ("organization_id" = Uuid, Path, description = "Organization ID")
145 ),
146 responses(
147 (status = 200, description = "List of tickets"),
148 (status = 500, description = "Internal server error"),
149 ),
150 security(("bearer_auth" = []))
151)]
152#[get("/organizations/{organization_id}/tickets")]
153pub async fn list_organization_tickets(
154 state: web::Data<AppState>,
155 user: AuthenticatedUser,
156 organization_id: web::Path<Uuid>,
157) -> impl Responder {
158 if let Err(e) = user.verify_org_access(*organization_id) {
159 return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
160 }
161 match state
162 .ticket_use_cases
163 .list_tickets_by_organization(*organization_id)
164 .await
165 {
166 Ok(tickets) => HttpResponse::Ok().json(tickets),
167 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
168 }
169}
170
171#[utoipa::path(
172 get,
173 path = "/tickets/my",
174 tag = "Tickets",
175 summary = "List tickets created by the authenticated user",
176 responses(
177 (status = 200, description = "List of tickets"),
178 (status = 500, description = "Internal server error"),
179 ),
180 security(("bearer_auth" = []))
181)]
182#[get("/tickets/my")]
183pub async fn list_my_tickets(
184 state: web::Data<AppState>,
185 user: AuthenticatedUser,
186) -> impl Responder {
187 let created_by = user.user_id;
188
189 match state.ticket_use_cases.list_my_tickets(created_by).await {
190 Ok(tickets) => HttpResponse::Ok().json(tickets),
191 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
192 }
193}
194
195#[utoipa::path(
196 get,
197 path = "/tickets/assigned-to-me",
198 tag = "Tickets",
199 summary = "List tickets assigned to the authenticated user",
200 responses(
201 (status = 200, description = "List of assigned tickets"),
202 (status = 500, description = "Internal server error"),
203 ),
204 security(("bearer_auth" = []))
205)]
206#[get("/tickets/assigned-to-me")]
207pub async fn list_assigned_tickets(
208 state: web::Data<AppState>,
209 user: AuthenticatedUser,
210) -> impl Responder {
211 let assigned_to = user.user_id;
212
213 match state
214 .ticket_use_cases
215 .list_assigned_tickets(assigned_to)
216 .await
217 {
218 Ok(tickets) => HttpResponse::Ok().json(tickets),
219 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
220 }
221}
222
223#[utoipa::path(
224 get,
225 path = "/buildings/{building_id}/tickets/status/{status}",
226 tag = "Tickets",
227 summary = "List tickets by status for a building",
228 params(
229 ("building_id" = Uuid, Path, description = "Building ID"),
230 ("status" = String, Path, description = "Ticket status (Open, InProgress, Resolved, Closed, Cancelled)")
231 ),
232 responses(
233 (status = 200, description = "List of tickets"),
234 (status = 400, description = "Invalid status"),
235 (status = 500, description = "Internal server error"),
236 ),
237 security(("bearer_auth" = []))
238)]
239#[get("/buildings/{building_id}/tickets/status/{status}")]
240pub async fn list_tickets_by_status(
241 state: web::Data<AppState>,
242 path: web::Path<(Uuid, String)>,
243) -> impl Responder {
244 let (building_id, status_str) = path.into_inner();
245
246 let status = match status_str.as_str() {
247 "Open" | "open" => TicketStatus::Open,
248 "InProgress" | "in_progress" => TicketStatus::InProgress,
249 "Resolved" | "resolved" => TicketStatus::Resolved,
250 "Closed" | "closed" => TicketStatus::Closed,
251 "Cancelled" | "cancelled" => TicketStatus::Cancelled,
252 _ => {
253 return HttpResponse::BadRequest().json(serde_json::json!({
254 "error": format!("Invalid status: {}", status_str)
255 }))
256 }
257 };
258
259 match state
260 .ticket_use_cases
261 .list_tickets_by_status(building_id, status)
262 .await
263 {
264 Ok(tickets) => HttpResponse::Ok().json(tickets),
265 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
266 }
267}
268
269#[utoipa::path(
270 delete,
271 path = "/tickets/{id}",
272 tag = "Tickets",
273 summary = "Delete a ticket",
274 params(
275 ("id" = Uuid, Path, description = "Ticket ID")
276 ),
277 responses(
278 (status = 204, description = "Ticket deleted"),
279 (status = 400, description = "Bad request"),
280 (status = 401, description = "Unauthorized"),
281 (status = 404, description = "Ticket not found"),
282 ),
283 security(("bearer_auth" = []))
284)]
285#[delete("/tickets/{id}")]
286pub async fn delete_ticket(
287 state: web::Data<AppState>,
288 user: AuthenticatedUser,
289 id: web::Path<Uuid>,
290) -> impl Responder {
291 let organization_id = match user.require_organization() {
292 Ok(org_id) => org_id,
293 Err(e) => {
294 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
295 }
296 };
297
298 match state.ticket_use_cases.delete_ticket(*id).await {
299 Ok(true) => {
300 AuditLogEntry::new(
301 AuditEventType::TicketDeleted,
302 Some(user.user_id),
303 Some(organization_id),
304 )
305 .with_resource("Ticket", *id)
306 .log();
307
308 HttpResponse::NoContent().finish()
309 }
310 Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
311 "error": "Ticket not found"
312 })),
313 Err(err) => {
314 AuditLogEntry::new(
315 AuditEventType::TicketDeleted,
316 Some(user.user_id),
317 Some(organization_id),
318 )
319 .with_error(err.clone())
320 .log();
321
322 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
323 }
324 }
325}
326
327#[utoipa::path(
330 put,
331 path = "/tickets/{id}/assign",
332 tag = "Tickets",
333 summary = "Assign a ticket to a contractor",
334 params(
335 ("id" = Uuid, Path, description = "Ticket ID")
336 ),
337 request_body = AssignTicketRequest,
338 responses(
339 (status = 200, description = "Ticket assigned"),
340 (status = 400, description = "Bad request"),
341 (status = 401, description = "Unauthorized"),
342 ),
343 security(("bearer_auth" = []))
344)]
345#[put("/tickets/{id}/assign")]
346pub async fn assign_ticket(
347 state: web::Data<AppState>,
348 user: AuthenticatedUser,
349 id: web::Path<Uuid>,
350 request: web::Json<AssignTicketRequest>,
351) -> impl Responder {
352 let organization_id = match user.require_organization() {
353 Ok(org_id) => org_id,
354 Err(e) => {
355 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
356 }
357 };
358
359 match state
360 .ticket_use_cases
361 .assign_ticket(*id, request.into_inner())
362 .await
363 {
364 Ok(ticket) => {
365 AuditLogEntry::new(
366 AuditEventType::TicketAssigned,
367 Some(user.user_id),
368 Some(organization_id),
369 )
370 .with_resource("Ticket", ticket.id)
371 .log();
372
373 HttpResponse::Ok().json(ticket)
374 }
375 Err(err) => {
376 AuditLogEntry::new(
377 AuditEventType::TicketAssigned,
378 Some(user.user_id),
379 Some(organization_id),
380 )
381 .with_error(err.clone())
382 .log();
383
384 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
385 }
386 }
387}
388
389#[utoipa::path(
390 put,
391 path = "/tickets/{id}/start-work",
392 tag = "Tickets",
393 summary = "Start work on an assigned ticket",
394 params(
395 ("id" = Uuid, Path, description = "Ticket ID")
396 ),
397 responses(
398 (status = 200, description = "Work started"),
399 (status = 400, description = "Bad request"),
400 (status = 401, description = "Unauthorized"),
401 ),
402 security(("bearer_auth" = []))
403)]
404#[put("/tickets/{id}/start-work")]
405pub async fn start_work(
406 state: web::Data<AppState>,
407 user: AuthenticatedUser,
408 id: web::Path<Uuid>,
409) -> impl Responder {
410 let organization_id = match user.require_organization() {
411 Ok(org_id) => org_id,
412 Err(e) => {
413 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
414 }
415 };
416
417 match state.ticket_use_cases.start_work(*id).await {
418 Ok(ticket) => {
419 AuditLogEntry::new(
420 AuditEventType::TicketStatusChanged,
421 Some(user.user_id),
422 Some(organization_id),
423 )
424 .with_resource("Ticket", ticket.id)
425 .log();
426
427 HttpResponse::Ok().json(ticket)
428 }
429 Err(err) => {
430 AuditLogEntry::new(
431 AuditEventType::TicketStatusChanged,
432 Some(user.user_id),
433 Some(organization_id),
434 )
435 .with_error(err.clone())
436 .log();
437
438 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
439 }
440 }
441}
442
443#[utoipa::path(
444 put,
445 path = "/tickets/{id}/resolve",
446 tag = "Tickets",
447 summary = "Mark a ticket as resolved",
448 params(
449 ("id" = Uuid, Path, description = "Ticket ID")
450 ),
451 request_body = ResolveTicketRequest,
452 responses(
453 (status = 200, description = "Ticket resolved"),
454 (status = 400, description = "Bad request"),
455 (status = 401, description = "Unauthorized"),
456 ),
457 security(("bearer_auth" = []))
458)]
459#[put("/tickets/{id}/resolve")]
460pub async fn resolve_ticket(
461 state: web::Data<AppState>,
462 user: AuthenticatedUser,
463 id: web::Path<Uuid>,
464 request: web::Json<ResolveTicketRequest>,
465) -> impl Responder {
466 let organization_id = match user.require_organization() {
467 Ok(org_id) => org_id,
468 Err(e) => {
469 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
470 }
471 };
472
473 match state
474 .ticket_use_cases
475 .resolve_ticket(*id, request.into_inner())
476 .await
477 {
478 Ok(ticket) => {
479 AuditLogEntry::new(
480 AuditEventType::TicketResolved,
481 Some(user.user_id),
482 Some(organization_id),
483 )
484 .with_resource("Ticket", ticket.id)
485 .log();
486
487 HttpResponse::Ok().json(ticket)
488 }
489 Err(err) => {
490 AuditLogEntry::new(
491 AuditEventType::TicketResolved,
492 Some(user.user_id),
493 Some(organization_id),
494 )
495 .with_error(err.clone())
496 .log();
497
498 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
499 }
500 }
501}
502
503#[utoipa::path(
504 put,
505 path = "/tickets/{id}/close",
506 tag = "Tickets",
507 summary = "Close a resolved ticket",
508 params(
509 ("id" = Uuid, Path, description = "Ticket ID")
510 ),
511 responses(
512 (status = 200, description = "Ticket closed"),
513 (status = 400, description = "Bad request"),
514 (status = 401, description = "Unauthorized"),
515 ),
516 security(("bearer_auth" = []))
517)]
518#[put("/tickets/{id}/close")]
519pub async fn close_ticket(
520 state: web::Data<AppState>,
521 user: AuthenticatedUser,
522 id: web::Path<Uuid>,
523) -> impl Responder {
524 let organization_id = match user.require_organization() {
525 Ok(org_id) => org_id,
526 Err(e) => {
527 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
528 }
529 };
530
531 match state.ticket_use_cases.close_ticket(*id).await {
532 Ok(ticket) => {
533 AuditLogEntry::new(
534 AuditEventType::TicketClosed,
535 Some(user.user_id),
536 Some(organization_id),
537 )
538 .with_resource("Ticket", ticket.id)
539 .log();
540
541 HttpResponse::Ok().json(ticket)
542 }
543 Err(err) => {
544 AuditLogEntry::new(
545 AuditEventType::TicketClosed,
546 Some(user.user_id),
547 Some(organization_id),
548 )
549 .with_error(err.clone())
550 .log();
551
552 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
553 }
554 }
555}
556
557#[utoipa::path(
558 put,
559 path = "/tickets/{id}/cancel",
560 tag = "Tickets",
561 summary = "Cancel a ticket",
562 params(
563 ("id" = Uuid, Path, description = "Ticket ID")
564 ),
565 request_body = CancelTicketRequest,
566 responses(
567 (status = 200, description = "Ticket cancelled"),
568 (status = 400, description = "Bad request"),
569 (status = 401, description = "Unauthorized"),
570 ),
571 security(("bearer_auth" = []))
572)]
573#[put("/tickets/{id}/cancel")]
574pub async fn cancel_ticket(
575 state: web::Data<AppState>,
576 user: AuthenticatedUser,
577 id: web::Path<Uuid>,
578 request: web::Json<CancelTicketRequest>,
579) -> impl Responder {
580 let organization_id = match user.require_organization() {
581 Ok(org_id) => org_id,
582 Err(e) => {
583 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
584 }
585 };
586
587 match state
588 .ticket_use_cases
589 .cancel_ticket(*id, request.into_inner())
590 .await
591 {
592 Ok(ticket) => {
593 AuditLogEntry::new(
594 AuditEventType::TicketCancelled,
595 Some(user.user_id),
596 Some(organization_id),
597 )
598 .with_resource("Ticket", ticket.id)
599 .log();
600
601 HttpResponse::Ok().json(ticket)
602 }
603 Err(err) => {
604 AuditLogEntry::new(
605 AuditEventType::TicketCancelled,
606 Some(user.user_id),
607 Some(organization_id),
608 )
609 .with_error(err.clone())
610 .log();
611
612 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
613 }
614 }
615}
616
617#[utoipa::path(
618 put,
619 path = "/tickets/{id}/reopen",
620 tag = "Tickets",
621 summary = "Reopen a closed or cancelled ticket",
622 params(
623 ("id" = Uuid, Path, description = "Ticket ID")
624 ),
625 request_body = ReopenTicketRequest,
626 responses(
627 (status = 200, description = "Ticket reopened"),
628 (status = 400, description = "Bad request"),
629 (status = 401, description = "Unauthorized"),
630 ),
631 security(("bearer_auth" = []))
632)]
633#[put("/tickets/{id}/reopen")]
634pub async fn reopen_ticket(
635 state: web::Data<AppState>,
636 user: AuthenticatedUser,
637 id: web::Path<Uuid>,
638 request: web::Json<ReopenTicketRequest>,
639) -> impl Responder {
640 let organization_id = match user.require_organization() {
641 Ok(org_id) => org_id,
642 Err(e) => {
643 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
644 }
645 };
646
647 match state
648 .ticket_use_cases
649 .reopen_ticket(*id, request.into_inner())
650 .await
651 {
652 Ok(ticket) => {
653 AuditLogEntry::new(
654 AuditEventType::TicketReopened,
655 Some(user.user_id),
656 Some(organization_id),
657 )
658 .with_resource("Ticket", ticket.id)
659 .log();
660
661 HttpResponse::Ok().json(ticket)
662 }
663 Err(err) => {
664 AuditLogEntry::new(
665 AuditEventType::TicketReopened,
666 Some(user.user_id),
667 Some(organization_id),
668 )
669 .with_error(err.clone())
670 .log();
671
672 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
673 }
674 }
675}
676
677#[utoipa::path(
680 put,
681 path = "/tickets/{id}/send-work-order",
682 tag = "Tickets",
683 summary = "Send work order to contractor (magic link PWA)",
684 params(
685 ("id" = Uuid, Path, description = "Ticket ID")
686 ),
687 responses(
688 (status = 200, description = "Work order sent"),
689 (status = 400, description = "Bad request"),
690 (status = 401, description = "Unauthorized"),
691 ),
692 security(("bearer_auth" = []))
693)]
694#[put("/tickets/{id}/send-work-order")]
695pub async fn send_work_order(
696 state: web::Data<AppState>,
697 user: AuthenticatedUser,
698 id: web::Path<Uuid>,
699) -> impl Responder {
700 let organization_id = match user.require_organization() {
701 Ok(org_id) => org_id,
702 Err(e) => {
703 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
704 }
705 };
706
707 match state.ticket_use_cases.send_work_order(*id).await {
708 Ok(ticket) => {
709 AuditLogEntry::new(
710 AuditEventType::TicketWorkOrderSent,
711 Some(user.user_id),
712 Some(organization_id),
713 )
714 .with_resource("Ticket", ticket.id)
715 .log();
716
717 HttpResponse::Ok().json(ticket)
718 }
719 Err(err) => {
720 AuditLogEntry::new(
721 AuditEventType::TicketWorkOrderSent,
722 Some(user.user_id),
723 Some(organization_id),
724 )
725 .with_error(err.clone())
726 .log();
727
728 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
729 }
730 }
731}
732
733#[utoipa::path(
736 get,
737 path = "/tickets/statistics",
738 tag = "Tickets",
739 summary = "Get ticket statistics for the organization",
740 responses(
741 (status = 200, description = "Ticket statistics"),
742 (status = 401, description = "Unauthorized"),
743 (status = 500, description = "Internal server error"),
744 ),
745 security(("bearer_auth" = []))
746)]
747#[get("/tickets/statistics")]
748pub async fn get_ticket_statistics_org(
749 state: web::Data<AppState>,
750 user: AuthenticatedUser,
751) -> impl Responder {
752 let organization_id = match user.require_organization() {
753 Ok(org_id) => org_id,
754 Err(e) => {
755 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
756 }
757 };
758
759 match state
760 .ticket_use_cases
761 .get_ticket_statistics_by_organization(organization_id)
762 .await
763 {
764 Ok(stats) => HttpResponse::Ok().json(stats),
765 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
766 }
767}
768
769#[utoipa::path(
770 get,
771 path = "/tickets/overdue",
772 tag = "Tickets",
773 summary = "List overdue tickets for the organization",
774 params(
775 ("max_days" = Option<i64>, Query, description = "Maximum overdue days filter (default: 7)")
776 ),
777 responses(
778 (status = 200, description = "List of overdue tickets"),
779 (status = 401, description = "Unauthorized"),
780 (status = 500, description = "Internal server error"),
781 ),
782 security(("bearer_auth" = []))
783)]
784#[get("/tickets/overdue")]
785pub async fn get_overdue_tickets_org(
786 state: web::Data<AppState>,
787 user: AuthenticatedUser,
788 query: web::Query<OverdueQuery>,
789) -> impl Responder {
790 let organization_id = match user.require_organization() {
791 Ok(org_id) => org_id,
792 Err(e) => {
793 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
794 }
795 };
796
797 let max_days = query.max_days.unwrap_or(7);
798
799 match state
800 .ticket_use_cases
801 .get_overdue_tickets_by_organization(organization_id, max_days)
802 .await
803 {
804 Ok(tickets) => HttpResponse::Ok().json(tickets),
805 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
806 }
807}
808
809#[utoipa::path(
810 get,
811 path = "/buildings/{building_id}/tickets/statistics",
812 tag = "Tickets",
813 summary = "Get ticket statistics for a building",
814 params(
815 ("building_id" = Uuid, Path, description = "Building ID")
816 ),
817 responses(
818 (status = 200, description = "Ticket statistics"),
819 (status = 500, description = "Internal server error"),
820 ),
821 security(("bearer_auth" = []))
822)]
823#[get("/buildings/{building_id}/tickets/statistics")]
824pub async fn get_ticket_statistics(
825 state: web::Data<AppState>,
826 building_id: web::Path<Uuid>,
827) -> impl Responder {
828 match state
829 .ticket_use_cases
830 .get_ticket_statistics(*building_id)
831 .await
832 {
833 Ok(stats) => HttpResponse::Ok().json(stats),
834 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
835 }
836}
837
838#[utoipa::path(
839 get,
840 path = "/buildings/{building_id}/tickets/overdue",
841 tag = "Tickets",
842 summary = "List overdue tickets for a building",
843 params(
844 ("building_id" = Uuid, Path, description = "Building ID"),
845 ("max_days" = Option<i64>, Query, description = "Maximum overdue days filter (default: 7)")
846 ),
847 responses(
848 (status = 200, description = "List of overdue tickets"),
849 (status = 500, description = "Internal server error"),
850 ),
851 security(("bearer_auth" = []))
852)]
853#[get("/buildings/{building_id}/tickets/overdue")]
854pub async fn get_overdue_tickets(
855 state: web::Data<AppState>,
856 building_id: web::Path<Uuid>,
857 query: web::Query<OverdueQuery>,
858) -> impl Responder {
859 let max_days = query.max_days.unwrap_or(7);
860
861 match state
862 .ticket_use_cases
863 .get_overdue_tickets(*building_id, max_days)
864 .await
865 {
866 Ok(tickets) => HttpResponse::Ok().json(tickets),
867 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
868 }
869}
870
871#[derive(serde::Deserialize)]
872pub struct OverdueQuery {
873 pub max_days: Option<i64>,
874}