koprogo_api/infrastructure/web/handlers/
work_report_handlers.rs

1use crate::application::dto::{
2    AddDocumentDto, AddPhotoDto, CreateWorkReportDto, PageRequest, UpdateWorkReportDto,
3    WorkReportFilters,
4};
5use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
6use crate::infrastructure::web::{AppState, AuthenticatedUser};
7use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
8use uuid::Uuid;
9
10// ==================== Work Report CRUD Endpoints ====================
11
12/// Create a new work report
13#[post("/work-reports")]
14pub async fn create_work_report(
15    state: web::Data<AppState>,
16    user: AuthenticatedUser,
17    request: web::Json<CreateWorkReportDto>,
18) -> impl Responder {
19    let organization_id = match user.require_organization() {
20        Ok(org_id) => org_id,
21        Err(e) => {
22            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
23        }
24    };
25
26    match state
27        .work_report_use_cases
28        .create_work_report(request.into_inner())
29        .await
30    {
31        Ok(work_report) => {
32            AuditLogEntry::new(
33                AuditEventType::WorkReportCreated,
34                Some(user.user_id),
35                Some(organization_id),
36            )
37            .with_resource("WorkReport", work_report.id.parse().unwrap())
38            .log();
39
40            HttpResponse::Created().json(work_report)
41        }
42        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
43    }
44}
45
46/// Get work report by ID
47#[get("/work-reports/{id}")]
48pub async fn get_work_report(
49    state: web::Data<AppState>,
50    user: AuthenticatedUser,
51    id: web::Path<Uuid>,
52) -> impl Responder {
53    match state.work_report_use_cases.get_work_report(*id).await {
54        Ok(Some(work_report)) => {
55            // Parse building_id (it's a String in the DTO) and fetch building to get organization_id
56            let building_id = match uuid::Uuid::parse_str(&work_report.building_id) {
57                Ok(bid) => bid,
58                Err(_) => {
59                    return HttpResponse::InternalServerError()
60                        .json(serde_json::json!({"error": "Invalid building_id format"}))
61                }
62            };
63
64            match state.building_use_cases.get_building(building_id).await {
65                Ok(Some(building)) => {
66                    // Verify organization access
67                    let org_id = match uuid::Uuid::parse_str(&building.organization_id) {
68                        Ok(oid) => oid,
69                        Err(_) => {
70                            return HttpResponse::InternalServerError().json(
71                                serde_json::json!({"error": "Invalid organization_id format"}),
72                            )
73                        }
74                    };
75
76                    if let Err(err) = user.verify_org_access(org_id) {
77                        return HttpResponse::Forbidden().json(serde_json::json!({"error": err}));
78                    }
79                    HttpResponse::Ok().json(work_report)
80                }
81                Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
82                    "error": "Building not found"
83                })),
84                Err(err) => {
85                    HttpResponse::InternalServerError().json(serde_json::json!({"error": err}))
86                }
87            }
88        }
89        Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
90            "error": "Work report not found"
91        })),
92        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
93    }
94}
95
96/// List work reports by building
97#[get("/buildings/{building_id}/work-reports")]
98pub async fn list_building_work_reports(
99    state: web::Data<AppState>,
100    building_id: web::Path<Uuid>,
101) -> impl Responder {
102    match state
103        .work_report_use_cases
104        .list_work_reports_by_building(*building_id)
105        .await
106    {
107        Ok(work_reports) => HttpResponse::Ok().json(work_reports),
108        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
109    }
110}
111
112/// List work reports by organization
113#[get("/organizations/{organization_id}/work-reports")]
114pub async fn list_organization_work_reports(
115    state: web::Data<AppState>,
116    organization_id: web::Path<Uuid>,
117) -> impl Responder {
118    match state
119        .work_report_use_cases
120        .list_work_reports_by_organization(*organization_id)
121        .await
122    {
123        Ok(work_reports) => HttpResponse::Ok().json(work_reports),
124        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
125    }
126}
127
128/// List work reports with pagination and filters
129#[get("/work-reports")]
130pub async fn list_work_reports_paginated(
131    state: web::Data<AppState>,
132    page_request: web::Query<PageRequest>,
133    filters: web::Query<WorkReportFilters>,
134) -> impl Responder {
135    match state
136        .work_report_use_cases
137        .list_work_reports_paginated(&page_request.into_inner(), &filters.into_inner())
138        .await
139    {
140        Ok(response) => HttpResponse::Ok().json(response),
141        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
142    }
143}
144
145/// Update work report
146#[put("/work-reports/{id}")]
147pub async fn update_work_report(
148    state: web::Data<AppState>,
149    user: AuthenticatedUser,
150    id: web::Path<Uuid>,
151    request: web::Json<UpdateWorkReportDto>,
152) -> impl Responder {
153    let organization_id = match user.require_organization() {
154        Ok(org_id) => org_id,
155        Err(e) => {
156            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
157        }
158    };
159
160    match state
161        .work_report_use_cases
162        .update_work_report(*id, request.into_inner())
163        .await
164    {
165        Ok(work_report) => {
166            AuditLogEntry::new(
167                AuditEventType::WorkReportUpdated,
168                Some(user.user_id),
169                Some(organization_id),
170            )
171            .with_resource("WorkReport", *id)
172            .log();
173
174            HttpResponse::Ok().json(work_report)
175        }
176        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
177    }
178}
179
180/// Delete work report
181#[delete("/work-reports/{id}")]
182pub async fn delete_work_report(
183    state: web::Data<AppState>,
184    user: AuthenticatedUser,
185    id: web::Path<Uuid>,
186) -> impl Responder {
187    let organization_id = match user.require_organization() {
188        Ok(org_id) => org_id,
189        Err(e) => {
190            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
191        }
192    };
193
194    match state.work_report_use_cases.delete_work_report(*id).await {
195        Ok(deleted) => {
196            if deleted {
197                AuditLogEntry::new(
198                    AuditEventType::WorkReportDeleted,
199                    Some(user.user_id),
200                    Some(organization_id),
201                )
202                .with_resource("WorkReport", *id)
203                .log();
204
205                HttpResponse::NoContent().finish()
206            } else {
207                HttpResponse::NotFound().json(serde_json::json!({
208                    "error": "Work report not found"
209                }))
210            }
211        }
212        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
213    }
214}
215
216// ==================== Warranty Management Endpoints ====================
217
218/// Get active warranties for a building
219#[get("/buildings/{building_id}/work-reports/warranties/active")]
220pub async fn get_active_warranties(
221    state: web::Data<AppState>,
222    building_id: web::Path<Uuid>,
223) -> impl Responder {
224    match state
225        .work_report_use_cases
226        .get_active_warranties(*building_id)
227        .await
228    {
229        Ok(warranties) => HttpResponse::Ok().json(warranties),
230        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
231    }
232}
233
234/// Get expiring warranties for a building (within X days)
235#[get("/buildings/{building_id}/work-reports/warranties/expiring")]
236pub async fn get_expiring_warranties(
237    state: web::Data<AppState>,
238    path: web::Path<Uuid>,
239    query: web::Query<serde_json::Value>,
240) -> impl Responder {
241    let building_id = path.into_inner();
242    let days = query.get("days").and_then(|v| v.as_i64()).unwrap_or(90) as i32;
243
244    match state
245        .work_report_use_cases
246        .get_expiring_warranties(building_id, days)
247        .await
248    {
249        Ok(warranties) => HttpResponse::Ok().json(warranties),
250        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
251    }
252}
253
254// ==================== Photo & Document Management ====================
255
256/// Add photo to work report
257#[post("/work-reports/{id}/photos")]
258pub async fn add_photo(
259    state: web::Data<AppState>,
260    user: AuthenticatedUser,
261    id: web::Path<Uuid>,
262    request: web::Json<AddPhotoDto>,
263) -> impl Responder {
264    let organization_id = match user.require_organization() {
265        Ok(org_id) => org_id,
266        Err(e) => {
267            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
268        }
269    };
270
271    match state
272        .work_report_use_cases
273        .add_photo(*id, request.into_inner())
274        .await
275    {
276        Ok(work_report) => {
277            AuditLogEntry::new(
278                AuditEventType::WorkReportPhotoAdded,
279                Some(user.user_id),
280                Some(organization_id),
281            )
282            .with_resource("WorkReport", *id)
283            .log();
284
285            HttpResponse::Ok().json(work_report)
286        }
287        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
288    }
289}
290
291/// Add document to work report
292#[post("/work-reports/{id}/documents")]
293pub async fn add_document(
294    state: web::Data<AppState>,
295    user: AuthenticatedUser,
296    id: web::Path<Uuid>,
297    request: web::Json<AddDocumentDto>,
298) -> impl Responder {
299    let organization_id = match user.require_organization() {
300        Ok(org_id) => org_id,
301        Err(e) => {
302            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
303        }
304    };
305
306    match state
307        .work_report_use_cases
308        .add_document(*id, request.into_inner())
309        .await
310    {
311        Ok(work_report) => {
312            AuditLogEntry::new(
313                AuditEventType::WorkReportDocumentAdded,
314                Some(user.user_id),
315                Some(organization_id),
316            )
317            .with_resource("WorkReport", *id)
318            .log();
319
320            HttpResponse::Ok().json(work_report)
321        }
322        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
323    }
324}