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    user: AuthenticatedUser,
117    organization_id: web::Path<Uuid>,
118) -> impl Responder {
119    if let Err(e) = user.verify_org_access(*organization_id) {
120        return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
121    }
122    match state
123        .work_report_use_cases
124        .list_work_reports_by_organization(*organization_id)
125        .await
126    {
127        Ok(work_reports) => HttpResponse::Ok().json(work_reports),
128        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
129    }
130}
131
132/// List work reports with pagination and filters
133#[get("/work-reports")]
134pub async fn list_work_reports_paginated(
135    state: web::Data<AppState>,
136    page_request: web::Query<PageRequest>,
137    filters: web::Query<WorkReportFilters>,
138) -> impl Responder {
139    match state
140        .work_report_use_cases
141        .list_work_reports_paginated(&page_request.into_inner(), &filters.into_inner())
142        .await
143    {
144        Ok(response) => HttpResponse::Ok().json(response),
145        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
146    }
147}
148
149/// Update work report
150#[put("/work-reports/{id}")]
151pub async fn update_work_report(
152    state: web::Data<AppState>,
153    user: AuthenticatedUser,
154    id: web::Path<Uuid>,
155    request: web::Json<UpdateWorkReportDto>,
156) -> impl Responder {
157    let organization_id = match user.require_organization() {
158        Ok(org_id) => org_id,
159        Err(e) => {
160            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
161        }
162    };
163
164    match state
165        .work_report_use_cases
166        .update_work_report(*id, request.into_inner())
167        .await
168    {
169        Ok(work_report) => {
170            AuditLogEntry::new(
171                AuditEventType::WorkReportUpdated,
172                Some(user.user_id),
173                Some(organization_id),
174            )
175            .with_resource("WorkReport", *id)
176            .log();
177
178            HttpResponse::Ok().json(work_report)
179        }
180        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
181    }
182}
183
184/// Delete work report
185#[delete("/work-reports/{id}")]
186pub async fn delete_work_report(
187    state: web::Data<AppState>,
188    user: AuthenticatedUser,
189    id: web::Path<Uuid>,
190) -> impl Responder {
191    let organization_id = match user.require_organization() {
192        Ok(org_id) => org_id,
193        Err(e) => {
194            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
195        }
196    };
197
198    match state.work_report_use_cases.delete_work_report(*id).await {
199        Ok(deleted) => {
200            if deleted {
201                AuditLogEntry::new(
202                    AuditEventType::WorkReportDeleted,
203                    Some(user.user_id),
204                    Some(organization_id),
205                )
206                .with_resource("WorkReport", *id)
207                .log();
208
209                HttpResponse::NoContent().finish()
210            } else {
211                HttpResponse::NotFound().json(serde_json::json!({
212                    "error": "Work report not found"
213                }))
214            }
215        }
216        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
217    }
218}
219
220// ==================== Warranty Management Endpoints ====================
221
222/// Get active warranties for a building
223#[get("/buildings/{building_id}/work-reports/warranties/active")]
224pub async fn get_active_warranties(
225    state: web::Data<AppState>,
226    building_id: web::Path<Uuid>,
227) -> impl Responder {
228    match state
229        .work_report_use_cases
230        .get_active_warranties(*building_id)
231        .await
232    {
233        Ok(warranties) => HttpResponse::Ok().json(warranties),
234        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
235    }
236}
237
238/// Get expiring warranties for a building (within X days)
239#[get("/buildings/{building_id}/work-reports/warranties/expiring")]
240pub async fn get_expiring_warranties(
241    state: web::Data<AppState>,
242    path: web::Path<Uuid>,
243    query: web::Query<serde_json::Value>,
244) -> impl Responder {
245    let building_id = path.into_inner();
246    let days = query.get("days").and_then(|v| v.as_i64()).unwrap_or(90) as i32;
247
248    match state
249        .work_report_use_cases
250        .get_expiring_warranties(building_id, days)
251        .await
252    {
253        Ok(warranties) => HttpResponse::Ok().json(warranties),
254        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
255    }
256}
257
258// ==================== Photo & Document Management ====================
259
260/// Add photo to work report
261#[post("/work-reports/{id}/photos")]
262pub async fn add_photo(
263    state: web::Data<AppState>,
264    user: AuthenticatedUser,
265    id: web::Path<Uuid>,
266    request: web::Json<AddPhotoDto>,
267) -> impl Responder {
268    let organization_id = match user.require_organization() {
269        Ok(org_id) => org_id,
270        Err(e) => {
271            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
272        }
273    };
274
275    match state
276        .work_report_use_cases
277        .add_photo(*id, request.into_inner())
278        .await
279    {
280        Ok(work_report) => {
281            AuditLogEntry::new(
282                AuditEventType::WorkReportPhotoAdded,
283                Some(user.user_id),
284                Some(organization_id),
285            )
286            .with_resource("WorkReport", *id)
287            .log();
288
289            HttpResponse::Ok().json(work_report)
290        }
291        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
292    }
293}
294
295/// Add document to work report
296#[post("/work-reports/{id}/documents")]
297pub async fn add_document(
298    state: web::Data<AppState>,
299    user: AuthenticatedUser,
300    id: web::Path<Uuid>,
301    request: web::Json<AddDocumentDto>,
302) -> impl Responder {
303    let organization_id = match user.require_organization() {
304        Ok(org_id) => org_id,
305        Err(e) => {
306            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
307        }
308    };
309
310    match state
311        .work_report_use_cases
312        .add_document(*id, request.into_inner())
313        .await
314    {
315        Ok(work_report) => {
316            AuditLogEntry::new(
317                AuditEventType::WorkReportDocumentAdded,
318                Some(user.user_id),
319                Some(organization_id),
320            )
321            .with_resource("WorkReport", *id)
322            .log();
323
324            HttpResponse::Ok().json(work_report)
325        }
326        Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
327    }
328}