koprogo_api/application/dto/stats_dto.rs
1use chrono::{DateTime, Utc};
2use rust_decimal::Decimal;
3use serde::Serialize;
4
5#[derive(Debug, Clone, Serialize)]
6pub struct AdminDashboardStats {
7 pub total_organizations: i64,
8 pub total_users: i64,
9 pub total_buildings: i64,
10 pub active_subscriptions: i64,
11 pub total_owners: i64,
12 pub total_units: i64,
13 pub total_expenses: i64,
14 pub total_meetings: i64,
15}
16
17#[derive(Debug, Clone, Serialize)]
18pub struct SeedDataStats {
19 pub seed_organizations: i64,
20 pub production_organizations: i64,
21 pub seed_buildings: i64,
22 pub seed_units: i64,
23 pub seed_owners: i64,
24 pub seed_unit_owners: i64,
25 pub seed_expenses: i64,
26 pub seed_meetings: i64,
27 pub seed_users: i64,
28}
29
30#[derive(Debug, Clone, Serialize)]
31pub struct NextMeetingInfo {
32 pub id: String,
33 pub date: DateTime<Utc>,
34 pub building_name: String,
35}
36
37#[derive(Debug, Clone, Serialize)]
38pub struct SyndicDashboardStats {
39 pub total_buildings: i64,
40 /// Lots effectivement encodés (lignes de la table `units`).
41 pub total_units: i64,
42 /// Lots déclarés à l'acte de base, sommés sur les immeubles de
43 /// l'organisation (`buildings.total_units`).
44 ///
45 /// Les deux nombres mesurent des choses différentes et étaient affichés
46 /// sous le même libellé : le tableau de bord annonçait « 0 lots au
47 /// total » pendant que la liste des immeubles affichait « 8 Lots ». Tous
48 /// deux étaient exacts, mais l'un comptait l'encodage et l'autre la
49 /// déclaration. Les exposer ensemble lève l'ambiguïté, comme le fait
50 /// déjà la fiche immeuble avec son « 0/8 ».
51 pub declared_units: i64,
52 pub total_owners: i64,
53 pub pending_expenses_count: i64,
54 /// Total des dépenses en attente, en EUR.
55 ///
56 /// `Decimal` : la colonne `expenses.amount` est en `NUMERIC(12,2)` depuis
57 /// la migration 20260502000000. La lire en `f64` était une dégradation
58 /// gratuite d'une valeur déjà exacte (même défaut que celui trouvé dans
59 /// `find_overdue_expenses_without_reminders` sous #661).
60 ///
61 /// `serde::float` conserve la représentation JSON numérique attendue par
62 /// `OwnerDashboard.svelte` / `SyndicDashboard.svelte`, qui typent ce champ
63 /// `number` — aucun drift de contrat.
64 #[serde(with = "rust_decimal::serde::float")]
65 pub pending_expenses_amount: Decimal,
66 pub next_meeting: Option<NextMeetingInfo>,
67}
68
69/// Ce qu'un copropriétaire doit à UNE copropriété.
70///
71/// ── Pourquoi par ACP, et pas un montant global ──────────────────────────
72///
73/// Un copropriétaire peut détenir des lots dans plusieurs ACP — situation
74/// ordinaire d'un investisseur, et `unit_owners` est une relation n:n sans
75/// contrainte d'ACP unique.
76///
77/// Or **chaque ACP est une personne morale distincte, avec son propre compte
78/// bancaire** : l'Art. 3.86 § 1er lui donne la personnalité juridique, le § 3
79/// impose des comptes ouverts à son nom.
80///
81/// L'écran servait jusqu'ici un `SyndicDashboardStats` — un compte, un
82/// montant, rien qui distingue les copropriétés. Un copropriétaire qui lit
83/// « 1 262,50 € à payer » et fait un seul virement PAIE LA MAUVAISE PERSONNE
84/// MORALE pour une partie de la somme : l'argent atterrit sur le compte de
85/// l'ACP A pour des charges dues à l'ACP B. Le syndic de B devra réclamer,
86/// celui de A rembourser.
87///
88/// Ce n'est donc pas un défaut d'affichage mais un paiement mal imputé.
89/// Agréger pour informer, séparer pour agir (#867).
90#[derive(Debug, Clone, Serialize)]
91pub struct DuAupresDuneAcp {
92 pub acp_id: String,
93 pub acp_name: String,
94 /// Le numéro d'entreprise, à recopier sur le virement.
95 ///
96 /// C'est lui qui identifie la personne morale créancière — l'Art. 3.86
97 /// § 1er al. 4 impose d'ailleurs qu'il figure sur tous les documents qui
98 /// émanent de l'association.
99 pub bce_number: Option<String>,
100 pub charges_en_attente: i64,
101 /// Montant dû à CETTE association.
102 ///
103 /// `Decimal` sérialisé en flottant JSON, comme `pending_expenses_amount` :
104 /// le contrat frontend type ce champ `number`, et le changer ici créerait
105 /// une dérive silencieuse.
106 #[serde(with = "rust_decimal::serde::float")]
107 pub montant: Decimal,
108}
109
110#[derive(Debug, Clone, Serialize)]
111pub struct UrgentTask {
112 pub task_type: String,
113 pub title: String,
114 pub description: String,
115 pub priority: String,
116 pub building_name: Option<String>,
117 pub entity_id: Option<String>,
118 pub due_date: Option<DateTime<Utc>>,
119
120 // ── Le décompte d'échéance légale ─────────────────────────────────────
121 //
122 // La remise de design en fait le différenciateur du produit : à côté de
123 // chaque tâche, « 18 j sur 30 » et la référence de l'article. Aucun
124 // concurrent ne dit sur quel fondement il réclame une action.
125 //
126 // Ces deux champs viennent du REGISTRE LÉGAL, jamais d'une chaîne écrite
127 // dans un composant. La remise met en garde sur ce point précis, et sa
128 // raison est bonne : un « 30 » recopié à l'écran est un nombre que rien
129 // ne relie à la loi. Le jour où l'on corrige le domaine — parce qu'on
130 // avait mal lu l'article — l'écran continue d'annoncer l'ancien délai, et
131 // le syndic agit sur une échéance fausse en croyant lire le produit.
132 /// L'article qui fonde l'échéance, tel qu'on le cite dans un courrier.
133 /// `None` quand la tâche n'est pas d'origine légale.
134 pub article: Option<String>,
135 /// Le délai que cet article accorde, en jours. C'est le DÉNOMINATEUR du
136 /// décompte : « 18 j sur 30 » n'a de sens que si l'on sait d'où vient le
137 /// 30.
138 pub delai_legal_jours: Option<i64>,
139}