Specs : fusion des évolutions dans les sections d'origine (design homogène)
Répartit le contenu de la section « Évolutions majeures » dans les sections natives de design.md, puis supprime cette section de synthèse, pour un document homogène (phase de dev, avant mise en prod officielle). design.md : - Data Models : entité Trajet (Distance nullable, AllerRetour, DistanceEffective, GeometrieJson), Vehicule (Denomination), TrancheBareme (KmMax nullable exclusif), nouvelles entités Compte et ResetPasswordToken ; schéma SQLite aligné. - DTOs : AllerRetour (create/update/trajet/carte), distance effective nullable, commentaire carte, dénomination véhicule, KmMax nullable, DTOs d'auth. - Endpoints : section Authentification (register/login/moi/forgot/reset), recalculer-distances, bareme/copier + annees-disponibles, code 401, retrait de jours-disponibles. - Frontend : services/guard/intercepteur d'auth, composants login/register/reset, sélecteur année/mois, liste groupée par jour repliable + drag limité au jour, carte (commentaire + A/R + tooltip borné), barème multi-tranches + recopie, décision localStorage du JWT. - Overview/contraintes/déploiement : compte authentifié, port 2202, Caddy externe, variables JWT/e-mail, migrations notables, ORS (géométrie portée par le trajet, recalcul ciblé). requirements.md : glossaire Sélecteur_Temporel corrigé (année puis mois).
This commit is contained in:
@@ -143,13 +143,15 @@ src/
|
|||||||
│ │ │ ├── notification.service.ts # Toast / snackbar
|
│ │ │ ├── notification.service.ts # Toast / snackbar
|
||||||
│ │ │ ├── adresse-cache.service.ts # Autocomplétion adresses
|
│ │ │ ├── adresse-cache.service.ts # Autocomplétion adresses
|
||||||
│ │ │ ├── adresse-nommee.service.ts # CRUD carnet d'adresses nommées
|
│ │ │ ├── adresse-nommee.service.ts # CRUD carnet d'adresses nommées
|
||||||
│ │ │ └── profil.service.ts # CRUD profil
|
│ │ │ ├── profil.service.ts # CRUD profil
|
||||||
|
│ │ │ └── auth.service.ts # Inscription, connexion, session, reset mot de passe
|
||||||
│ │ ├── models/
|
│ │ ├── models/
|
||||||
│ │ │ ├── trajet.model.ts
|
│ │ │ ├── trajet.model.ts
|
||||||
│ │ │ ├── resume-mensuel.model.ts
|
│ │ │ ├── resume-mensuel.model.ts
|
||||||
│ │ │ └── resume-annuel.model.ts
|
│ │ │ └── resume-annuel.model.ts
|
||||||
│ │ └── interceptors/
|
│ │ └── interceptors/
|
||||||
│ │ └── timeout.interceptor.ts # Timeout 10s + erreurs réseau
|
│ │ ├── timeout.interceptor.ts # Timeout 10s + erreurs réseau
|
||||||
|
│ │ └── auth.interceptor.ts # Ajoute le jeton JWT (Authorization: Bearer)
|
||||||
│ ├── features/
|
│ ├── features/
|
||||||
│ │ ├── trajet-form/
|
│ │ ├── trajet-form/
|
||||||
│ │ │ ├── trajet-form.component.ts # Saisie + édition
|
│ │ │ ├── trajet-form.component.ts # Saisie + édition
|
||||||
@@ -174,7 +176,7 @@ src/
|
|||||||
│ │ │ ├── vehicules.component.html
|
│ │ │ ├── vehicules.component.html
|
||||||
│ │ │ └── reaffectation-dialog.component.ts # Dialog de réaffectation
|
│ │ │ └── reaffectation-dialog.component.ts # Dialog de réaffectation
|
||||||
│ │ ├── navigation-temporelle/
|
│ │ ├── navigation-temporelle/
|
||||||
│ │ │ ├── selecteur-temporel.component.ts # Année / mois / jour
|
│ │ │ ├── selecteur-temporel.component.ts # Année / mois (repliable, sans jour)
|
||||||
│ │ │ └── selecteur-temporel.component.html
|
│ │ │ └── selecteur-temporel.component.html
|
||||||
│ │ ├── consultation-jour/
|
│ │ ├── consultation-jour/
|
||||||
│ │ │ ├── consultation-jour.component.ts # Layout principal
|
│ │ │ ├── consultation-jour.component.ts # Layout principal
|
||||||
@@ -194,8 +196,14 @@ src/
|
|||||||
│ │ ├── onboarding/
|
│ │ ├── onboarding/
|
||||||
│ │ │ ├── onboarding.component.ts # Saisie initiale du profil (identité + 1er véhicule)
|
│ │ │ ├── onboarding.component.ts # Saisie initiale du profil (identité + 1er véhicule)
|
||||||
│ │ │ └── onboarding.component.html
|
│ │ │ └── onboarding.component.html
|
||||||
|
│ │ ├── auth/
|
||||||
|
│ │ │ ├── login.component.ts # Connexion (email + mot de passe)
|
||||||
|
│ │ │ ├── register.component.ts # Création de l'unique compte (étape 1 de l'initialisation)
|
||||||
|
│ │ │ ├── forgot-password.component.ts # Demande de réinitialisation par email
|
||||||
|
│ │ │ └── reset-password.component.ts # Définition d'un nouveau mot de passe via jeton
|
||||||
│ ├── core/
|
│ ├── core/
|
||||||
│ │ └── guards/
|
│ │ └── guards/
|
||||||
|
│ │ ├── auth.guard.ts # Valide la session serveur (GET /api/auth/moi) avant les vues protégées
|
||||||
│ │ └── profil-configure.guard.ts # Redirige vers l'onboarding si le profil n'est pas configuré
|
│ │ └── profil-configure.guard.ts # Redirige vers l'onboarding si le profil n'est pas configuré
|
||||||
│ ├── shared/
|
│ ├── shared/
|
||||||
│ │ ├── components/
|
│ │ ├── components/
|
||||||
@@ -208,7 +216,7 @@ src/
|
|||||||
#### Composants principaux
|
#### Composants principaux
|
||||||
|
|
||||||
**`TrajetFormComponent`**
|
**`TrajetFormComponent`**
|
||||||
- Reactive Form avec les champs : `date` (string JJ/MM/AAAA), `adresseDepart` (1–255 car.), `adresseArrivee` (1–255 car.), `commentaire` (optionnel, 0–500 car.)
|
- Reactive Form avec les champs : `date` (string JJ/MM/AAAA), `adresseDepart` (1–255 car.), `adresseArrivee` (1–255 car.), `commentaire` (optionnel, 0–500 car.), `allerRetour` (case à cocher — distance comptée doublée)
|
||||||
- Validations synchrones : présence, longueur, format date via `DateJjMmAAAAValidator`, longueur commentaire
|
- Validations synchrones : présence, longueur, format date via `DateJjMmAAAAValidator`, longueur commentaire
|
||||||
- Mode création / édition : pré-remplissage en édition, recalcul distance uniquement si adresse modifiée, modification commentaire sans recalcul
|
- Mode création / édition : pré-remplissage en édition, recalcul distance uniquement si adresse modifiée, modification commentaire sans recalcul
|
||||||
- **Mode insertion** : accepte en entrée `{ adresseDepart?: string, adresseArrivee?: string }` pour pré-remplir les champs depuis le Trajet_Précédent / Trajet_Suivant ; les champs restent modifiables
|
- **Mode insertion** : accepte en entrée `{ adresseDepart?: string, adresseArrivee?: string }` pour pré-remplir les champs depuis le Trajet_Précédent / Trajet_Suivant ; les champs restent modifiables
|
||||||
@@ -228,10 +236,11 @@ src/
|
|||||||
- Bouton "Export PDF annuel"
|
- Bouton "Export PDF annuel"
|
||||||
|
|
||||||
**`TrajetListComponent`**
|
**`TrajetListComponent`**
|
||||||
- Ligne par trajet triée par `Position` croissante au sein de chaque journée : date, adresse départ, adresse arrivée, distance, commentaire (icône + tooltip si présent)
|
- Affiche TOUS les trajets du mois, regroupés par journée : chaque groupe/jour a un **en-tête repliable** (date, nombre de trajets) ; par défaut tout est replié sauf le dernier jour du mois ayant des trajets.
|
||||||
|
- Ligne par trajet (triée par `Position` croissante au sein du jour) : colonne dédiée **A/R** en début de ligne pour les allers/retours, commentaire (si présent), adresses (taille réduite), distance effective
|
||||||
- Actions par trajet : modifier (→ `TrajetFormComponent` pré-rempli en < 500 ms), supprimer (→ dialog de confirmation), **insérer après** (→ `TrajetFormComponent` en mode insertion avec `adresseDepart` = adresse d'arrivée du trajet courant, `positionApres` = Position du trajet courant)
|
- Actions par trajet : modifier (→ `TrajetFormComponent` pré-rempli en < 500 ms), supprimer (→ dialog de confirmation), **insérer après** (→ `TrajetFormComponent` en mode insertion avec `adresseDepart` = adresse d'arrivée du trajet courant, `positionApres` = Position du trajet courant)
|
||||||
- Si le trajet n'est pas le dernier de la journée : action **insérer avant le suivant** disponible, pré-remplissant les deux adresses et calculant la position d'insertion `(P_courant + P_suivant) / 2`
|
- Si le trajet n'est pas le dernier de la journée : action **insérer avant le suivant** disponible, pré-remplissant les deux adresses et calculant la position d'insertion `(P_courant + P_suivant) / 2`
|
||||||
- **Glisser-déposer** (drag & drop) pour réordonner manuellement les trajets d'une journée → appel `PATCH /api/trajets/reordonner`
|
- **Glisser-déposer** (drag & drop) limité à chaque journée : un `cdkDropList` par jour, listes **non connectées** entre elles (pas de déplacement inter-journées) → recalcul des positions du jour puis `PATCH /api/trajets/reordonner`
|
||||||
- Rafraîchissement du résumé mensuel en < 1 000 ms après modification/suppression/insertion
|
- Rafraîchissement du résumé mensuel en < 1 000 ms après modification/suppression/insertion
|
||||||
|
|
||||||
**`CarteTrajetsComponent`**
|
**`CarteTrajetsComponent`**
|
||||||
@@ -239,7 +248,7 @@ src/
|
|||||||
- Sélecteur de date (JJ/MM/AAAA)
|
- Sélecteur de date (JJ/MM/AAAA)
|
||||||
- Affiche les tracés GeoJSON retournés par le backend, un tracé par trajet avec couleur distincte
|
- Affiche les tracés GeoJSON retournés par le backend, un tracé par trajet avec couleur distincte
|
||||||
- Fallback ligne droite si géométrie indisponible (trait pointillé)
|
- Fallback ligne droite si géométrie indisponible (trait pointillé)
|
||||||
- Infobulles au clic/survol : adresse départ, adresse arrivée, distance
|
- Infobulles au clic (popup) et au survol (tooltip) : commentaire en tête (si présent), adresse départ, adresse arrivée, distance suivie de « (A/R) » pour un aller/retour ; largeur bornée avec retour à la ligne pour ne pas déborder de la carte
|
||||||
- Fond de carte : OpenStreetMap (tile layer `https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png`)
|
- Fond de carte : OpenStreetMap (tile layer `https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png`)
|
||||||
|
|
||||||
**`ProfilComponent`**
|
**`ProfilComponent`**
|
||||||
@@ -257,31 +266,32 @@ src/
|
|||||||
- Deux actions : "Réaffecter au nouveau véhicule" (POST reaffectation accepter=true) / "Conserver l'existant" (accepter=false)
|
- Deux actions : "Réaffecter au nouveau véhicule" (POST reaffectation accepter=true) / "Conserver l'existant" (accepter=false)
|
||||||
|
|
||||||
**`SelecteurTemporelComponent`**
|
**`SelecteurTemporelComponent`**
|
||||||
|
- **Repliable** ; l'en-tête affiche le mois et l'année sélectionnés (ex. « Septembre 2026 »)
|
||||||
- Sélecteur d'année : liste limitée à l'année courante + années ayant des données (endpoint `/api/annees-disponibles`)
|
- Sélecteur d'année : liste limitée à l'année courante + années ayant des données (endpoint `/api/annees-disponibles`)
|
||||||
- Sélecteur de mois : 12 mois affichés, surbrillance des mois avec données (endpoint `/api/mois-disponibles/{annee}`)
|
- Sélecteur de mois : 12 mois affichés, surbrillance des mois avec données (endpoint `/api/mois-disponibles/{annee}`)
|
||||||
- Sélecteur de jour : jours du mois affichés, surbrillance des jours avec données (endpoint `/api/jours-disponibles/{annee}/{mois}`)
|
- Pas de sélecteur de jour (la consultation se fait par mois) ; désactivation de tout mois > mois courant
|
||||||
- Désactivation de toute date > date courante (aucune sélection future)
|
- Émet un événement `moisSelectionne: { annee: number, mois: number }` à chaque changement
|
||||||
- Émet un événement `dateSelectionnee: DateOnly` à chaque changement
|
|
||||||
|
|
||||||
**`ConsultationJourComponent`** (composant conteneur / layout principal)
|
**`ConsultationJourComponent`** (composant conteneur / layout principal)
|
||||||
- Disposition en deux colonnes : zone principale (gauche/centre) + panneau latéral (droite)
|
- Deux états : un signal `mois` (`{ annee, mois }`) pilote la **liste mensuelle** ; un signal `date` (ISO) pilote la **carte** et la date par défaut d'ajout/insertion
|
||||||
- Zone principale : `TrajetListComponent` des trajets du jour sélectionné, avec scroll vertical (overflow-y auto)
|
- Colonne gauche (fixe) : `SelecteurTemporelComponent`, puis la **bande de navigation par jour** (« ‹ Jour préc. / date / Jour suiv. › »), puis `PanneauTotauxComponent`, puis `CarteTrajetsComponent`
|
||||||
- Panneau latéral toujours visible (sticky) : `PanneauTotauxComponent` en haut, `CarteTrajetsComponent` en dessous
|
- Colonne droite : bande fixe (sélecteur de Véhicule + bouton « Ajouter un trajet »), puis `TrajetListComponent` du mois avec scroll vertical
|
||||||
- Boutons "Jour précédent" / "Jour suivant" ; "Jour suivant" désactivé si le jour suivant > date courante
|
- Navigation par jour **bornée aux jours ayant des trajets** dans le mois (exception : le jour courant dans le mois courant reste navigable même sans trajet) ; elle ne recharge que la carte et les totaux du jour
|
||||||
- Sélecteur de Véhicule (par défaut : véhicule courant) ; filtre la zone des trajets, le Panneau_Totaux et la Carte sur le véhicule sélectionné
|
- Sélecteur de Véhicule (par défaut : véhicule courant) ; filtre la liste, le Panneau_Totaux et la Carte sur le véhicule sélectionné
|
||||||
- Coordonne le rafraîchissement de la liste, des totaux et de la carte (< 1 000 ms) à chaque changement de date
|
- Coordonne le rafraîchissement (< 1 000 ms) ; un mois vide affiche des totaux à 0 et un unique message « Aucun trajet pour ce mois », sans alerte d'erreur
|
||||||
|
|
||||||
**`PanneauTotauxComponent`**
|
**`PanneauTotauxComponent`**
|
||||||
- Affiche trois totaux : total du jour sélectionné, total du mois sélectionné, Total_Annuel_Partiel (cumul de l'année jusqu'au mois sélectionné inclus)
|
- Affiche trois totaux : total du jour ciblé par la navigation, total du mois sélectionné, Total_Annuel_Partiel (cumul de l'année jusqu'au mois sélectionné inclus)
|
||||||
- Reçoit en entrée la date sélectionnée et recalcule les totaux à chaque changement
|
- Reçoit en entrée la date sélectionnée et recalcule les totaux à chaque changement
|
||||||
- Tous les totaux affichés à 2 décimales
|
- Tous les totaux affichés à 2 décimales
|
||||||
- Affiche également le Montant_Previsionnel minoré à 80% calculé sur le Total_Annuel_Partiel du véhicule sélectionné
|
- Affiche également le Montant_Previsionnel minoré à 80% calculé sur le Total_Annuel_Partiel du véhicule sélectionné
|
||||||
|
|
||||||
**`BaremeComponent`**
|
**`BaremeComponent`**
|
||||||
- Sélecteur d'année
|
- Sélecteur d'année
|
||||||
- Tableau éditable des tranches : puissance fiscale, km min, km max, coefficient €/km, part fixe €
|
- Tableau éditable des tranches : puissance fiscale, km min, km max (**vide = illimité**), coefficient €/km (**3 décimales**, saisi à côté de la base), base €
|
||||||
- Ajout / modification / suppression de tranches
|
- Plusieurs tranches possibles pour une même puissance fiscale ; ajout / modification / suppression
|
||||||
- Validation du non-chevauchement des tranches pour une même puissance fiscale et année (message d'erreur si chevauchement)
|
- Bouton **« Recopier le dernier barème connu »** (recopie l'année antérieure la plus récente, confirmation si l'année cible contient déjà des tranches, inaccessible sur l'année du dernier barème)
|
||||||
|
- Validation du non-chevauchement des tranches pour une même puissance fiscale et année
|
||||||
|
|
||||||
**`MontantsReglesComponent`**
|
**`MontantsReglesComponent`**
|
||||||
- Saisie du Montant_Regle par mois pour l'année et le véhicule sélectionnés
|
- Saisie du Montant_Regle par mois pour l'année et le véhicule sélectionnés
|
||||||
@@ -310,6 +320,19 @@ src/
|
|||||||
- À l'enregistrement réussi (`PUT /api/profil` + `POST /api/vehicules`), navigue vers la vue principale de consultation (`ConsultationJourComponent`)
|
- À l'enregistrement réussi (`PUT /api/profil` + `POST /api/vehicules`), navigue vers la vue principale de consultation (`ConsultationJourComponent`)
|
||||||
- Réutilise `ProfilService.saveProfil()` et `VehiculeService.createVehicule()`
|
- Réutilise `ProfilService.saveProfil()` et `VehiculeService.createVehicule()`
|
||||||
|
|
||||||
|
**Composants d'authentification** (`LoginComponent`, `RegisterComponent`, `ForgotPasswordComponent`, `ResetPasswordComponent`)
|
||||||
|
- `RegisterComponent` : création de l'unique compte (email + mot de passe ≥ 8 car.), étape 1 de l'initialisation ; à la première ouverture sans compte, l'application y redirige directement. Après création, redirige vers la connexion.
|
||||||
|
- `LoginComponent` : connexion (email + mot de passe) → `POST /api/auth/login` ; à la réussite, stocke le JWT et navigue vers la vue protégée. Propose un lien « Mot de passe oublié ».
|
||||||
|
- `ForgotPasswordComponent` : saisie de l'email → `POST /api/auth/forgot-password` (lien envoyé par email, ou journalisé en repli).
|
||||||
|
- `ResetPasswordComponent` : nouveau mot de passe via le jeton reçu → `POST /api/auth/reset-password`.
|
||||||
|
|
||||||
|
**`AuthService` / `AuthInterceptor` / `authGuard`**
|
||||||
|
- `AuthService` gère l'inscription, la connexion, la déconnexion et le stockage du JWT dans `localStorage`.
|
||||||
|
- `AuthInterceptor` ajoute l'en-tête `Authorization: Bearer <jwt>` à chaque appel HTTP protégé.
|
||||||
|
- `authGuard` valide la session **côté serveur** via `GET /api/auth/moi` avant d'afficher les vues protégées : un jeton résiduel dont le compte n'existe plus est ainsi rejeté.
|
||||||
|
|
||||||
|
> **Décision — stockage du JWT dans `localStorage`** : retenu pour sa simplicité, malgré le risque XSS, acceptable pour une application personnelle mono-utilisateur. Le cookie HttpOnly a été rejeté pour éviter la complexité de protection CSRF.
|
||||||
|
|
||||||
**`ProfilConfigureGuard`** (garde de route Angular — `CanActivate` / `CanActivateFn`)
|
**`ProfilConfigureGuard`** (garde de route Angular — `CanActivate` / `CanActivateFn`)
|
||||||
- Au chargement de l'application, appelle `ProfilService.getEtat()` (`GET /api/profil/etat`)
|
- Au chargement de l'application, appelle `ProfilService.getEtat()` (`GET /api/profil/etat`)
|
||||||
- Si `EstConfigure` est `false`, redirige vers la route d'onboarding (`/onboarding`) et bloque l'accès à la saisie / consultation des trajets tant que le profil n'est pas configuré
|
- Si `EstConfigure` est `false`, redirige vers la route d'onboarding (`/onboarding`) et bloque l'accès à la saisie / consultation des trajets tant que le profil n'est pas configuré
|
||||||
@@ -480,14 +503,41 @@ public class Trajet
|
|||||||
[Required, MaxLength(255)]
|
[Required, MaxLength(255)]
|
||||||
public string AdresseArrivee { get; set; } = string.Empty;
|
public string AdresseArrivee { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>Distance en kilomètres, arrondie à 2 décimales. Toujours > 0.</summary>
|
/// <summary>
|
||||||
[Required]
|
/// Distance SIMPLE en kilomètres (aller), arrondie à 2 décimales, strictement positive
|
||||||
public decimal Distance { get; set; }
|
/// lorsqu'elle est connue. <c>null</c> lorsque la distance n'a pas pu être calculée (aucune
|
||||||
|
/// coordonnée résoluble) : le Trajet est alors persisté avec une Distance nulle et un
|
||||||
|
/// avertissement (Exigence 2.4). Une modification ultérieure peut la recalculer (Exigence 5.10).
|
||||||
|
/// </summary>
|
||||||
|
public decimal? Distance { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trajet aller/retour (Exigence 18). Lorsqu'il vaut <c>true</c>, la distance COMPTÉE est le
|
||||||
|
/// double de la distance simple, sans créer de ligne supplémentaire. Défaut <c>false</c>.
|
||||||
|
/// </summary>
|
||||||
|
public bool AllerRetour { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Distance COMPTÉE (effective), doublée si <see cref="AllerRetour"/>. Propriété calculée
|
||||||
|
/// NON MAPPÉE (non persistée), dérivée de <see cref="Distance"/> et <see cref="AllerRetour"/> ;
|
||||||
|
/// <c>null</c> si la distance simple est inconnue. C'est cette valeur qui alimente tous les
|
||||||
|
/// agrégats (totaux, résumés, barème, PDF) et l'affichage.
|
||||||
|
/// </summary>
|
||||||
|
[NotMapped]
|
||||||
|
public decimal? DistanceEffective => Distance.HasValue ? Distance.Value * (AllerRetour ? 2m : 1m) : null;
|
||||||
|
|
||||||
/// <summary>Commentaire optionnel (0–500 caractères).</summary>
|
/// <summary>Commentaire optionnel (0–500 caractères).</summary>
|
||||||
[MaxLength(500)]
|
[MaxLength(500)]
|
||||||
public string? Commentaire { get; set; }
|
public string? Commentaire { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Géométrie GeoJSON (LineString) de l'itinéraire routier départ→arrivée renvoyée par ORS,
|
||||||
|
/// pour le tracé exact sur la carte. <c>null</c> si l'itinéraire routier n'a pu être obtenu
|
||||||
|
/// (fallback ligne droite). Portée par le Trajet (dépend de la paire départ→arrivée) et non
|
||||||
|
/// par le Cache_Adresses.
|
||||||
|
/// </summary>
|
||||||
|
public string? GeometrieJson { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rang du trajet dans la liste de la journée. Valeur décimale (ex. 1.0, 1.5, 2.0)
|
/// Rang du trajet dans la liste de la journée. Valeur décimale (ex. 1.0, 1.5, 2.0)
|
||||||
/// permettant l'insertion sans réordonnancement systématique.
|
/// permettant l'insertion sans réordonnancement systématique.
|
||||||
@@ -520,7 +570,9 @@ CREATE TABLE Trajets (
|
|||||||
Date TEXT NOT NULL,
|
Date TEXT NOT NULL,
|
||||||
AdresseDepart TEXT NOT NULL,
|
AdresseDepart TEXT NOT NULL,
|
||||||
AdresseArrivee TEXT NOT NULL,
|
AdresseArrivee TEXT NOT NULL,
|
||||||
Distance REAL NOT NULL CHECK (Distance > 0),
|
Distance REAL NULL, -- distance SIMPLE ; NULL si non calculable
|
||||||
|
AllerRetour INTEGER NOT NULL DEFAULT 0, -- booléen ; distance comptée doublée si 1
|
||||||
|
GeometrieJson TEXT, -- GeoJSON LineString de l'itinéraire, ou NULL
|
||||||
Commentaire TEXT,
|
Commentaire TEXT,
|
||||||
Position REAL NOT NULL DEFAULT 0,
|
Position REAL NOT NULL DEFAULT 0,
|
||||||
VehiculeId INTEGER NOT NULL REFERENCES Vehicules(Id),
|
VehiculeId INTEGER NOT NULL REFERENCES Vehicules(Id),
|
||||||
@@ -620,11 +672,59 @@ public class Profil
|
|||||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compte unique de l'application mono-utilisateur (singleton, comme le Profil).
|
||||||
|
/// Protège l'accès aux données ; authentification par email + mot de passe (Exigence 17).
|
||||||
|
/// </summary>
|
||||||
|
public class Compte
|
||||||
|
{
|
||||||
|
/// <summary>Identifiant du compte. Valeur fixe (1) : le compte est un singleton.</summary>
|
||||||
|
public int Id { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>Adresse email, unique, servant d'identifiant de connexion (1–255 caractères).</summary>
|
||||||
|
[Required, MaxLength(255)]
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Hachage salé du mot de passe (PBKDF2 via PasswordHasher). Jamais exposé ni journalisé.</summary>
|
||||||
|
[Required]
|
||||||
|
public string MotDePasseHash { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
/// <summary>Mise à jour notamment lors d'une réinitialisation du mot de passe.</summary>
|
||||||
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Jeton de réinitialisation de mot de passe à usage unique et à durée de vie limitée (Exigence 17.11–17.14).
|
||||||
|
/// Seul le hachage du jeton est persisté ; le jeton en clair n'existe que dans l'email envoyé.
|
||||||
|
/// </summary>
|
||||||
|
public class ResetPasswordToken
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Hachage SHA-256 du jeton (le jeton en clair n'est jamais persisté).</summary>
|
||||||
|
[Required]
|
||||||
|
public string TokenHash { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Date d'expiration (UTC), par défaut création + 60 min (configurable).</summary>
|
||||||
|
public DateTime Expiration { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Date d'utilisation (UTC) ; null tant qu'inutilisé (garantit l'usage unique).</summary>
|
||||||
|
public DateTime? UsedAt { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Véhicule de l'utilisateur avec date d'effet. Historique conservé.</summary>
|
/// <summary>Véhicule de l'utilisateur avec date d'effet. Historique conservé.</summary>
|
||||||
public class Vehicule
|
public class Vehicule
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Dénomination libre du véhicule (ex. « Marque, Modèle »), obligatoire (1–100 caractères).</summary>
|
||||||
|
[Required, MaxLength(100)]
|
||||||
|
public string Denomination { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>Plaque d'immatriculation (1–15 caractères).</summary>
|
/// <summary>Plaque d'immatriculation (1–15 caractères).</summary>
|
||||||
[Required, MaxLength(15)]
|
[Required, MaxLength(15)]
|
||||||
public string Plaque { get; set; } = string.Empty;
|
public string Plaque { get; set; } = string.Empty;
|
||||||
@@ -643,8 +743,9 @@ public class Vehicule
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tranche du barème kilométrique URSSAF pour une année et une puissance fiscale.
|
/// Tranche du barème kilométrique URSSAF pour une année et une puissance fiscale. Une puissance
|
||||||
/// Trois tranches par (année, puissance fiscale) : ≤5000 km, 5001–20000 km, >20000 km.
|
/// fiscale peut comporter PLUSIEURS tranches paramétrables (ex. 0–4999, 5000–19999, 20000+),
|
||||||
|
/// la dernière pouvant être illimitée. Les bornes sont exclusives en haut : [KmMin, KmMax[.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TrancheBareme
|
public class TrancheBareme
|
||||||
{
|
{
|
||||||
@@ -660,15 +761,17 @@ public class TrancheBareme
|
|||||||
[Required]
|
[Required]
|
||||||
public decimal KmMin { get; set; }
|
public decimal KmMin { get; set; }
|
||||||
|
|
||||||
/// <summary>Borne maximale de kilométrage annuel (incluse) ; valeur élevée pour la 3e tranche.</summary>
|
/// <summary>
|
||||||
[Required]
|
/// Borne maximale de kilométrage annuel, EXCLUSIVE : la tranche couvre [KmMin, KmMax[.
|
||||||
public decimal KmMax { get; set; }
|
/// <c>null</c> signifie « illimité » (dernière tranche, sans borne supérieure).
|
||||||
|
/// </summary>
|
||||||
|
public decimal? KmMax { get; set; }
|
||||||
|
|
||||||
/// <summary>Coefficient en euros par kilomètre.</summary>
|
/// <summary>Coefficient en euros par kilomètre (3 décimales).</summary>
|
||||||
[Required]
|
[Required]
|
||||||
public decimal Coefficient { get; set; }
|
public decimal Coefficient { get; set; }
|
||||||
|
|
||||||
/// <summary>Base fixe en euros ajoutée au montant (nulle pour les tranches 1 et 3). Formule : km × Coefficient + Base.</summary>
|
/// <summary>Base fixe en euros ajoutée au montant (souvent nulle). Formule : km × Coefficient + Base.</summary>
|
||||||
public decimal Base { get; set; } = 0m;
|
public decimal Base { get; set; } = 0m;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -733,8 +836,26 @@ CREATE TABLE Profil (
|
|||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX UX_Profil_Singleton ON Profil (Id);
|
CREATE UNIQUE INDEX UX_Profil_Singleton ON Profil (Id);
|
||||||
|
|
||||||
|
CREATE TABLE Comptes (
|
||||||
|
Id INTEGER PRIMARY KEY DEFAULT 1, -- singleton (un seul compte)
|
||||||
|
Email TEXT NOT NULL UNIQUE, -- identifiant de connexion, 1–255 car.
|
||||||
|
MotDePasseHash TEXT NOT NULL, -- hachage salé (PBKDF2), jamais en clair
|
||||||
|
CreatedAt TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
UpdatedAt TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX UX_Compte_Singleton ON Comptes (Id);
|
||||||
|
|
||||||
|
CREATE TABLE ResetPasswordTokens (
|
||||||
|
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
TokenHash TEXT NOT NULL, -- SHA-256 du jeton (jeton en clair jamais persisté)
|
||||||
|
Expiration TEXT NOT NULL, -- UTC ; défaut création + 60 min
|
||||||
|
UsedAt TEXT, -- NULL tant qu'inutilisé (usage unique)
|
||||||
|
CreatedAt TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE Vehicules (
|
CREATE TABLE Vehicules (
|
||||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
Denomination TEXT NOT NULL, -- libellé libre (ex. « Marque, Modèle »), 1–100 car.
|
||||||
Plaque TEXT NOT NULL,
|
Plaque TEXT NOT NULL,
|
||||||
PuissanceFiscale INTEGER NOT NULL CHECK (PuissanceFiscale > 0),
|
PuissanceFiscale INTEGER NOT NULL CHECK (PuissanceFiscale > 0),
|
||||||
DateEffet TEXT NOT NULL, -- YYYY-MM-DD, <= date du jour
|
DateEffet TEXT NOT NULL, -- YYYY-MM-DD, <= date du jour
|
||||||
@@ -750,12 +871,13 @@ CREATE TABLE TranchesBareme (
|
|||||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
Annee INTEGER NOT NULL,
|
Annee INTEGER NOT NULL,
|
||||||
PuissanceFiscale INTEGER NOT NULL,
|
PuissanceFiscale INTEGER NOT NULL,
|
||||||
KmMin REAL NOT NULL, -- 0, 5000.01, 20000.01
|
KmMin REAL NOT NULL, -- borne minimale incluse (ex. 0, 5000, 20000)
|
||||||
KmMax REAL NOT NULL, -- 5000, 20000, valeur élevée
|
KmMax REAL NULL, -- borne maximale EXCLUSIVE ; NULL = illimité (dernière tranche)
|
||||||
Coefficient REAL NOT NULL,
|
Coefficient REAL NOT NULL, -- €/km, 3 décimales
|
||||||
Base REAL NOT NULL DEFAULT 0,
|
Base REAL NOT NULL DEFAULT 0,
|
||||||
CHECK (KmMin <= KmMax)
|
CHECK (KmMax IS NULL OR KmMin < KmMax)
|
||||||
);
|
);
|
||||||
|
-- Plusieurs tranches par (Annee, PuissanceFiscale) : bornes exclusives [KmMin, KmMax[.
|
||||||
CREATE INDEX IX_TranchesBareme_Annee_PF ON TranchesBareme (Annee, PuissanceFiscale);
|
CREATE INDEX IX_TranchesBareme_Annee_PF ON TranchesBareme (Annee, PuissanceFiscale);
|
||||||
|
|
||||||
CREATE TABLE MontantsRegles (
|
CREATE TABLE MontantsRegles (
|
||||||
@@ -769,7 +891,7 @@ CREATE TABLE MontantsRegles (
|
|||||||
CREATE UNIQUE INDEX UX_MontantsRegles_Periode_Vehicule ON MontantsRegles (Annee, Mois, VehiculeId);
|
CREATE UNIQUE INDEX UX_MontantsRegles_Periode_Vehicule ON MontantsRegles (Annee, Mois, VehiculeId);
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Calcul du barème (URSSAF)** : pour un total kilométrique annuel, une année et une puissance fiscale donnés, on sélectionne la tranche dont [KmMin, KmMax] contient le total annuel, puis `Montant_Reel = total_km_annuel × Coefficient + Base`. La tranche est déterminée par le **total annuel** et non par les kilomètres d'un mois isolé. Le **frais réel mensuel** d'un mois = `km_du_mois × Coefficient de la tranche annuelle` ; la somme des douze frais réels mensuels + la Base de la tranche = Montant_Reel. Le **Montant_Previsionnel** (80% du barème sur le total partiel) est purement indicatif et n'entre pas dans le calcul de la régularisation.
|
> **Calcul du barème (URSSAF)** : pour un total kilométrique annuel, une année et une puissance fiscale donnés, on sélectionne la tranche dont l'intervalle `[KmMin, KmMax[` (KmMax exclusive, ou illimité si `KmMax` est NULL) contient le total annuel, puis `Montant_Reel = total_km_annuel × Coefficient + Base`. Le nombre de tranches par puissance fiscale est libre (paramétrable). La tranche est déterminée par le **total annuel** et non par les kilomètres d'un mois isolé. Le **frais réel mensuel** d'un mois = `km_du_mois × Coefficient de la tranche annuelle` ; la somme des douze frais réels mensuels + la Base de la tranche = Montant_Reel. Le **Montant_Previsionnel** (80% du barème sur le total partiel) est purement indicatif et n'entre pas dans le calcul de la régularisation.
|
||||||
|
|
||||||
### DTOs
|
### DTOs
|
||||||
|
|
||||||
@@ -784,7 +906,8 @@ public record CreateTrajetDto(
|
|||||||
double? ArriveeLatitude, // null si adresse saisie manuellement sans BAN
|
double? ArriveeLatitude, // null si adresse saisie manuellement sans BAN
|
||||||
double? ArriveeLongitude,
|
double? ArriveeLongitude,
|
||||||
string? Commentaire, // optionnel, max 500 car.
|
string? Commentaire, // optionnel, max 500 car.
|
||||||
int? VehiculeId // optionnel ; null → véhicule courant à la date du trajet
|
int? VehiculeId, // optionnel ; null → véhicule courant à la date du trajet
|
||||||
|
bool AllerRetour = false // true → distance comptée doublée (aller/retour)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mise à jour
|
// Mise à jour
|
||||||
@@ -797,7 +920,8 @@ public record UpdateTrajetDto(
|
|||||||
double? ArriveeLatitude,
|
double? ArriveeLatitude,
|
||||||
double? ArriveeLongitude,
|
double? ArriveeLongitude,
|
||||||
string? Commentaire,
|
string? Commentaire,
|
||||||
int? VehiculeId // optionnel ; null → véhicule courant à la date du trajet
|
int? VehiculeId, // optionnel ; null → véhicule courant à la date du trajet
|
||||||
|
bool AllerRetour = false // true → distance comptée doublée (aller/retour)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Résultat de recherche BAN (proxied via backend si nécessaire)
|
// Résultat de recherche BAN (proxied via backend si nécessaire)
|
||||||
@@ -809,17 +933,18 @@ public record BANAdresseDto(
|
|||||||
string City
|
string City
|
||||||
);
|
);
|
||||||
|
|
||||||
// Réponse trajet
|
// Réponse trajet — Distance = distance EFFECTIVE (doublée si aller/retour), null si non calculée
|
||||||
public record TrajetDto(
|
public record TrajetDto(
|
||||||
int Id,
|
int Id,
|
||||||
DateOnly Date,
|
DateOnly Date,
|
||||||
string AdresseDepart,
|
string AdresseDepart,
|
||||||
string AdresseArrivee,
|
string AdresseArrivee,
|
||||||
decimal Distance, // km, 2 décimales
|
decimal? Distance, // distance COMPTÉE (effective), km 2 décimales ; null si non calculée
|
||||||
string? Commentaire,
|
string? Commentaire,
|
||||||
decimal Position, // rang dans la journée
|
decimal Position, // rang dans la journée
|
||||||
int VehiculeId,
|
int VehiculeId,
|
||||||
int PuissanceFiscale
|
int PuissanceFiscale,
|
||||||
|
bool AllerRetour // information aller/retour conservée par trajet
|
||||||
);
|
);
|
||||||
|
|
||||||
// Réordonnancement manuel des trajets d'une journée
|
// Réordonnancement manuel des trajets d'une journée
|
||||||
@@ -857,12 +982,14 @@ public record TrajetCarteDto(
|
|||||||
DateOnly Date,
|
DateOnly Date,
|
||||||
string AdresseDepart,
|
string AdresseDepart,
|
||||||
string AdresseArrivee,
|
string AdresseArrivee,
|
||||||
decimal Distance,
|
decimal Distance, // distance EFFECTIVE (doublée si aller/retour)
|
||||||
double? DepartLatitude,
|
double? DepartLatitude,
|
||||||
double? DepartLongitude,
|
double? DepartLongitude,
|
||||||
double? ArriveeLatitude,
|
double? ArriveeLatitude,
|
||||||
double? ArriveeLongitude,
|
double? ArriveeLongitude,
|
||||||
string? GeometrieJson // GeoJSON LineString ou null
|
string? GeometrieJson, // GeoJSON LineString ou null
|
||||||
|
string? Commentaire = null, // affiché en tête de l'infobulle carte
|
||||||
|
bool AllerRetour = false // mention « (A/R) » après la distance dans l'infobulle
|
||||||
);
|
);
|
||||||
|
|
||||||
// Suggestion autocomplétion
|
// Suggestion autocomplétion
|
||||||
@@ -913,12 +1040,14 @@ public record EtatProfilDto(
|
|||||||
// Véhicules
|
// Véhicules
|
||||||
public record VehiculeDto(
|
public record VehiculeDto(
|
||||||
int Id,
|
int Id,
|
||||||
|
string Denomination, // libellé libre (ex. « Marque, Modèle »)
|
||||||
string Plaque,
|
string Plaque,
|
||||||
int PuissanceFiscale,
|
int PuissanceFiscale,
|
||||||
DateOnly DateEffet
|
DateOnly DateEffet
|
||||||
);
|
);
|
||||||
|
|
||||||
public record CreateVehiculeDto(
|
public record CreateVehiculeDto(
|
||||||
|
string Denomination, // 1 à 100 car.
|
||||||
string Plaque,
|
string Plaque,
|
||||||
int PuissanceFiscale,
|
int PuissanceFiscale,
|
||||||
string DateEffet // JJ/MM/AAAA, <= date du jour
|
string DateEffet // JJ/MM/AAAA, <= date du jour
|
||||||
@@ -952,8 +1081,8 @@ public record TrancheBaremeDto(
|
|||||||
int Annee,
|
int Annee,
|
||||||
int PuissanceFiscale,
|
int PuissanceFiscale,
|
||||||
decimal KmMin,
|
decimal KmMin,
|
||||||
decimal KmMax,
|
decimal? KmMax, // borne exclusive ; null = illimité (dernière tranche)
|
||||||
decimal Coefficient,
|
decimal Coefficient, // €/km, 3 décimales
|
||||||
decimal Base
|
decimal Base
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -961,7 +1090,7 @@ public record SaveTrancheBaremeDto(
|
|||||||
int Annee,
|
int Annee,
|
||||||
int PuissanceFiscale,
|
int PuissanceFiscale,
|
||||||
decimal KmMin,
|
decimal KmMin,
|
||||||
decimal KmMax,
|
decimal? KmMax, // borne exclusive ; null = illimité
|
||||||
decimal Coefficient,
|
decimal Coefficient,
|
||||||
decimal Base
|
decimal Base
|
||||||
);
|
);
|
||||||
@@ -1007,6 +1136,37 @@ public record RegularisationAnnuelleDto(
|
|||||||
decimal MontantReel, // barème plein : total_km × coef + base
|
decimal MontantReel, // barème plein : total_km × coef + base
|
||||||
decimal Regularisation // MontantReel − TotalPaye
|
decimal Regularisation // MontantReel − TotalPaye
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// --- Authentification (Exigence 17) ---
|
||||||
|
|
||||||
|
// Inscription : création de l'unique compte
|
||||||
|
public record RegisterDto(
|
||||||
|
string Email, // identifiant de connexion, 1–255 car.
|
||||||
|
string MotDePasse // >= 8 caractères
|
||||||
|
);
|
||||||
|
|
||||||
|
// Connexion
|
||||||
|
public record LoginDto(
|
||||||
|
string Email,
|
||||||
|
string MotDePasse
|
||||||
|
);
|
||||||
|
|
||||||
|
// Réponse de connexion : jeton JWT + expiration
|
||||||
|
public record LoginResultDto(
|
||||||
|
string Token, // JWT signé HS256
|
||||||
|
DateTime Expiration // date d'expiration (UTC)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Demande de réinitialisation de mot de passe
|
||||||
|
public record ForgotPasswordDto(
|
||||||
|
string Email
|
||||||
|
);
|
||||||
|
|
||||||
|
// Réinitialisation effective à partir du jeton reçu par email
|
||||||
|
public record ResetPasswordDto(
|
||||||
|
string Token, // jeton en clair reçu par email
|
||||||
|
string MotDePasse // nouveau mot de passe, >= 8 caractères
|
||||||
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1024,6 +1184,20 @@ public record RegularisationAnnuelleDto(
|
|||||||
| `PUT` | `/api/trajets/{id}` | Modifier un trajet | `UpdateTrajetDto` | `200 OK` + `TrajetDto` |
|
| `PUT` | `/api/trajets/{id}` | Modifier un trajet | `UpdateTrajetDto` | `200 OK` + `TrajetDto` |
|
||||||
| `DELETE` | `/api/trajets/{id}` | Supprimer un trajet | — | `204 No Content` |
|
| `DELETE` | `/api/trajets/{id}` | Supprimer un trajet | — | `204 No Content` |
|
||||||
| `PATCH` | `/api/trajets/reordonner` | Mettre à jour l'ordre des trajets d'une journée | `ReordonnerTrajetsDto` | `204 No Content` |
|
| `PATCH` | `/api/trajets/reordonner` | Mettre à jour l'ordre des trajets d'une journée | `ReordonnerTrajetsDto` | `204 No Content` |
|
||||||
|
| `POST` | `/api/trajets/recalculer-distances` | Recalculer via ORS les trajets sans géométrie routière (limite les appels ORS aux trajets en mode dégradé) | — | `200 OK` + nombre recalculé |
|
||||||
|
|
||||||
|
#### Authentification (Exigence 17)
|
||||||
|
|
||||||
|
> Ces endpoints sont **anonymes** (`[AllowAnonymous]`). Tous les autres endpoints exigent un JWT valide et renvoient `401 Unauthorized` sans jeton (ou si le compte associé n'existe plus).
|
||||||
|
|
||||||
|
| Méthode | Route | Description | Corps | Réponse succès |
|
||||||
|
|---------|-------|-------------|-------|----------------|
|
||||||
|
| `GET` | `/api/auth/compte-existe` | Indique si l'unique compte existe déjà (oriente inscription vs connexion) | — | `200 OK` + `{ existe: bool }` |
|
||||||
|
| `POST` | `/api/auth/register` | Créer l'unique compte | `RegisterDto` | `200 OK` (compte créé ; `400/409` si déjà existant) |
|
||||||
|
| `POST` | `/api/auth/login` | Se connecter | `LoginDto` | `200 OK` + `LoginResultDto` ; `401` si identifiants invalides |
|
||||||
|
| `GET` | `/api/auth/moi` | Valider la session courante (jeton valide + compte existant) — endpoint **protégé** | — | `200 OK` ; `401` sinon |
|
||||||
|
| `POST` | `/api/auth/forgot-password` | Demander un lien de réinitialisation (envoi email, sinon lien journalisé) | `ForgotPasswordDto` | `200 OK` |
|
||||||
|
| `POST` | `/api/auth/reset-password` | Réinitialiser le mot de passe via le jeton reçu | `ResetPasswordDto` | `200 OK` ; `400` si jeton invalide/expiré/déjà utilisé |
|
||||||
|
|
||||||
#### Résumés
|
#### Résumés
|
||||||
|
|
||||||
@@ -1083,7 +1257,6 @@ public record RegularisationAnnuelleDto(
|
|||||||
|---------|-------|-------------|----------------|
|
|---------|-------|-------------|----------------|
|
||||||
| `GET` | `/api/annees-disponibles` | Années ayant des données + année courante | `200 OK` + `number[]` |
|
| `GET` | `/api/annees-disponibles` | Années ayant des données + année courante | `200 OK` + `number[]` |
|
||||||
| `GET` | `/api/mois-disponibles/{annee}` | Mois de l'année ayant des données | `200 OK` + `number[]` (1–12) |
|
| `GET` | `/api/mois-disponibles/{annee}` | Mois de l'année ayant des données | `200 OK` + `number[]` (1–12) |
|
||||||
| `GET` | `/api/jours-disponibles/{annee}/{mois}` | Jours du mois ayant des données | `200 OK` + `number[]` |
|
|
||||||
| `GET` | `/api/totaux/{date}?vehiculeId={id}` | Totaux jour/mois/année partiel du véhicule (courant si omis) | `200 OK` + `TotauxDto` |
|
| `GET` | `/api/totaux/{date}?vehiculeId={id}` | Totaux jour/mois/année partiel du véhicule (courant si omis) | `200 OK` + `TotauxDto` |
|
||||||
|
|
||||||
#### Barème et remboursements
|
#### Barème et remboursements
|
||||||
@@ -1094,6 +1267,8 @@ public record RegularisationAnnuelleDto(
|
|||||||
| `POST` | `/api/bareme` | Ajouter une tranche | `SaveTrancheBaremeDto` | `201 Created` + `TrancheBaremeDto` |
|
| `POST` | `/api/bareme` | Ajouter une tranche | `SaveTrancheBaremeDto` | `201 Created` + `TrancheBaremeDto` |
|
||||||
| `PUT` | `/api/bareme/{id}` | Modifier une tranche | `SaveTrancheBaremeDto` | `200 OK` + `TrancheBaremeDto` |
|
| `PUT` | `/api/bareme/{id}` | Modifier une tranche | `SaveTrancheBaremeDto` | `200 OK` + `TrancheBaremeDto` |
|
||||||
| `DELETE` | `/api/bareme/{id}` | Supprimer une tranche | — | `204 No Content` |
|
| `DELETE` | `/api/bareme/{id}` | Supprimer une tranche | — | `204 No Content` |
|
||||||
|
| `GET` | `/api/bareme/annees-disponibles` | Années ayant un barème défini | — | `200 OK` + `number[]` |
|
||||||
|
| `POST` | `/api/bareme/copier/{anneeCible}` | Recopier le dernier barème antérieur vers l'année cible (annule et remplace) | — | `200 OK` + nombre de tranches copiées |
|
||||||
| `GET` | `/api/montant-previsionnel/{annee}/{mois}?vehiculeId={id}` | Montant prévisionnel minoré 80% | — | `200 OK` + `MontantPrevisionnelDto` |
|
| `GET` | `/api/montant-previsionnel/{annee}/{mois}?vehiculeId={id}` | Montant prévisionnel minoré 80% | — | `200 OK` + `MontantPrevisionnelDto` |
|
||||||
| `GET` | `/api/montants-regles/{annee}?vehiculeId={id}` | Montants réglés de l'année | — | `200 OK` + `MontantRegleDto[]` |
|
| `GET` | `/api/montants-regles/{annee}?vehiculeId={id}` | Montants réglés de l'année | — | `200 OK` + `MontantRegleDto[]` |
|
||||||
| `PUT` | `/api/montants-regles` | Enregistrer/mettre à jour un montant réglé | `MontantRegleDto` | `200 OK` + `MontantRegleDto` |
|
| `PUT` | `/api/montants-regles` | Enregistrer/mettre à jour un montant réglé | `MontantRegleDto` | `200 OK` + `MontantRegleDto` |
|
||||||
@@ -1104,6 +1279,7 @@ public record RegularisationAnnuelleDto(
|
|||||||
| Code HTTP | Cas |
|
| Code HTTP | Cas |
|
||||||
|-----------|-----|
|
|-----------|-----|
|
||||||
| `400 Bad Request` | Validation échouée (champ manquant, date invalide, paramètre hors plage) |
|
| `400 Bad Request` | Validation échouée (champ manquant, date invalide, paramètre hors plage) |
|
||||||
|
| `401 Unauthorized` | Jeton JWT absent, invalide, expiré, ou compte associé inexistant |
|
||||||
| `404 Not Found` | Trajet inexistant |
|
| `404 Not Found` | Trajet inexistant |
|
||||||
| `503 Service Unavailable` | ORS indisponible ou timeout |
|
| `503 Service Unavailable` | ORS indisponible ou timeout |
|
||||||
| `500 Internal Server Error` | Erreur inattendue |
|
| `500 Internal Server Error` | Erreur inattendue |
|
||||||
@@ -1172,6 +1348,10 @@ sequenceDiagram
|
|||||||
|
|
||||||
ORS est appelé **uniquement** avec des coordonnées déjà résolues. Plus aucun appel de géocodage ORS.
|
ORS est appelé **uniquement** avec des coordonnées déjà résolues. Plus aucun appel de géocodage ORS.
|
||||||
|
|
||||||
|
> **Géométrie portée par le Trajet** : la géométrie GeoJSON de l'itinéraire dépend de la paire départ→arrivée ; elle est donc stockée sur le `Trajet` (`GeometrieJson`) et non dans le Cache_Adresses (indexé par adresse seule). Le cache sert de repli pour les trajets hérités sans géométrie.
|
||||||
|
|
||||||
|
> **Recalcul ciblé des distances** : l'endpoint `POST /api/trajets/recalculer-distances` ne (re)sollicite ORS que pour les trajets **sans géométrie routière** (`GeometrieJson` NULL), afin de corriger les distances calculées en mode dégradé (ex. clé ORS invalide à la saisie) sans re-solliciter inutilement ORS pour les trajets déjà tracés.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
participant SVC as DistanceCalculatorService
|
participant SVC as DistanceCalculatorService
|
||||||
@@ -1337,11 +1517,11 @@ La génération PDF est enveloppée dans un `CancellationToken` avec délai de 1
|
|||||||
La solution est hébergée via **Docker Compose** avec deux services :
|
La solution est hébergée via **Docker Compose** avec deux services :
|
||||||
|
|
||||||
- **`backend`** : API ASP.NET Core (.NET 8), exposant le port `8080` en interne. Le fichier SQLite est stocké sous le chemin du volume monté `/data/trajets.db`.
|
- **`backend`** : API ASP.NET Core (.NET 8), exposant le port `8080` en interne. Le fichier SQLite est stocké sous le chemin du volume monté `/data/trajets.db`.
|
||||||
- **`frontend`** : application Angular compilée et servie par **nginx**. Nginx sert la SPA (fallback vers `index.html` pour le routage côté client) et agit comme reverse-proxy en relayant les requêtes `/api` vers le service `backend`. C'est le seul service exposé à l'hôte (port `8080` de l'hôte → nginx).
|
- **`frontend`** : application Angular compilée et servie par **nginx**. Nginx sert la SPA (fallback vers `index.html` pour le routage côté client) et agit comme reverse-proxy en relayant les requêtes `/api` vers le service `backend`. C'est le seul service exposé à l'hôte (port **`2202`** de l'hôte → nginx). La terminaison HTTPS est assurée par un reverse-proxy **Caddy externe**.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph LR
|
graph LR
|
||||||
U[Navigateur] -- "http://hôte:8080" --> NGINX
|
U[Navigateur] -- "Caddy HTTPS externe → http://hôte:2202" --> NGINX
|
||||||
subgraph "Réseau Docker Compose"
|
subgraph "Réseau Docker Compose"
|
||||||
NGINX[frontend<br/>nginx + SPA Angular]
|
NGINX[frontend<br/>nginx + SPA Angular]
|
||||||
API[backend<br/>ASP.NET Core :8080]
|
API[backend<br/>ASP.NET Core :8080]
|
||||||
@@ -1485,59 +1665,7 @@ volumes:
|
|||||||
|
|
||||||
Les **migrations EF Core sont appliquées automatiquement au démarrage du backend** (`await dbContext.Database.MigrateAsync()` dans `Program.cs` avant `app.Run()`). Ainsi, au premier lancement, la base `/data/trajets.db` est créée sur le volume monté ; aux lancements suivants, elle est mise à jour avec les migrations en attente sans intervention manuelle.
|
Les **migrations EF Core sont appliquées automatiquement au démarrage du backend** (`await dbContext.Database.MigrateAsync()` dans `Program.cs` avant `app.Run()`). Ainsi, au premier lancement, la base `/data/trajets.db` est créée sur le volume monté ; aux lancements suivants, elle est mise à jour avec les migrations en attente sans intervention manuelle.
|
||||||
|
|
||||||
---
|
> **Migrations notables** : `VehiculeDenomination` (dénomination du véhicule), `TrajetGeometrieJson` (géométrie d'itinéraire portée par le trajet), `TrancheBaremeKmMaxNullable` / bornes exclusives / coefficient numérique (barème multi-tranches à dernière tranche illimitée), `AjoutAuthentification` (comptes + jetons de réinitialisation), `TrajetAllerRetour` (indicateur aller/retour).
|
||||||
|
|
||||||
## Évolutions majeures postérieures à la conception initiale
|
|
||||||
|
|
||||||
> Cette section documente les décisions d'architecture des fonctionnalités ajoutées après la conception initiale. Elle fait autorité en cas de divergence avec les sections antérieures rédigées pour la version mono-utilisateur sans compte.
|
|
||||||
|
|
||||||
### Authentification par compte unique (Exigence 17)
|
|
||||||
|
|
||||||
- **Modèle** : une entité `Compte` (Id, Email, MotDePasseHash, champs de réinitialisation) — un seul enregistrement autorisé. Le mot de passe est stocké en **hachage salé** (jamais en clair).
|
|
||||||
- **Session** : à la connexion, le backend émet un **JWT signé HS256** portant l'identifiant du Compte (`sub`) et une expiration configurable (`Jwt:DurationHours`, 12 h par défaut). Le frontend conserve le jeton dans `localStorage` et l'ajoute en `Authorization: Bearer` via un intercepteur HTTP.
|
|
||||||
- _Décision_ : `localStorage` retenu malgré le risque XSS, acceptable pour une application personnelle mono-utilisateur ; cookie HttpOnly rejeté pour éviter la complexité CSRF.
|
|
||||||
- **Protection des endpoints** : tous les endpoints de données exigent un JWT valide (`[Authorize]` global) ; 401 sinon.
|
|
||||||
- **Validation renforcée** : `OnTokenValidated` rejette un JWT dont le Compte associé n'existe plus (lecture du claim via `sub` **ou** `ClaimTypes.NameIdentifier`). Un endpoint protégé `GET /api/auth/moi` permet au frontend de valider la Session ; un `authGuard` Angular appelle cette validation avant d'afficher les vues protégées.
|
|
||||||
- **Réinitialisation de mot de passe** : `POST` d'une demande → génération d'un `Jeton_Reinitialisation` à usage unique et durée limitée, envoyé par e-mail (**MailKit/SMTP**). Si l'e-mail n'est pas configurable, le lien est **journalisé en repli**. Un second endpoint consomme le jeton et remplace le hachage.
|
|
||||||
- **Clé secrète obligatoire** : `JWT_SECRET_KEY` est requise au démarrage (`:?` dans Compose) ; le backend refuse de démarrer sans elle.
|
|
||||||
- **Endpoints** : `POST /api/auth/inscription` (création du Compte unique), `POST /api/auth/connexion` (émission du JWT), `GET /api/auth/moi` (validation de session), `POST /api/auth/mot-de-passe/oubli` et `POST /api/auth/mot-de-passe/reinitialisation`.
|
|
||||||
|
|
||||||
### Onboarding en deux étapes (Exigences 16, 17)
|
|
||||||
|
|
||||||
L'initialisation se fait en deux temps : (1) création du Compte (e-mail + mot de passe ≥ 8 caractères) puis redirection vers la connexion ; (2) une fois authentifié, saisie de l'identité (nom, prénom, matricule), d'une adresse de domicile optionnelle et d'au moins un Véhicule. La première ouverture sans Compte redirige directement vers la page de création de compte.
|
|
||||||
|
|
||||||
### Trajet aller/retour (Exigence 18)
|
|
||||||
|
|
||||||
- **Stockage** : le `Trajet` conserve la **distance simple** mesurée et un booléen `AllerRetour`. Une propriété calculée `DistanceEffective` vaut `Distance × 2` si `AllerRetour`, sinon `Distance`.
|
|
||||||
- _Décision_ : on stocke la distance simple + le booléen plutôt que la distance déjà doublée, afin de conserver la valeur réelle et d'éviter toute ambiguïté à l'édition.
|
|
||||||
- **Agrégats** : `DistanceEffective` est la seule distance exposée par les DTO (liste, carte, PDF) et utilisée dans tous les totaux et calculs de barème.
|
|
||||||
- **UI** : badge « A/R » dans une colonne dédiée en début de ligne de la liste ; mention « (A/R) » après la distance dans l'infobulle de la carte.
|
|
||||||
- **Migration** : `TrajetAllerRetour` (ajout de la colonne `AllerRetour`).
|
|
||||||
|
|
||||||
### Consultation par mois (Exigence 12, refonte)
|
|
||||||
|
|
||||||
- **Sélecteur temporel** : réduit à année → mois (repliable, libellé « Mois Année »). Output `moisSelectionne: { annee, mois }` (l'ancien segment jour est retiré). Les mois futurs sont désactivés.
|
|
||||||
- _Décision_ : émettre `{ annee, mois }` plutôt qu'une date ISO du 1er du mois, car le conteneur a besoin des deux valeurs sans reparser.
|
|
||||||
- **Conteneur `ConsultationJour`** : deux états — un signal `mois` pilote la **liste mensuelle**, un signal `date` (ISO) pilote la **carte** et la date par défaut d'ajout/insertion.
|
|
||||||
- **Liste** : tous les trajets du mois, groupés par journée (`GroupeJour`), chaque groupe étant un `cdkDropList` **non connecté** aux autres → le glisser-déposer réordonne uniquement au sein d'une journée. Chaque journée est **repliable** ; par défaut tout est replié sauf le dernier jour du mois comportant des trajets.
|
|
||||||
- **Navigation jour** : bande « ‹ Jour préc. / date / Jour suiv. › » au-dessus des totaux et de la carte. Elle ne s'arrête que sur les **jours ayant des trajets** dans le mois ; exception : dans le mois courant, le jour d'aujourd'hui reste navigable même sans trajet. La navigation jour ne recharge que la carte et les totaux du jour, pas la liste.
|
|
||||||
- **Mois vide** : traité comme un cas normal — totaux à 0 et un unique message « Aucun trajet pour ce mois », **sans alerte d'erreur**.
|
|
||||||
|
|
||||||
### Barème multi-tranches et recopie (Exigence 13)
|
|
||||||
|
|
||||||
- **Modèle** : les `Tranche_Bareme` d'une (année, puissance fiscale) ne sont plus figées à trois bornes fixes. `KmMax` est **nullable** pour matérialiser une dernière tranche **illimitée**, et plusieurs tranches peuvent coexister pour une même puissance fiscale (bornes exclusives, coefficient sur 3 décimales).
|
|
||||||
- **Migrations** : `TrancheBaremeKmMaxNullable`, ainsi que les évolutions associées (bornes exclusives, coefficient numérique).
|
|
||||||
- **Recopie** : un endpoint/opération recopie l'intégralité des tranches de la dernière année antérieure comportant un barème vers l'année affichée (annule et remplace après confirmation si l'année cible en contient déjà). L'action est indisponible sur l'année du dernier barème connu.
|
|
||||||
|
|
||||||
### Dénomination de véhicule (Exigence 10)
|
|
||||||
|
|
||||||
Le `Vehicule` porte un champ `Denomination` (libellé libre, ex. « Marque, Modèle », watermark « Ex. Marque, Modèle »). Migration `VehiculeDenomination`.
|
|
||||||
|
|
||||||
### Recalcul ciblé des distances
|
|
||||||
|
|
||||||
Un endpoint `POST /api/trajets/recalculer-distances` recalcule via ORS uniquement les trajets **sans géométrie routière** (`GeometrieJson` NULL), afin de limiter les appels ORS aux seuls trajets calculés en mode dégradé (ex. clé ORS invalide au moment de la saisie). La géométrie de l'itinéraire est portée par le `Trajet` lui-même (paire départ→arrivée), le cache d'adresses servant de repli pour la rétrocompatibilité.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
@@ -2132,7 +2260,7 @@ public Property EtatProfilConfigureIffNomPrenomEtVehicule() { ... }
|
|||||||
- GET `/api/adresses/search?q=...`
|
- GET `/api/adresses/search?q=...`
|
||||||
- PUT `/api/profil` avec et sans géocodage réussi
|
- PUT `/api/profil` avec et sans géocodage réussi
|
||||||
- GET `/api/profil/etat` → `EstConfigure = true` (profil complet + véhicule) ; `EstConfigure = false` (profil incomplet ou sans véhicule)
|
- GET `/api/profil/etat` → `EstConfigure = true` (profil complet + véhicule) ; `EstConfigure = false` (profil incomplet ou sans véhicule)
|
||||||
- GET `/api/annees-disponibles`, `/api/mois-disponibles/{annee}`, `/api/jours-disponibles/{annee}/{mois}`
|
- GET `/api/annees-disponibles`, `/api/mois-disponibles/{annee}`
|
||||||
- GET `/api/totaux/{date}` : vérification TotalAnnuelPartiel borné au mois sélectionné
|
- GET `/api/totaux/{date}` : vérification TotalAnnuelPartiel borné au mois sélectionné
|
||||||
- POST `/api/vehicules` → détermination réaffectation proposée si trajets >= DateEffet avec autre puissance
|
- POST `/api/vehicules` → détermination réaffectation proposée si trajets >= DateEffet avec autre puissance
|
||||||
- POST `/api/vehicules/reaffectation` accepter=true → trajets >= DateEffet réaffectés ; accepter=false → inchangés
|
- POST `/api/vehicules/reaffectation` accepter=true → trajets >= DateEffet réaffectés ; accepter=false → inchangés
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ L'application est composée d'un frontend Angular/TypeScript et d'un backend C#.
|
|||||||
- **Trajet_Précédent** : Le Trajet immédiatement antérieur dans la liste chronologique d'une journée, dont l'adresse d'arrivée peut servir de départ lors d'une insertion.
|
- **Trajet_Précédent** : Le Trajet immédiatement antérieur dans la liste chronologique d'une journée, dont l'adresse d'arrivée peut servir de départ lors d'une insertion.
|
||||||
- **Trajet_Suivant** : Le Trajet immédiatement postérieur dans la liste chronologique d'une journée, dont l'adresse de départ peut servir d'arrivée lors d'une insertion.
|
- **Trajet_Suivant** : Le Trajet immédiatement postérieur dans la liste chronologique d'une journée, dont l'adresse de départ peut servir d'arrivée lors d'une insertion.
|
||||||
- **Position** : Un entier positif associé à chaque Trajet au sein d'une même journée, déterminant son rang dans la liste affichée. L'ordre d'affichage est toujours trié par Position croissante, indépendamment de l'heure de création du Trajet.
|
- **Position** : Un entier positif associé à chaque Trajet au sein d'une même journée, déterminant son rang dans la liste affichée. L'ordre d'affichage est toujours trié par Position croissante, indépendamment de l'heure de création du Trajet.
|
||||||
- **Sélecteur_Temporel** : Le composant de navigation permettant de choisir successivement une année, un mois puis une journée pour consulter les Trajets correspondants.
|
- **Sélecteur_Temporel** : Le composant de navigation (repliable) permettant de choisir successivement une année puis un mois pour consulter les Trajets correspondants. La journée ciblée sur la Carte est gérée séparément par une bande de navigation par jour.
|
||||||
- **Panneau_Totaux** : La zone latérale, toujours visible, affichant les totaux kilométriques du jour, du mois sélectionné et de l'année sélectionnée (cumul jusqu'au mois sélectionné inclus).
|
- **Panneau_Totaux** : La zone latérale, toujours visible, affichant les totaux kilométriques du jour, du mois sélectionné et de l'année sélectionnée (cumul jusqu'au mois sélectionné inclus).
|
||||||
- **Total_Annuel_Partiel** : Le total kilométrique de l'année sélectionnée cumulant uniquement les Trajets jusqu'au mois sélectionné inclus, sans inclure les mois postérieurs.
|
- **Total_Annuel_Partiel** : Le total kilométrique de l'année sélectionnée cumulant uniquement les Trajets jusqu'au mois sélectionné inclus, sans inclure les mois postérieurs.
|
||||||
- **Véhicule** : Un véhicule de l'utilisateur, caractérisé par une plaque d'immatriculation et une puissance fiscale, avec une date d'effet à partir de laquelle il devient le véhicule courant. L'historique des Véhicules est conservé.
|
- **Véhicule** : Un véhicule de l'utilisateur, caractérisé par une plaque d'immatriculation et une puissance fiscale, avec une date d'effet à partir de laquelle il devient le véhicule courant. L'historique des Véhicules est conservé.
|
||||||
|
|||||||
Reference in New Issue
Block a user