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