koprogo_api/infrastructure/web/handlers/
ticket_handlers.rs

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// ==================== Ticket CRUD Endpoints ====================
12
13#[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    // Use owner_id from AuthenticatedUser as created_by
41    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            // Multi-tenant isolation: verify ticket belongs to user's organization
97            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// ==================== Ticket Workflow Endpoints ====================
324
325#[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// ==================== Ticket Work Order Endpoint ====================
674
675#[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// ==================== Ticket Statistics Endpoints ====================
730
731#[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}