1use crate::application::dto::{
2 AddAgendaItemRequest, CompleteMeetingRequest, CreateMeetingRequest, PageRequest, PageResponse,
3 RescheduleMeetingRequest, UpdateMeetingRequest,
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#[post("/meetings")]
11pub async fn create_meeting(
12 state: web::Data<AppState>,
13 user: AuthenticatedUser, mut request: web::Json<CreateMeetingRequest>,
15) -> impl Responder {
16 let organization_id = match user.require_organization() {
19 Ok(org_id) => org_id,
20 Err(e) => {
21 return HttpResponse::Unauthorized().json(serde_json::json!({
22 "error": e.to_string()
23 }))
24 }
25 };
26 request.organization_id = organization_id;
27
28 match state
29 .meeting_use_cases
30 .create_meeting(request.into_inner())
31 .await
32 {
33 Ok(meeting) => {
34 AuditLogEntry::new(
36 AuditEventType::MeetingCreated,
37 Some(user.user_id),
38 Some(organization_id),
39 )
40 .with_resource("Meeting", meeting.id)
41 .log();
42
43 HttpResponse::Created().json(meeting)
44 }
45 Err(err) => {
46 AuditLogEntry::new(
48 AuditEventType::MeetingCreated,
49 Some(user.user_id),
50 Some(organization_id),
51 )
52 .with_error(err.clone())
53 .log();
54
55 HttpResponse::BadRequest().json(serde_json::json!({
56 "error": err
57 }))
58 }
59 }
60}
61
62#[get("/meetings/{id}")]
63pub async fn get_meeting(state: web::Data<AppState>, id: web::Path<Uuid>) -> impl Responder {
64 match state.meeting_use_cases.get_meeting(*id).await {
65 Ok(Some(meeting)) => HttpResponse::Ok().json(meeting),
66 Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
67 "error": "Meeting not found"
68 })),
69 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
70 "error": err
71 })),
72 }
73}
74
75#[get("/meetings")]
76pub async fn list_meetings(
77 state: web::Data<AppState>,
78 user: AuthenticatedUser,
79 page_request: web::Query<PageRequest>,
80) -> impl Responder {
81 let organization_id = user.organization_id;
82
83 match state
84 .meeting_use_cases
85 .list_meetings_paginated(&page_request, organization_id)
86 .await
87 {
88 Ok((meetings, total)) => {
89 let response =
90 PageResponse::new(meetings, page_request.page, page_request.per_page, total);
91 HttpResponse::Ok().json(response)
92 }
93 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
94 "error": err
95 })),
96 }
97}
98
99#[get("/buildings/{building_id}/meetings")]
100pub async fn list_meetings_by_building(
101 state: web::Data<AppState>,
102 building_id: web::Path<Uuid>,
103) -> impl Responder {
104 match state
105 .meeting_use_cases
106 .list_meetings_by_building(*building_id)
107 .await
108 {
109 Ok(meetings) => HttpResponse::Ok().json(meetings),
110 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
111 "error": err
112 })),
113 }
114}
115
116#[put("/meetings/{id}")]
117pub async fn update_meeting(
118 state: web::Data<AppState>,
119 id: web::Path<Uuid>,
120 request: web::Json<UpdateMeetingRequest>,
121) -> impl Responder {
122 match state
123 .meeting_use_cases
124 .update_meeting(*id, request.into_inner())
125 .await
126 {
127 Ok(meeting) => HttpResponse::Ok().json(meeting),
128 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
129 "error": err
130 })),
131 }
132}
133
134#[post("/meetings/{id}/agenda")]
135pub async fn add_agenda_item(
136 state: web::Data<AppState>,
137 id: web::Path<Uuid>,
138 request: web::Json<AddAgendaItemRequest>,
139) -> impl Responder {
140 match state
141 .meeting_use_cases
142 .add_agenda_item(*id, request.into_inner())
143 .await
144 {
145 Ok(meeting) => HttpResponse::Ok().json(meeting),
146 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
147 "error": err
148 })),
149 }
150}
151
152#[post("/meetings/{id}/complete")]
153pub async fn complete_meeting(
154 state: web::Data<AppState>,
155 user: AuthenticatedUser,
156 id: web::Path<Uuid>,
157 request: web::Json<CompleteMeetingRequest>,
158) -> impl Responder {
159 match state
160 .meeting_use_cases
161 .complete_meeting(*id, request.into_inner())
162 .await
163 {
164 Ok(meeting) => {
165 AuditLogEntry::new(
167 AuditEventType::MeetingCompleted,
168 Some(user.user_id),
169 user.organization_id,
170 )
171 .with_resource("Meeting", *id)
172 .log();
173
174 HttpResponse::Ok().json(meeting)
175 }
176 Err(err) => {
177 AuditLogEntry::new(
179 AuditEventType::MeetingCompleted,
180 Some(user.user_id),
181 user.organization_id,
182 )
183 .with_resource("Meeting", *id)
184 .with_error(err.clone())
185 .log();
186
187 HttpResponse::BadRequest().json(serde_json::json!({
188 "error": err
189 }))
190 }
191 }
192}
193
194#[post("/meetings/{id}/cancel")]
195pub async fn cancel_meeting(
196 state: web::Data<AppState>,
197 user: AuthenticatedUser,
198 id: web::Path<Uuid>,
199) -> impl Responder {
200 match state.meeting_use_cases.cancel_meeting(*id).await {
201 Ok(meeting) => {
202 AuditLogEntry::new(
203 AuditEventType::MeetingCompleted,
204 Some(user.user_id),
205 user.organization_id,
206 )
207 .with_resource("Meeting", *id)
208 .log();
209
210 HttpResponse::Ok().json(meeting)
211 }
212 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
213 "error": err
214 })),
215 }
216}
217
218#[post("/meetings/{id}/reschedule")]
219pub async fn reschedule_meeting(
220 state: web::Data<AppState>,
221 user: AuthenticatedUser,
222 id: web::Path<Uuid>,
223 request: web::Json<RescheduleMeetingRequest>,
224) -> impl Responder {
225 match state
226 .meeting_use_cases
227 .reschedule_meeting(*id, request.scheduled_date)
228 .await
229 {
230 Ok(meeting) => {
231 AuditLogEntry::new(
232 AuditEventType::MeetingCompleted,
233 Some(user.user_id),
234 user.organization_id,
235 )
236 .with_resource("Meeting", *id)
237 .log();
238
239 HttpResponse::Ok().json(meeting)
240 }
241 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({
242 "error": err
243 })),
244 }
245}
246
247#[delete("/meetings/{id}")]
248pub async fn delete_meeting(state: web::Data<AppState>, id: web::Path<Uuid>) -> impl Responder {
249 match state.meeting_use_cases.delete_meeting(*id).await {
250 Ok(true) => HttpResponse::NoContent().finish(),
251 Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
252 "error": "Meeting not found"
253 })),
254 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
255 "error": err
256 })),
257 }
258}
259
260#[get("/meetings/{id}/export-minutes-pdf")]
261pub async fn export_meeting_minutes_pdf(
262 state: web::Data<AppState>,
263 user: AuthenticatedUser,
264 id: web::Path<Uuid>,
265) -> impl Responder {
266 use crate::domain::services::{AttendeeInfo, MeetingMinutesExporter, ResolutionWithVotes};
267
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!({
272 "error": e.to_string()
273 }))
274 }
275 };
276
277 let meeting_id = *id;
278
279 let meeting = match state.meeting_use_cases.get_meeting(meeting_id).await {
281 Ok(Some(meeting_dto)) => meeting_dto,
282 Ok(None) => {
283 return HttpResponse::NotFound().json(serde_json::json!({
284 "error": "Meeting not found"
285 }))
286 }
287 Err(err) => {
288 return HttpResponse::InternalServerError().json(serde_json::json!({
289 "error": err
290 }))
291 }
292 };
293
294 let building = match state
296 .building_use_cases
297 .get_building(meeting.building_id)
298 .await
299 {
300 Ok(Some(building_dto)) => building_dto,
301 Ok(None) => {
302 return HttpResponse::NotFound().json(serde_json::json!({
303 "error": "Building not found"
304 }))
305 }
306 Err(err) => {
307 return HttpResponse::InternalServerError().json(serde_json::json!({
308 "error": err
309 }))
310 }
311 };
312
313 let resolutions = match state
315 .resolution_use_cases
316 .get_meeting_resolutions(meeting_id)
317 .await
318 {
319 Ok(resolutions) => resolutions,
320 Err(err) => {
321 return HttpResponse::InternalServerError().json(serde_json::json!({
322 "error": format!("Failed to get resolutions: {}", err)
323 }))
324 }
325 };
326
327 let mut attendees_map = std::collections::HashMap::new();
329 let mut resolutions_with_votes = Vec::new();
330
331 for resolution_dto in resolutions {
332 let votes_dto = match state
334 .resolution_use_cases
335 .get_resolution_votes(resolution_dto.id)
336 .await
337 {
338 Ok(votes) => votes,
339 Err(err) => {
340 return HttpResponse::InternalServerError().json(serde_json::json!({
341 "error": format!("Failed to get votes: {}", err)
342 }))
343 }
344 };
345
346 for vote_dto in &votes_dto {
348 if let std::collections::hash_map::Entry::Vacant(e) =
349 attendees_map.entry(vote_dto.owner_id)
350 {
351 if let Ok(Some(owner_dto)) =
353 state.owner_use_cases.get_owner(vote_dto.owner_id).await
354 {
355 let proxy_for_name = if let Some(proxy_id) = vote_dto.proxy_owner_id {
356 state
357 .owner_use_cases
358 .get_owner(proxy_id)
359 .await
360 .ok()
361 .flatten()
362 .map(|o| format!("{} {}", o.first_name, o.last_name))
363 } else {
364 None
365 };
366
367 let full_name = format!("{} {}", owner_dto.first_name, owner_dto.last_name);
368
369 e.insert(AttendeeInfo {
370 owner_id: vote_dto.owner_id,
371 name: full_name,
372 email: owner_dto.email.clone(),
373 voting_power: vote_dto.voting_power,
374 is_proxy: vote_dto.proxy_owner_id.is_some(),
375 proxy_for: proxy_for_name,
376 });
377 }
378 }
379 }
380
381 use crate::domain::entities::{Resolution, Vote};
383
384 let resolution_entity = Resolution {
385 id: resolution_dto.id,
386 meeting_id: resolution_dto.meeting_id,
387 title: resolution_dto.title,
388 description: resolution_dto.description,
389 resolution_type: resolution_dto.resolution_type,
390 majority_required: resolution_dto.majority_required,
391 vote_count_pour: resolution_dto.vote_count_pour,
392 vote_count_contre: resolution_dto.vote_count_contre,
393 vote_count_abstention: resolution_dto.vote_count_abstention,
394 total_voting_power_pour: resolution_dto.total_voting_power_pour,
395 total_voting_power_contre: resolution_dto.total_voting_power_contre,
396 total_voting_power_abstention: resolution_dto.total_voting_power_abstention,
397 status: resolution_dto.status,
398 voted_at: resolution_dto.voted_at,
399 created_at: resolution_dto.created_at,
400 };
401
402 let votes: Vec<Vote> = votes_dto
403 .iter()
404 .map(|v| Vote {
405 id: v.id,
406 resolution_id: v.resolution_id,
407 owner_id: v.owner_id,
408 unit_id: v.unit_id,
409 vote_choice: v.vote_choice.clone(),
410 voting_power: v.voting_power,
411 proxy_owner_id: v.proxy_owner_id,
412 voted_at: v.voted_at,
413 })
414 .collect();
415
416 resolutions_with_votes.push(ResolutionWithVotes {
417 resolution: resolution_entity,
418 votes,
419 });
420 }
421
422 let attendees: Vec<AttendeeInfo> = attendees_map.into_values().collect();
423
424 use crate::domain::entities::{Building, Meeting};
426
427 let building_org_id = match Uuid::parse_str(&building.organization_id) {
429 Ok(id) => id,
430 Err(err) => {
431 return HttpResponse::InternalServerError().json(serde_json::json!({
432 "error": format!("Invalid organization_id: {}", err)
433 }))
434 }
435 };
436
437 use chrono::DateTime;
439 let building_created_at = match DateTime::parse_from_rfc3339(&building.created_at) {
440 Ok(dt) => dt.with_timezone(&chrono::Utc),
441 Err(_) => chrono::Utc::now(),
442 };
443 let building_updated_at = match DateTime::parse_from_rfc3339(&building.updated_at) {
444 Ok(dt) => dt.with_timezone(&chrono::Utc),
445 Err(_) => chrono::Utc::now(),
446 };
447
448 let building_entity = Building {
449 id: match Uuid::parse_str(&building.id) {
450 Ok(id) => id,
451 Err(err) => {
452 return HttpResponse::InternalServerError().json(serde_json::json!({
453 "error": format!("Invalid building id: {}", err)
454 }))
455 }
456 },
457 name: building.name,
458 address: building.address,
459 city: building.city,
460 postal_code: building.postal_code,
461 country: building.country,
462 total_units: building.total_units,
463 total_tantiemes: building.total_tantiemes,
464 construction_year: building.construction_year,
465 syndic_name: None,
466 syndic_email: None,
467 syndic_phone: None,
468 syndic_address: None,
469 syndic_office_hours: None,
470 syndic_emergency_contact: None,
471 slug: None,
472 organization_id: building_org_id,
473 created_at: building_created_at,
474 updated_at: building_updated_at,
475 };
476
477 let meeting_entity = Meeting {
478 id: meeting.id,
479 organization_id,
480 building_id: meeting.building_id,
481 meeting_type: meeting.meeting_type,
482 title: meeting.title,
483 description: meeting.description,
484 scheduled_date: meeting.scheduled_date,
485 location: meeting.location,
486 status: meeting.status,
487 agenda: meeting.agenda,
488 attendees_count: meeting.attendees_count,
489 created_at: meeting.created_at,
490 updated_at: meeting.updated_at,
491 };
492
493 match MeetingMinutesExporter::export_to_pdf(
495 &building_entity,
496 &meeting_entity,
497 &attendees,
498 &resolutions_with_votes,
499 ) {
500 Ok(pdf_bytes) => {
501 AuditLogEntry::new(
503 AuditEventType::ReportGenerated,
504 Some(user.user_id),
505 Some(organization_id),
506 )
507 .with_resource("Meeting", meeting_id)
508 .with_metadata(serde_json::json!({
509 "report_type": "meeting_minutes_pdf",
510 "building_name": building_entity.name,
511 "meeting_date": meeting_entity.scheduled_date.to_rfc3339()
512 }))
513 .log();
514
515 HttpResponse::Ok()
516 .content_type("application/pdf")
517 .insert_header((
518 "Content-Disposition",
519 format!(
520 "attachment; filename=\"PV_{}_{}_{}.pdf\"",
521 building_entity.name.replace(' ', "_"),
522 meeting_entity.scheduled_date.format("%Y%m%d"),
523 meeting_entity.id
524 ),
525 ))
526 .body(pdf_bytes)
527 }
528 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
529 "error": format!("Failed to generate PDF: {}", err)
530 })),
531 }
532}