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    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// ==================== Ticket Workflow Endpoints ====================
328
329#[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// ==================== Ticket Work Order Endpoint ====================
678
679#[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// ==================== Ticket Statistics Endpoints ====================
734
735#[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}