1use crate::infrastructure::web::{AppState, AuthenticatedUser};
2use actix_web::{get, web, HttpResponse, Responder};
3use chrono::{DateTime, Utc};
4use serde::Serialize;
5
6#[derive(Serialize)]
7pub struct DashboardStats {
8 pub total_organizations: i64,
9 pub total_users: i64,
10 pub total_buildings: i64,
11 pub active_subscriptions: i64,
12 pub total_owners: i64,
13 pub total_units: i64,
14 pub total_expenses: i64,
15 pub total_meetings: i64,
16}
17
18#[derive(Serialize)]
19pub struct SeedDataStats {
20 pub seed_organizations: i64,
21 pub production_organizations: i64,
22 pub seed_buildings: i64,
23 pub seed_units: i64,
24 pub seed_owners: i64,
25 pub seed_unit_owners: i64,
26 pub seed_expenses: i64,
27 pub seed_meetings: i64,
28 pub seed_users: i64,
29}
30
31#[derive(Serialize)]
32pub struct SyndicDashboardStats {
33 pub total_buildings: i64,
34 pub total_units: i64,
35 pub total_owners: i64,
36 pub pending_expenses_count: i64,
37 pub pending_expenses_amount: f64,
38 pub next_meeting: Option<NextMeetingInfo>,
39}
40
41#[derive(Serialize)]
42pub struct NextMeetingInfo {
43 pub id: String,
44 pub date: DateTime<Utc>,
45 pub building_name: String,
46}
47
48#[derive(Serialize)]
49pub struct UrgentTask {
50 pub task_type: String, pub title: String,
52 pub description: String,
53 pub priority: String, pub building_name: Option<String>,
55 pub entity_id: Option<String>,
56 pub due_date: Option<DateTime<Utc>>,
57}
58
59#[get("/stats/dashboard")]
61pub async fn get_dashboard_stats(
62 state: web::Data<AppState>,
63 user: AuthenticatedUser,
64) -> impl Responder {
65 if user.role != "superadmin" {
67 return HttpResponse::Forbidden().json(serde_json::json!({
68 "error": "Only SuperAdmin can access dashboard statistics"
69 }));
70 }
71
72 let orgs_result = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM organizations")
74 .fetch_one(&state.pool)
75 .await;
76
77 let users_result = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM users")
79 .fetch_one(&state.pool)
80 .await;
81
82 let buildings_result = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM buildings")
84 .fetch_one(&state.pool)
85 .await;
86
87 let active_subs_result =
89 sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM organizations WHERE is_active = true")
90 .fetch_one(&state.pool)
91 .await;
92
93 let owners_result = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM owners")
95 .fetch_one(&state.pool)
96 .await;
97
98 let units_result = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM units")
100 .fetch_one(&state.pool)
101 .await;
102
103 let expenses_result = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM expenses")
105 .fetch_one(&state.pool)
106 .await;
107
108 let meetings_result = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM meetings")
110 .fetch_one(&state.pool)
111 .await;
112
113 match (
115 orgs_result,
116 users_result,
117 buildings_result,
118 active_subs_result,
119 owners_result,
120 units_result,
121 expenses_result,
122 meetings_result,
123 ) {
124 (
125 Ok(total_organizations),
126 Ok(total_users),
127 Ok(total_buildings),
128 Ok(active_subscriptions),
129 Ok(total_owners),
130 Ok(total_units),
131 Ok(total_expenses),
132 Ok(total_meetings),
133 ) => {
134 let stats = DashboardStats {
135 total_organizations,
136 total_users,
137 total_buildings,
138 active_subscriptions,
139 total_owners,
140 total_units,
141 total_expenses,
142 total_meetings,
143 };
144 HttpResponse::Ok().json(stats)
145 }
146 _ => HttpResponse::InternalServerError().json(serde_json::json!({
147 "error": "Failed to fetch dashboard statistics"
148 })),
149 }
150}
151
152#[get("/stats/owner")]
155pub async fn get_owner_stats(
156 state: web::Data<AppState>,
157 user: AuthenticatedUser,
158) -> impl Responder {
159 if user.role != "owner" && user.role != "superadmin" {
161 return HttpResponse::Forbidden().json(serde_json::json!({
162 "error": "Only Owner can access these statistics"
163 }));
164 }
165
166 let owner_record = sqlx::query!("SELECT id FROM owners WHERE user_id = $1", user.user_id)
168 .fetch_optional(&state.pool)
169 .await;
170
171 let owner_id = match owner_record {
172 Ok(Some(record)) => record.id,
173 Ok(None) => {
174 return HttpResponse::Ok().json(SyndicDashboardStats {
176 total_buildings: 0,
177 total_units: 0,
178 total_owners: 0,
179 pending_expenses_count: 0,
180 pending_expenses_amount: 0.0,
181 next_meeting: None,
182 });
183 }
184 Err(_) => {
185 return HttpResponse::InternalServerError().json(serde_json::json!({
186 "error": "Failed to fetch owner information"
187 }))
188 }
189 };
190
191 let buildings_result = sqlx::query_scalar::<_, i64>(
193 "SELECT COUNT(DISTINCT b.id) FROM buildings b
194 INNER JOIN units u ON b.id = u.building_id
195 INNER JOIN unit_owners uo ON u.id = uo.unit_id
196 WHERE uo.owner_id = $1 AND uo.end_date IS NULL",
197 )
198 .bind(owner_id)
199 .fetch_one(&state.pool)
200 .await;
201
202 let units_result = sqlx::query_scalar::<_, i64>(
204 "SELECT COUNT(*) FROM unit_owners uo
205 WHERE uo.owner_id = $1 AND uo.end_date IS NULL",
206 )
207 .bind(owner_id)
208 .fetch_one(&state.pool)
209 .await;
210
211 let owners_result = sqlx::query_scalar::<_, i64>(
213 "SELECT COUNT(DISTINCT uo2.owner_id) FROM unit_owners uo2
214 INNER JOIN units u ON uo2.unit_id = u.id
215 WHERE u.building_id IN (
216 SELECT DISTINCT u2.building_id FROM units u2
217 INNER JOIN unit_owners uo ON u2.id = uo.unit_id
218 WHERE uo.owner_id = $1 AND uo.end_date IS NULL
219 ) AND uo2.end_date IS NULL",
220 )
221 .bind(owner_id)
222 .fetch_one(&state.pool)
223 .await;
224
225 let pending_expenses = sqlx::query!(
227 "SELECT COUNT(*) as count, COALESCE(SUM(amount), 0) as total
228 FROM expenses e
229 WHERE e.building_id IN (
230 SELECT DISTINCT u.building_id FROM units u
231 INNER JOIN unit_owners uo ON u.id = uo.unit_id
232 WHERE uo.owner_id = $1 AND uo.end_date IS NULL
233 ) AND e.payment_status = 'pending'",
234 owner_id
235 )
236 .fetch_one(&state.pool)
237 .await;
238
239 let next_meeting = sqlx::query!(
241 "SELECT m.id, m.scheduled_date, b.name as building_name
242 FROM meetings m
243 INNER JOIN buildings b ON m.building_id = b.id
244 WHERE b.id IN (
245 SELECT DISTINCT u.building_id FROM units u
246 INNER JOIN unit_owners uo ON u.id = uo.unit_id
247 WHERE uo.owner_id = $1 AND uo.end_date IS NULL
248 )
249 AND m.scheduled_date > NOW() AND m.status = 'scheduled'
250 ORDER BY m.scheduled_date ASC
251 LIMIT 1",
252 owner_id
253 )
254 .fetch_optional(&state.pool)
255 .await;
256
257 match (
258 buildings_result,
259 units_result,
260 owners_result,
261 pending_expenses,
262 next_meeting,
263 ) {
264 (
265 Ok(total_buildings),
266 Ok(total_units),
267 Ok(total_owners),
268 Ok(expenses_data),
269 Ok(meeting_data),
270 ) => {
271 let next_meeting_info = meeting_data.map(|m| NextMeetingInfo {
272 id: m.id.to_string(),
273 date: m.scheduled_date,
274 building_name: m.building_name,
275 });
276
277 let stats = SyndicDashboardStats {
278 total_buildings,
279 total_units,
280 total_owners,
281 pending_expenses_count: expenses_data.count.unwrap_or(0),
282 pending_expenses_amount: expenses_data.total.unwrap_or(0.0),
283 next_meeting: next_meeting_info,
284 };
285 HttpResponse::Ok().json(stats)
286 }
287 _ => HttpResponse::InternalServerError().json(serde_json::json!({
288 "error": "Failed to fetch owner dashboard statistics"
289 })),
290 }
291}
292
293#[get("/stats/syndic")]
295pub async fn get_syndic_stats(
296 state: web::Data<AppState>,
297 user: AuthenticatedUser,
298) -> impl Responder {
299 if user.role != "syndic" && user.role != "accountant" && user.role != "superadmin" {
301 return HttpResponse::Forbidden().json(serde_json::json!({
302 "error": "Only Syndic and Accountant can access these statistics"
303 }));
304 }
305
306 let buildings_result =
308 sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM buildings WHERE organization_id = $1")
309 .bind(user.organization_id)
310 .fetch_one(&state.pool)
311 .await;
312
313 let units_result = sqlx::query_scalar::<_, i64>(
315 "SELECT COUNT(*) FROM units u
316 INNER JOIN buildings b ON u.building_id = b.id
317 WHERE b.organization_id = $1",
318 )
319 .bind(user.organization_id)
320 .fetch_one(&state.pool)
321 .await;
322
323 let owners_result = sqlx::query_scalar::<_, i64>(
325 "SELECT COUNT(DISTINCT o.id) FROM owners o
326 INNER JOIN unit_owners uo ON o.id = uo.owner_id
327 INNER JOIN units u ON uo.unit_id = u.id
328 INNER JOIN buildings b ON u.building_id = b.id
329 WHERE b.organization_id = $1 AND uo.end_date IS NULL",
330 )
331 .bind(user.organization_id)
332 .fetch_one(&state.pool)
333 .await;
334
335 let pending_expenses = sqlx::query!(
337 "SELECT COUNT(*) as count, COALESCE(SUM(amount), 0) as total
338 FROM expenses e
339 INNER JOIN buildings b ON e.building_id = b.id
340 WHERE b.organization_id = $1 AND e.payment_status = 'pending'",
341 user.organization_id
342 )
343 .fetch_one(&state.pool)
344 .await;
345
346 let next_meeting = sqlx::query!(
348 "SELECT m.id, m.scheduled_date, b.name as building_name
349 FROM meetings m
350 INNER JOIN buildings b ON m.building_id = b.id
351 WHERE b.organization_id = $1 AND m.scheduled_date > NOW() AND m.status = 'scheduled'
352 ORDER BY m.scheduled_date ASC
353 LIMIT 1",
354 user.organization_id
355 )
356 .fetch_optional(&state.pool)
357 .await;
358
359 match (
360 buildings_result,
361 units_result,
362 owners_result,
363 pending_expenses,
364 next_meeting,
365 ) {
366 (
367 Ok(total_buildings),
368 Ok(total_units),
369 Ok(total_owners),
370 Ok(expenses_data),
371 Ok(meeting_data),
372 ) => {
373 let next_meeting_info = meeting_data.map(|m| NextMeetingInfo {
374 id: m.id.to_string(),
375 date: m.scheduled_date,
376 building_name: m.building_name,
377 });
378
379 let stats = SyndicDashboardStats {
380 total_buildings,
381 total_units,
382 total_owners,
383 pending_expenses_count: expenses_data.count.unwrap_or(0),
384 pending_expenses_amount: expenses_data.total.unwrap_or(0.0),
385 next_meeting: next_meeting_info,
386 };
387 HttpResponse::Ok().json(stats)
388 }
389 _ => HttpResponse::InternalServerError().json(serde_json::json!({
390 "error": "Failed to fetch syndic dashboard statistics"
391 })),
392 }
393}
394
395#[get("/stats/syndic/urgent-tasks")]
397pub async fn get_syndic_urgent_tasks(
398 state: web::Data<AppState>,
399 user: AuthenticatedUser,
400) -> impl Responder {
401 if user.role != "syndic" && user.role != "accountant" && user.role != "superadmin" {
403 return HttpResponse::Forbidden().json(serde_json::json!({
404 "error": "Only Syndic and Accountant can access these tasks"
405 }));
406 }
407
408 let mut urgent_tasks: Vec<UrgentTask> = Vec::new();
409
410 let overdue_expenses = sqlx::query!(
412 "SELECT e.id, e.description, e.amount, b.name as building_name, e.expense_date
413 FROM expenses e
414 INNER JOIN buildings b ON e.building_id = b.id
415 WHERE b.organization_id = $1
416 AND e.payment_status = 'overdue'
417 ORDER BY e.expense_date ASC
418 LIMIT 5",
419 user.organization_id
420 )
421 .fetch_all(&state.pool)
422 .await;
423
424 if let Ok(expenses) = overdue_expenses {
425 for expense in expenses {
426 urgent_tasks.push(UrgentTask {
427 task_type: "expense".to_string(),
428 title: format!("Charge en retard - {:.2}€", expense.amount),
429 description: expense.description,
430 priority: "urgent".to_string(),
431 building_name: Some(expense.building_name),
432 entity_id: Some(expense.id.to_string()),
433 due_date: Some(expense.expense_date),
434 });
435 }
436 }
437
438 let upcoming_meetings = sqlx::query!(
440 "SELECT m.id, m.title, m.scheduled_date, b.name as building_name
441 FROM meetings m
442 INNER JOIN buildings b ON m.building_id = b.id
443 WHERE b.organization_id = $1
444 AND m.status = 'scheduled'
445 AND m.scheduled_date BETWEEN NOW() AND NOW() + INTERVAL '7 days'
446 ORDER BY m.scheduled_date ASC
447 LIMIT 3",
448 user.organization_id
449 )
450 .fetch_all(&state.pool)
451 .await;
452
453 if let Ok(meetings) = upcoming_meetings {
454 for meeting in meetings {
455 let days_until = (meeting.scheduled_date - Utc::now()).num_days();
456 let priority = if days_until <= 3 { "urgent" } else { "high" };
457
458 urgent_tasks.push(UrgentTask {
459 task_type: "meeting".to_string(),
460 title: meeting.title,
461 description: format!("AG dans {} jours", days_until),
462 priority: priority.to_string(),
463 building_name: Some(meeting.building_name),
464 entity_id: Some(meeting.id.to_string()),
465 due_date: Some(meeting.scheduled_date),
466 });
467 }
468 }
469
470 let pending_overdue = sqlx::query!(
472 "SELECT COUNT(*) as count
473 FROM expenses e
474 INNER JOIN buildings b ON e.building_id = b.id
475 WHERE b.organization_id = $1
476 AND e.payment_status = 'pending'
477 AND e.expense_date < NOW() - INTERVAL '30 days'",
478 user.organization_id
479 )
480 .fetch_one(&state.pool)
481 .await;
482
483 if let Ok(data) = pending_overdue {
484 if let Some(count) = data.count {
485 if count > 0 {
486 urgent_tasks.push(UrgentTask {
487 task_type: "expense".to_string(),
488 title: "Relance paiements".to_string(),
489 description: format!("{} charges en attente depuis plus de 30 jours", count),
490 priority: "high".to_string(),
491 building_name: None,
492 entity_id: None,
493 due_date: None,
494 });
495 }
496 }
497 }
498
499 urgent_tasks.sort_by(|a, b| {
501 let priority_order = |p: &str| match p {
502 "urgent" => 0,
503 "high" => 1,
504 _ => 2,
505 };
506 priority_order(&a.priority).cmp(&priority_order(&b.priority))
507 });
508
509 HttpResponse::Ok().json(urgent_tasks)
510}
511
512#[get("/stats/seed-data")]
514pub async fn get_seed_data_stats(
515 state: web::Data<AppState>,
516 user: AuthenticatedUser,
517) -> impl Responder {
518 if user.role != "superadmin" {
520 return HttpResponse::Forbidden().json(serde_json::json!({
521 "error": "Only SuperAdmin can access seed data statistics"
522 }));
523 }
524
525 let seed_orgs_result = sqlx::query_scalar::<_, i64>(
527 "SELECT COUNT(*) FROM organizations WHERE is_seed_data = true",
528 )
529 .fetch_one(&state.pool)
530 .await;
531
532 let prod_orgs_result = sqlx::query_scalar::<_, i64>(
534 "SELECT COUNT(*) FROM organizations WHERE is_seed_data = false",
535 )
536 .fetch_one(&state.pool)
537 .await;
538
539 let seed_buildings_result = sqlx::query_scalar::<_, i64>(
541 "SELECT COUNT(*) FROM buildings b
542 INNER JOIN organizations o ON b.organization_id = o.id
543 WHERE o.is_seed_data = true",
544 )
545 .fetch_one(&state.pool)
546 .await;
547
548 let seed_units_result = sqlx::query_scalar::<_, i64>(
550 "SELECT COUNT(*) FROM units u
551 INNER JOIN buildings b ON u.building_id = b.id
552 INNER JOIN organizations o ON b.organization_id = o.id
553 WHERE o.is_seed_data = true",
554 )
555 .fetch_one(&state.pool)
556 .await;
557
558 let seed_owners_result = sqlx::query_scalar::<_, i64>(
560 "SELECT COUNT(DISTINCT o.id) FROM owners o
561 INNER JOIN unit_owners uo ON o.id = uo.owner_id
562 INNER JOIN units u ON uo.unit_id = u.id
563 INNER JOIN buildings b ON u.building_id = b.id
564 INNER JOIN organizations org ON b.organization_id = org.id
565 WHERE org.is_seed_data = true",
566 )
567 .fetch_one(&state.pool)
568 .await;
569
570 let seed_unit_owners_result = sqlx::query_scalar::<_, i64>(
572 "SELECT COUNT(*) FROM unit_owners uo
573 INNER JOIN units u ON uo.unit_id = u.id
574 INNER JOIN buildings b ON u.building_id = b.id
575 INNER JOIN organizations o ON b.organization_id = o.id
576 WHERE o.is_seed_data = true",
577 )
578 .fetch_one(&state.pool)
579 .await;
580
581 let seed_expenses_result = sqlx::query_scalar::<_, i64>(
583 "SELECT COUNT(*) FROM expenses e
584 INNER JOIN buildings b ON e.building_id = b.id
585 INNER JOIN organizations o ON b.organization_id = o.id
586 WHERE o.is_seed_data = true",
587 )
588 .fetch_one(&state.pool)
589 .await;
590
591 let seed_meetings_result = sqlx::query_scalar::<_, i64>(
593 "SELECT COUNT(*) FROM meetings m
594 INNER JOIN buildings b ON m.building_id = b.id
595 INNER JOIN organizations o ON b.organization_id = o.id
596 WHERE o.is_seed_data = true",
597 )
598 .fetch_one(&state.pool)
599 .await;
600
601 let seed_users_result = sqlx::query_scalar::<_, i64>(
603 "SELECT COUNT(*) FROM users u
604 INNER JOIN organizations o ON u.organization_id = o.id
605 WHERE o.is_seed_data = true",
606 )
607 .fetch_one(&state.pool)
608 .await;
609
610 match (
612 seed_orgs_result,
613 prod_orgs_result,
614 seed_buildings_result,
615 seed_units_result,
616 seed_owners_result,
617 seed_unit_owners_result,
618 seed_expenses_result,
619 seed_meetings_result,
620 seed_users_result,
621 ) {
622 (
623 Ok(seed_organizations),
624 Ok(production_organizations),
625 Ok(seed_buildings),
626 Ok(seed_units),
627 Ok(seed_owners),
628 Ok(seed_unit_owners),
629 Ok(seed_expenses),
630 Ok(seed_meetings),
631 Ok(seed_users),
632 ) => {
633 let stats = SeedDataStats {
634 seed_organizations,
635 production_organizations,
636 seed_buildings,
637 seed_units,
638 seed_owners,
639 seed_unit_owners,
640 seed_expenses,
641 seed_meetings,
642 seed_users,
643 };
644 HttpResponse::Ok().json(stats)
645 }
646 _ => HttpResponse::InternalServerError().json(serde_json::json!({
647 "error": "Failed to fetch seed data statistics"
648 })),
649 }
650}