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