Skip to main content

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}