From 73a70142e452d3c70be217b69405dbb1c06dba45 Mon Sep 17 00:00:00 2001 From: Nikolaz Date: Wed, 16 Sep 2026 07:19:14 +0200 Subject: [PATCH] =?UTF-8?q?Specs=20:=20rattrapage=20de=20l'=C3=A9cart=20av?= =?UTF-8?q?ec=20le=20code=20+=20r=C3=A8gle=20de=20synchro=20continue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligne requirements.md et design.md sur l'état réel du code (écart accumulé depuis le 14/09 en mode dev direct). requirements.md : - Intro : compte unique authentifié (au lieu de « sans authentification ». - Glossaire : Compte, Adresse_Email, Mot_De_Passe, Session, JWT, Jeton_Reinitialisation, Aller_Retour, Distance_Effective. - Exigence 1 : case Aller/Retour. Exigence 10 : dénomination véhicule. - Exigence 12 : refonte consultation par mois (sélecteur année/mois, liste mensuelle groupée par jour repliable, nav jour bornée aux jours avec trajets). - Exigence 13 : tranches de barème multiples (dernière illimitée, coef 3 décimales) + recopie du dernier barème. - Exigence 16 : onboarding relié à l'authentification. - Nouvelles exigences 17 (Authentification) et 18 (Trajet aller/retour). design.md : - Overview + contraintes : compte authentifié, port 2202, Caddy externe HTTPS. - Déploiement : compose (port 2202, JWT_SECRET_KEY obligatoire, e-mail/MailKit, AUTH_FRONTEND_BASE_URL), reverse-proxy Caddy externe, pas de UseHttpsRedirection. - Nouvelle section « Évolutions majeures » : décisions d'architecture pour l'auth, l'onboarding 2 étapes, l'aller/retour, la consultation par mois, le barème multi-tranches, la dénomination véhicule et le recalcul ciblé des distances. Ajoute .kiro/steering/spec-sync.md : règle de mise à jour des specs dans le même commit que le code, à chaque évolution validée. --- .kiro/specs/schotziad-km-tracker/design.md | 78 ++++++++++++-- .../schotziad-km-tracker/requirements.md | 102 +++++++++++++----- .kiro/steering/spec-sync.md | 38 +++++++ 3 files changed, 186 insertions(+), 32 deletions(-) create mode 100644 .kiro/steering/spec-sync.md diff --git a/.kiro/specs/schotziad-km-tracker/design.md b/.kiro/specs/schotziad-km-tracker/design.md index 7be48ce..4e438ae 100644 --- a/.kiro/specs/schotziad-km-tracker/design.md +++ b/.kiro/specs/schotziad-km-tracker/design.md @@ -4,7 +4,7 @@ ## Overview -**SchotzIAD KM Tracker** est une application web mono-utilisateur permettant de saisir, calculer et consulter l'historique kilométrique de déplacements professionnels ou personnels. L'application calcule automatiquement les distances routières via l'API OpenRouteService (ORS), agrège les données par mois et par année, et permet l'export en PDF des résumés. +**SchotzIAD KM Tracker** est une application web mono-utilisateur, protégée par un **compte unique authentifié** (e-mail + mot de passe, session JWT), permettant de saisir, calculer et consulter l'historique kilométrique de déplacements professionnels ou personnels. L'application calcule automatiquement les distances routières via l'API OpenRouteService (ORS), agrège les données par mois et par année, et permet l'export en PDF des résumés. ### Périmètre fonctionnel @@ -22,25 +22,28 @@ | 10 | Profil / adresse domicile | Saisie du domicile, bouton "Utiliser mon domicile" dans le formulaire | | 11 | Commentaire par trajet | Champ optionnel texte libre (0–500 car.) associé à chaque Trajet | | 12 | Insertion entre deux trajets | Formulaire pré-rempli avec adresse d'arrivée du précédent / adresse de départ du suivant | -| 13 | Navigation temporelle | Sélecteur année/mois/jour avec surbrillance des périodes ayant des données, pas de date future | -| 14 | Mise en page consultation | Zone principale (trajets du jour, scroll) + panneau latéral totaux + carte + navigation jour±1 | +| 13 | Navigation temporelle | Sélecteur année/mois (repliable) avec surbrillance des mois ayant des données, pas de mois futur | +| 14 | Mise en page consultation | Liste mensuelle groupée par jour (repliable, scroll) + panneau totaux + carte du jour ciblé + navigation jour bornée au mois | | 15 | Profil enrichi & véhicules | Nom, prénom, matricule (8 car.), plaque, puissance fiscale ; historique des véhicules daté | | 16 | Association véhicule par trajet | Chaque trajet référence un véhicule + puissance fiscale ; totaux filtrés par véhicule | | 17 | Barème kilométrique annuel | Saisie par tranches (puissance fiscale, bornes km, coefficient, part fixe) ; montant prévisionnel minoré 80% | | 18 | Montants réglés & régularisation | Saisie du montant réglé par mois ; rapport annuel avec montant réel, versé, dû et régularisation | | 19 | Carnet d'adresses nommées | Menu CRUD d'Adresses_Nommées (Dénomination unique + adresse BAN) ; réutilisation par Dénomination dans la saisie et l'insertion de Trajets | | 20 | Onboarding première utilisation | Présentation de la saisie du profil (identité + véhicule) si le Profil n'est pas configuré, avant tout accès à la saisie / consultation des trajets | +| 21 | Authentification | Compte unique (e-mail + mot de passe), connexion par JWT (12 h par défaut), réinitialisation de mot de passe par e-mail, garde de session serveur | +| 22 | Trajet aller/retour | Case à cocher A/R doublant la distance comptée (Distance_Effective) sans ligne supplémentaire ; badge A/R en liste et mention « (A/R) » sur la carte | +| 23 | Barème multi-tranches & recopie | Tranches paramétrables par puissance fiscale (dernière tranche illimitée), coefficient sur 3 décimales, bouton « Recopier le dernier barème connu » | ### Contraintes techniques -- **Mono-utilisateur** : pas d'authentification ni de gestion de comptes +- **Mono-utilisateur authentifié** : un seul Compte (e-mail + mot de passe), session par jeton JWT signé HS256 (durée configurable, 12 h par défaut) ; tous les endpoints de données sont protégés - **Frontend** : Angular (TypeScript), compilé et servi en SPA - **Backend** : ASP.NET Core Web API (C#) -- **Base de données** : SQLite (choix retenu pour simplicité mono-utilisateur, zero-configuration, fichier unique portable) +- **Base de données** : SQLite (simplicité mono-utilisateur, zero-configuration, fichier unique portable) ; migrations EF Core appliquées automatiquement au démarrage - **Service externe — adresses** : API BAN (Base Adresses Nationale) — `https://api-adresse.data.gouv.fr` — autocomplétion et géocodage des adresses françaises, sans clé d'authentification - **Service externe — itinéraires** : OpenRouteService (ORS) — calcul de distance routière et géométrie d'itinéraire entre coordonnées géographiques connues - **Génération PDF** : côté backend, bibliothèque QuestPDF -- **Hébergement** : l'application est packagée et hébergée via **Docker Compose** — un conteneur `frontend` (Angular servi par nginx) + un conteneur `backend` (ASP.NET Core API). Le fichier SQLite est persisté sur un volume nommé (ou bind mount) monté dans le conteneur backend, afin de survivre aux redémarrages et mises à jour des conteneurs +- **Hébergement** : l'application est packagée et hébergée via **Docker Compose** — un conteneur `frontend` (Angular servi par nginx en interne) publié sur le port hôte **2202**, et un conteneur `backend` (ASP.NET Core API) non exposé à l'hôte. Le fichier SQLite est persisté sur un volume monté dans le conteneur backend. La terminaison **HTTPS est assurée par un reverse-proxy Caddy externe** (sur une autre machine) qui relaie vers `IP_hôte:2202` ; le backend ne fait donc pas de redirection HTTPS lui-même --- @@ -1347,7 +1350,7 @@ graph LR end ``` -> **Approche retenue** : reverse-proxy unique via nginx (frontend). Le backend n'est pas exposé directement à l'hôte ; tout le trafic transite par nginx, qui sert les fichiers statiques et relaie `/api` vers le backend sur le réseau interne Compose. +> **Approche retenue** : à l'intérieur de Compose, nginx (conteneur frontend) sert les fichiers statiques Angular et relaie `/api` vers le backend sur le réseau interne ; le backend n'est pas exposé directement à l'hôte. La terminaison **HTTPS est déléguée à un reverse-proxy Caddy externe** (sur une autre machine) qui expose l'application sur le net via une URL HTTPS et relaie vers `IP_hôte:2202`. Le port hôte 2202 est donc à restreindre par pare-feu à l'IP du proxy Caddy. Le backend ne réalise pas de redirection HTTPS (`UseHttpsRedirection` retiré), la terminaison TLS étant assurée en amont. ### Backend `Dockerfile` (multi-stage) @@ -1438,6 +1441,10 @@ services: - ConnectionStrings__Default=Data Source=/data/trajets.db # La clé ORS est fournie par l'environnement / .env — jamais intégrée à l'image - ORS__ApiKey=${ORS_API_KEY} + # Clé de signature JWT : OBLIGATOIRE (le backend refuse de démarrer si absente) + - JWT__SecretKey=${JWT_SECRET_KEY:?JWT_SECRET_KEY requis} + # URL publique du frontend (liens de réinitialisation de mot de passe), ex. https://mon-domaine + - Auth__FrontendBaseUrl=${AUTH_FRONTEND_BASE_URL} volumes: - trajets-data:/data restart: unless-stopped @@ -1453,7 +1460,7 @@ services: context: ./frontend dockerfile: Dockerfile ports: - - "8080:80" # hôte 8080 → nginx (frontend) + - "2202:80" # hôte 2202 → nginx (frontend) ; 8080 évité (conflit hôte fréquent) depends_on: backend: condition: service_healthy @@ -1468,6 +1475,9 @@ volumes: ### Configuration et secrets - La **clé ORS** est fournie exclusivement via variable d'environnement (`ORS_API_KEY`), typiquement à partir d'un fichier `.env` **non commité** (ajouté au `.gitignore`) et lu automatiquement par Docker Compose. Elle n'est **jamais** intégrée dans une image Docker. +- La **clé secrète JWT** (`JWT_SECRET_KEY`) est **obligatoire** : le backend refuse de démarrer si elle est absente. Elle est fournie via `.env`, jamais commitée. +- L'**URL publique du frontend** (`AUTH_FRONTEND_BASE_URL`, ex. `https://mon-domaine`) sert à construire les liens de réinitialisation de mot de passe envoyés par e-mail — et non le port interne. +- La **configuration e-mail** (SMTP via MailKit) est optionnelle : si elle est absente, le lien de réinitialisation est journalisé en repli (fallback) au lieu d'être envoyé. - L'**API BAN** ne nécessite aucune clé — aucune configuration de secret n'est requise pour le géocodage. - Le **volume nommé `trajets-data`** (monté sur `/data`) garantit que le fichier `trajets.db` persiste à travers les redémarrages, recréations et mises à jour des conteneurs. @@ -1477,6 +1487,58 @@ Les **migrations EF Core sont appliquées automatiquement au démarrage du backe --- +## É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 ### Stratégie globale diff --git a/.kiro/specs/schotziad-km-tracker/requirements.md b/.kiro/specs/schotziad-km-tracker/requirements.md index 640a1d5..bb52f9a 100644 --- a/.kiro/specs/schotziad-km-tracker/requirements.md +++ b/.kiro/specs/schotziad-km-tracker/requirements.md @@ -2,7 +2,7 @@ ## Introduction -L'application **SchotzIAD KM Tracker** est une application web **mono-utilisateur** (sans authentification ni gestion de comptes) permettant de saisir et de suivre ses trajets mensuels (jour, adresse de départ, adresse d'arrivée), de calculer automatiquement les kilomètres parcourus par mois, et de consulter une vue annuelle consolidée des kilomètres parcourus. +L'application **SchotzIAD KM Tracker** est une application web **mono-utilisateur** protégée par un **compte unique authentifié** (adresse e-mail + mot de passe, session par jeton JWT). Elle permet de saisir et de suivre ses trajets mensuels (jour, adresse de départ, adresse d'arrivée, éventuellement en aller/retour), de calculer automatiquement les kilomètres parcourus par mois, et de consulter une vue annuelle consolidée des kilomètres parcourus. L'application ne gère qu'un seul compte : la première utilisation crée ce compte via un parcours d'inscription, et toute utilisation ultérieure exige une authentification. L'application est composée d'un frontend Angular/TypeScript et d'un backend C#. La saisie des adresses s'appuie sur l'API de la **Base Adresses Nationale (BAN)** (disponible sur data.gouv.fr) pour l'autocomplétion et la résolution des coordonnées géographiques. Le calcul de distance routière utilise l'API **OpenRouteService (ORS)** uniquement lorsque les coordonnées des deux adresses sont connues. Elle cible des utilisateurs souhaitant garder un historique fiable de leurs déplacements, notamment à des fins professionnelles ou de remboursement de frais. @@ -54,6 +54,14 @@ L'application est composée d'un frontend Angular/TypeScript et d'un backend C#. - **Regularisation** : La différence entre le Montant_Reel annuel et le total des Montant_Regle versés sur l'année, indiquant le montant restant à verser (ou à récupérer si négatif). - **Frais_Reel_Mensuel** : La quote-part mensuelle du Montant_Reel, calculée en appliquant à chaque mois le coefficient de la tranche annuelle applicable (déterminée par le total annuel) : `Frais_Reel_Mensuel = km_du_mois × coefficient_tranche_annuelle`. La somme des douze Frais_Reel_Mensuel plus la base éventuelle de la tranche annuelle est égale au Montant_Reel. - **Coefficient_Base_Vehicule** : Le coefficient et la base de la tranche du Bareme_Kilometrique correspondant à la Puissance_Fiscale du Véhicule et au total kilométrique annuel, affichés dans l'en-tête des feuilles mensuelles (ex. « Coef. 0,447 / Base 0 »). +- **Compte** : L'unique compte de l'Application, identifié par une adresse e-mail (servant de login) et protégé par un mot de passe. L'Application n'autorise l'existence que d'un seul Compte. +- **Adresse_Email** : L'adresse e-mail de l'utilisateur, utilisée comme identifiant de connexion du Compte et comme destinataire des e-mails de réinitialisation de mot de passe. +- **Mot_De_Passe** : Le secret d'au moins 8 caractères associé au Compte, stocké uniquement sous forme de hachage salé (jamais en clair). +- **Session** : L'état authentifié d'un utilisateur, matérialisé par un jeton JWT signé émis à la connexion, transmis par le Frontend à chaque appel protégé du Backend. +- **JWT** : JSON Web Token signé (HS256) émis par le Backend à la connexion, portant l'identifiant du Compte et une durée de validité configurable (12 heures par défaut). +- **Jeton_Reinitialisation** : Un jeton à usage unique et à durée de vie limitée, envoyé par e-mail, permettant de définir un nouveau Mot_De_Passe sans connaître l'ancien. +- **Aller_Retour** : Un indicateur booléen d'un Trajet signifiant que le déplacement est effectué dans les deux sens. Lorsqu'il est vrai, la Distance comptée du Trajet (Distance_Effective) est le double de la distance simple mesurée entre le départ et l'arrivée. +- **Distance_Effective** : La distance comptée d'un Trajet dans tous les agrégats (liste, totaux, carte, PDF, barème) : égale à la distance simple, ou à son double lorsque le Trajet est un Aller_Retour. --- @@ -84,6 +92,7 @@ L'application est composée d'un frontend Angular/TypeScript et d'un backend C#. 17. IF l'utilisateur soumet une adresse saisie manuellement sans passer par l'autocomplétion BAN (coordonnées non disponibles), THEN THE Frontend SHALL afficher un avertissement indiquant que la distance ne pourra pas être calculée automatiquement si les coordonnées sont absentes. 18. THE Frontend SHALL proposer, pour le champ adresse de départ et pour le champ adresse d'arrivée du formulaire de saisie, un choix entre sélectionner une Adresse_Nommee pré-enregistrée du Carnet_Adresses (par sa Dénomination) via un sélecteur et saisir une nouvelle adresse manuellement avec l'autocomplétion BAN. 19. WHEN l'utilisateur sélectionne une Adresse_Nommee (par sa Dénomination) dans le sélecteur associé au champ adresse de départ ou adresse d'arrivée, THE Frontend SHALL pré-remplir ce champ avec le texte d'adresse de l'Adresse_Nommee et associer ses coordonnées géographiques (latitude, longitude) pour transmission au Backend. +20. THE Frontend SHALL afficher, dans le formulaire de saisie d'un Trajet, une case à cocher "Aller/Retour" (Aller_Retour) qui, lorsqu'elle est cochée, indique que la Distance comptée du Trajet doit être doublée sans créer de Trajet supplémentaire ni de ligne additionnelle dans les résumés. --- @@ -238,7 +247,7 @@ L'application est composée d'un frontend Angular/TypeScript et d'un backend C#. 6. IF aucune Adresse_Domicile n'est enregistrée dans le Profil, THEN THE Frontend SHALL désactiver le bouton "Utiliser mon domicile" et afficher une infobulle indiquant que l'adresse de domicile n'est pas encore configurée. 7. WHEN l'utilisateur modifie l'Adresse_Domicile dans le Profil et sauvegarde, THE Backend SHALL re-géocoder la nouvelle adresse via l'API BAN pour mettre à jour les coordonnées dans le Profil et dans le Cache_Adresses. 8. IF le géocodage BAN de l'Adresse_Domicile échoue, THEN THE Backend SHALL sauvegarder l'Adresse_Domicile textuelle sans coordonnées et THE Frontend SHALL afficher un avertissement indiquant que l'adresse n'a pas pu être vérifiée. -9. THE Frontend SHALL permettre à l'utilisateur de saisir un Véhicule caractérisé par une plaque d'immatriculation (1 à 15 caractères) et une Puissance_Fiscale (entier positif, exprimé en chevaux fiscaux). +9. THE Frontend SHALL permettre à l'utilisateur de saisir un Véhicule caractérisé par une Dénomination libre (ex. « Marque, Modèle », watermark indicatif « Ex. Marque, Modèle »), une plaque d'immatriculation (1 à 15 caractères) et une Puissance_Fiscale (entier positif, exprimé en chevaux fiscaux). 10. WHEN l'utilisateur enregistre un nouveau Véhicule, THE Frontend SHALL exiger la saisie d'une Date_Effet inférieure ou égale à la date du jour. 11. IF l'utilisateur saisit une Date_Effet postérieure à la date du jour, THEN THE Frontend SHALL rejeter la saisie et afficher un message indiquant que la date d'effet ne peut pas être dans le futur. 12. WHEN le Backend enregistre un nouveau Véhicule, THE Backend SHALL conserver l'historique complet des Véhicules précédents sans les supprimer. @@ -273,30 +282,32 @@ L'application est composée d'un frontend Angular/TypeScript et d'un backend C#. ### Requirement 12: Navigation temporelle et mise en page -**User Story:** En tant qu'utilisateur, je veux naviguer facilement entre les années, mois et journées ayant des données, et voir en permanence les totaux et la carte, afin de consulter et saisir mes trajets de façon fluide. +**User Story:** En tant qu'utilisateur, je veux naviguer facilement par année et par mois, consulter d'un coup tous les trajets d'un mois regroupés par jour, cibler une journée précise sur la carte, et voir en permanence les totaux et la carte, afin de consulter et saisir mes trajets de façon fluide. #### Acceptance Criteria -1. THE Frontend SHALL afficher un Sélecteur_Temporel permettant de choisir successivement une année, puis un mois, puis une journée. +1. THE Frontend SHALL afficher un Sélecteur_Temporel repliable permettant de choisir successivement une année puis un mois ; la sélection ne descend pas jusqu'à la journée. Le libellé de l'en-tête indique le mois et l'année sélectionnés (ex. « Septembre 2026 »). 2. THE Frontend SHALL proposer dans le sélecteur d'année uniquement l'année courante et les années pour lesquelles au moins un Trajet est enregistré. 3. WHEN l'utilisateur sélectionne une année, THE Frontend SHALL afficher les douze mois de cette année et mettre en surbrillance les mois comportant au moins un Trajet. -4. WHEN l'utilisateur sélectionne un mois, THE Frontend SHALL afficher les jours de ce mois et mettre en surbrillance les jours comportant au moins un Trajet. -5. THE Frontend SHALL empêcher la sélection de toute date postérieure à la date courante (aucune date future sélectionnable). -6. THE Frontend SHALL afficher la zone des Trajets de la journée sélectionnée comme zone principale, avec défilement vertical (scroll) lorsque le nombre de Trajets dépasse la hauteur visible. -7. THE Frontend SHALL afficher un Panneau_Totaux latéral toujours visible contenant : le total kilométrique du jour sélectionné, le total kilométrique du mois sélectionné, et le Total_Annuel_Partiel de l'année sélectionnée. -8. THE Panneau_Totaux SHALL calculer le Total_Annuel_Partiel en cumulant uniquement les Trajets de l'année sélectionnée jusqu'au mois sélectionné inclus, sans inclure les mois postérieurs au mois sélectionné. -9. THE Frontend SHALL afficher, sous le Panneau_Totaux, la Carte des Trajets de la journée sélectionnée. -10. THE Frontend SHALL afficher un bouton "Jour précédent" et un bouton "Jour suivant" permettant de naviguer d'une journée à l'autre. -11. WHEN l'utilisateur clique sur "Jour précédent", THE Frontend SHALL sélectionner la journée calendaire immédiatement antérieure et rafraîchir la zone des Trajets, le Panneau_Totaux et la Carte. -12. WHEN l'utilisateur clique sur "Jour suivant", THE Frontend SHALL sélectionner la journée calendaire immédiatement postérieure et rafraîchir la zone des Trajets, le Panneau_Totaux et la Carte. -13. IF la journée suivante est postérieure à la date courante, THEN THE Frontend SHALL désactiver le bouton "Jour suivant". -14. WHEN l'utilisateur change de jour, de mois ou d'année via le Sélecteur_Temporel ou les boutons de navigation, THE Frontend SHALL mettre à jour la zone des Trajets, le Panneau_Totaux et la Carte dans un délai de 1000 ms sans rechargement complet de la page. -15. WHEN le Backend reçoit une requête des années disponibles, THE Backend SHALL retourner la liste des années comportant au moins un Trajet, ainsi que l'année courante si elle n'y figure pas déjà. -16. WHEN le Backend reçoit une requête des jours comportant des Trajets pour un mois et une année donnés, THE Backend SHALL retourner la liste des jours de ce mois pour lesquels au moins un Trajet est enregistré. -17. THE Frontend SHALL afficher un sélecteur de Véhicule permettant de choisir le Véhicule dont les données sont affichées ; par défaut, le Véhicule_Courant est sélectionné. -18. WHEN l'utilisateur sélectionne un Véhicule dans le sélecteur de Véhicule, THE Frontend SHALL filtrer la zone des Trajets, le Panneau_Totaux et la Carte pour n'afficher que les Trajets associés à ce Véhicule. -19. THE Panneau_Totaux SHALL calculer le total du jour, le total du mois et le Total_Annuel_Partiel uniquement à partir des Trajets associés au Véhicule sélectionné. -20. WHEN l'utilisateur change de Véhicule sélectionné, THE Frontend SHALL mettre à jour la zone des Trajets, le Panneau_Totaux et la Carte dans un délai de 1000 ms sans rechargement complet de la page. +4. THE Frontend SHALL empêcher la sélection de tout mois postérieur au mois courant (aucun mois futur sélectionnable). +5. WHEN l'utilisateur sélectionne un mois, THE Frontend SHALL afficher la liste de TOUS les Trajets de ce mois, regroupés par journée et triés par date croissante puis par Position croissante au sein de chaque journée. +6. THE Frontend SHALL afficher chaque journée de la liste mensuelle comme un groupe repliable ; par défaut toutes les journées sont repliées et, au changement de mois, seule la dernière journée comportant des Trajets est dépliée. +7. THE Frontend SHALL afficher la liste mensuelle des Trajets comme zone principale, avec défilement vertical (scroll) lorsque le nombre de Trajets dépasse la hauteur visible, tandis que le Sélecteur_Temporel, la navigation par jour, le Panneau_Totaux et la Carte restent visibles. +8. THE Frontend SHALL afficher un Panneau_Totaux toujours visible contenant : le total kilométrique du jour ciblé sur la Carte, le total kilométrique du mois sélectionné, et le Total_Annuel_Partiel de l'année sélectionnée. +9. THE Panneau_Totaux SHALL calculer le Total_Annuel_Partiel en cumulant uniquement les Trajets de l'année sélectionnée jusqu'au mois sélectionné inclus, sans inclure les mois postérieurs au mois sélectionné. +10. THE Frontend SHALL afficher une bande de navigation par jour (« Jour précédent », date affichée, « Jour suivant ») au-dessus du Panneau_Totaux et de la Carte ; cette date pilote le jour dont les Trajets sont représentés sur la Carte et sert de date par défaut lors de l'ajout ou de l'insertion d'un Trajet. +11. THE Frontend SHALL afficher la Carte des Trajets de la seule journée ciblée par la bande de navigation par jour. +12. WHEN l'utilisateur clique sur « Jour précédent » ou « Jour suivant », THE Frontend SHALL cibler respectivement le jour comportant des Trajets immédiatement antérieur ou postérieur DANS LE MOIS sélectionné, et rafraîchir uniquement la Carte et le Panneau_Totaux du jour, sans recharger la liste mensuelle. +13. WHERE le mois sélectionné est le mois courant, THE Frontend SHALL rendre la journée courante navigable et ciblable même si elle ne comporte aucun Trajet (afin de pouvoir y ajouter un Trajet). +14. IF aucun jour comportant des Trajets n'existe avant (respectivement après) le jour ciblé dans le mois, THEN THE Frontend SHALL désactiver le bouton « Jour précédent » (respectivement « Jour suivant »). +15. WHEN l'utilisateur change de mois via le Sélecteur_Temporel, ou de jour via la bande de navigation, ou de Véhicule, THE Frontend SHALL mettre à jour la liste, le Panneau_Totaux et la Carte concernés dans un délai de 1000 ms sans rechargement complet de la page. +16. WHEN le Backend reçoit une requête des années disponibles, THE Backend SHALL retourner la liste des années comportant au moins un Trajet, ainsi que l'année courante si elle n'y figure pas déjà. +17. WHEN le Backend reçoit une requête des mois comportant des Trajets pour une année donnée, THE Backend SHALL retourner la liste des mois de cette année pour lesquels au moins un Trajet est enregistré. +18. IF le mois sélectionné ne comporte aucun Trajet, THEN THE Frontend SHALL afficher un unique message « Aucun trajet pour ce mois » et des totaux à zéro, sans afficher de message d'erreur (un mois vide n'est pas une erreur). +19. THE Frontend SHALL afficher un sélecteur de Véhicule permettant de choisir le Véhicule dont les données sont affichées ; par défaut, le Véhicule_Courant est sélectionné. +20. WHEN l'utilisateur sélectionne un Véhicule dans le sélecteur de Véhicule, THE Frontend SHALL filtrer la liste mensuelle des Trajets, le Panneau_Totaux et la Carte pour n'afficher que les Trajets associés à ce Véhicule. +21. THE Panneau_Totaux SHALL calculer le total du jour, le total du mois et le Total_Annuel_Partiel uniquement à partir des Trajets associés au Véhicule sélectionné. +22. WHEN l'utilisateur change de Véhicule sélectionné, THE Frontend SHALL mettre à jour la liste mensuelle des Trajets, le Panneau_Totaux et la Carte dans un délai de 1000 ms sans rechargement complet de la page. --- @@ -306,7 +317,7 @@ L'application est composée d'un frontend Angular/TypeScript et d'un backend C#. #### Acceptance Criteria -1. THE Frontend SHALL afficher un menu de saisie du Bareme_Kilometrique par année, permettant de saisir, pour chaque Puissance_Fiscale, les trois tranches de kilométrage : jusqu'à 5 000 km, de 5 001 à 20 000 km, et au-delà de 20 000 km, chacune avec son coefficient (€/km) et sa base (€). +1. THE Frontend SHALL afficher un menu de saisie du Bareme_Kilometrique par année permettant de saisir, pour chaque Puissance_Fiscale, une ou plusieurs Tranches_Bareme définies par une borne minimale de kilométrage, une borne maximale de kilométrage (la dernière tranche pouvant être illimitée, sans borne maximale), un coefficient (€/km sur trois décimales) et une base (€). L'utilisateur peut ajouter plusieurs tranches pour une même Puissance_Fiscale (ex. 0–4 999 km, 5 000–19 999 km, 20 000 km et plus). 2. WHEN l'utilisateur enregistre une Tranche_Bareme, THE Backend SHALL persister la tranche associée à l'année, à la Puissance_Fiscale et à la borne de kilométrage concernées. 3. WHEN le Backend applique le Bareme_Kilometrique au total kilométrique annuel d'un Véhicule pour une année donnée, THE Backend SHALL sélectionner la tranche dont l'intervalle de kilométrage contient le total annuel, puis calculer le montant selon la formule `montant = total_km_annuel × coefficient + base`. 4. IF aucune Tranche_Bareme n'existe pour la Puissance_Fiscale et l'année d'un calcul, THEN THE Backend SHALL retourner le montant zéro et signaler que le barème n'est pas configuré pour cette combinaison. @@ -314,6 +325,7 @@ L'application est composée d'un frontend Angular/TypeScript et d'un backend C#. 6. THE Frontend SHALL indiquer explicitement que le Montant_Previsionnel (80%) est une simple indication de versement et que le montant réellement versé est celui saisi par l'utilisateur (Montant_Regle). 7. THE Frontend SHALL afficher tous les montants en euros avec une précision de deux décimales. 8. THE Frontend SHALL afficher, dans l'en-tête de chaque feuille mensuelle, le Coefficient_Base_Vehicule (coefficient et base de la tranche applicable selon la puissance fiscale du Véhicule et le total kilométrique annuel). +9. THE Frontend SHALL proposer un bouton « Recopier le dernier barème connu » qui, pour l'année affichée, recopie l'intégralité des Tranches_Bareme de l'année la plus récente antérieure comportant un barème. IF des Tranches_Bareme existent déjà pour l'année affichée, THEN THE Frontend SHALL demander une confirmation avant de les remplacer intégralement (annule et remplace). THE Frontend SHALL rendre ce bouton inaccessible lorsque l'année affichée est celle du dernier barème connu (aucune année antérieure à recopier). --- @@ -361,13 +373,15 @@ L'application est composée d'un frontend Angular/TypeScript et d'un backend C#. ### Requirement 16: Initialisation du profil à la première utilisation -**User Story:** En tant qu'utilisateur, je veux que l'application me présente la saisie de mon profil dès la première connexion, afin de configurer mon identité et mon véhicule avant de commencer à saisir mes trajets. +**User Story:** En tant qu'utilisateur, je veux que l'application me présente la saisie de mon profil juste après la création et la connexion de mon Compte, afin de configurer mon identité et mon véhicule avant de commencer à saisir mes trajets. + +> **Note** : Le parcours d'initialisation se déroule en deux étapes. La première étape (création du Compte : Adresse_Email + Mot_De_Passe, puis connexion) est décrite à l'Exigence 17 (Authentification). La présente exigence décrit la seconde étape (identité, matricule, adresse de domicile optionnelle et Véhicule), présentée une fois l'utilisateur authentifié. #### Acceptance Criteria 1. THE Backend SHALL considérer le Profil comme non configuré tant qu'aucun Profil n'est enregistré avec un nom et un prénom non vides, ou tant qu'aucun Véhicule (plaque, Puissance_Fiscale, Date_Effet) n'est enregistré. 2. THE Backend SHALL exposer un indicateur de configuration du Profil (`estConfigure`) valant vrai lorsque le Profil est un Profil_Configure et faux dans le cas contraire. -3. WHEN l'utilisateur ouvre l'Application, WHILE le Profil est non configuré, THE Frontend SHALL présenter le parcours d'Onboarding (formulaire de saisie du Profil) au lieu de la Vue_Consultation. +3. WHEN l'utilisateur authentifié ouvre l'Application, WHILE le Profil est non configuré, THE Frontend SHALL présenter le parcours d'Onboarding (formulaire de saisie du Profil) au lieu de la Vue_Consultation. 4. WHILE le Profil est non configuré, THE Frontend SHALL bloquer l'accès à la saisie et à la consultation des Trajets et maintenir l'affichage du parcours d'Onboarding. 5. THE Frontend SHALL exiger, dans le parcours d'Onboarding, la saisie du nom (1 à 100 caractères), du prénom (1 à 100 caractères) et du matricule (1 à 8 caractères) avant d'autoriser la validation du Profil. 6. THE Frontend SHALL exiger, dans le parcours d'Onboarding, la saisie d'au moins un Véhicule comportant une plaque d'immatriculation (1 à 15 caractères), une Puissance_Fiscale (entier positif) et une Date_Effet inférieure ou égale à la date du jour avant d'autoriser la validation du Profil. @@ -378,3 +392,43 @@ L'application est composée d'un frontend Angular/TypeScript et d'un backend C#. 11. IF le Profil est déjà un Profil_Configure lors de l'ouverture de l'Application, THEN THE Frontend SHALL afficher directement la Vue_Consultation sans présenter le parcours d'Onboarding. 12. IF la persistance du Profil ou du Véhicule échoue au niveau du Backend lors de la soumission du parcours d'Onboarding, THEN THE Frontend SHALL afficher un message d'erreur indiquant l'échec de l'enregistrement, ne pas marquer le Profil comme configuré et conserver les données saisies par l'utilisateur. 13. IF la récupération de l'indicateur `estConfigure` échoue lors de l'ouverture de l'Application, THEN THE Frontend SHALL afficher un message d'erreur indiquant un problème de connexion au serveur et proposer de réessayer, sans présenter la Vue_Consultation. + +--- + +### Requirement 17: Authentification par compte unique + +**User Story:** En tant qu'utilisateur, je veux protéger l'accès à mon application par un compte (e-mail + mot de passe) avec la possibilité de réinitialiser mon mot de passe, afin que mes données de trajets restent privées. + +#### Acceptance Criteria + +1. THE Application SHALL n'autoriser l'existence que d'un seul Compte (application mono-utilisateur). +2. WHEN aucun Compte n'existe et que l'utilisateur ouvre l'Application, THE Frontend SHALL présenter la page de création de compte (première étape de l'initialisation) au lieu de la page de connexion. +3. THE Frontend SHALL exiger, à la création du Compte, une Adresse_Email valide (servant de login) et un Mot_De_Passe d'au moins 8 caractères avant d'autoriser la soumission. +4. WHEN l'utilisateur soumet la création du Compte avec une Adresse_Email valide et un Mot_De_Passe d'au moins 8 caractères, THE Backend SHALL créer le Compte en stockant uniquement un hachage salé du Mot_De_Passe (jamais le mot de passe en clair) et rediriger l'utilisateur vers la page de connexion. +5. IF un Compte existe déjà lors d'une tentative de création de compte, THEN THE Backend SHALL rejeter la création avec une erreur explicite (un seul Compte autorisé). +6. WHEN l'utilisateur soumet la page de connexion avec une Adresse_Email et un Mot_De_Passe correspondant au Compte, THE Backend SHALL émettre un JWT signé (HS256) portant l'identifiant du Compte et une durée de validité configurable (12 heures par défaut), et THE Frontend SHALL conserver ce jeton pour authentifier les appels suivants. +7. IF l'Adresse_Email ou le Mot_De_Passe soumis à la connexion ne correspond pas au Compte, THEN THE Backend SHALL rejeter la connexion avec une erreur d'identifiants invalides, sans indiquer lequel des deux champs est erroné. +8. THE Backend SHALL protéger tous les endpoints de données (Trajets, résumés, totaux, profil, véhicules, barème, carnet d'adresses, export PDF) en exigeant un JWT valide, et SHALL retourner une erreur HTTP 401 en l'absence de jeton valide. +9. WHEN le Backend valide un JWT dont le Compte associé n'existe plus, THE Backend SHALL rejeter la requête (jeton invalide) afin qu'un jeton résiduel ne donne pas accès à une application dont le compte a été supprimé. +10. THE Backend SHALL exposer un endpoint permettant au Frontend de valider la Session courante (existence du Compte et validité du jeton), utilisé par la garde de navigation du Frontend avant d'afficher les vues protégées. +11. WHEN l'utilisateur demande la réinitialisation de son Mot_De_Passe en fournissant son Adresse_Email, THE Backend SHALL générer un Jeton_Reinitialisation à usage unique et à durée de vie limitée, et l'envoyer par e-mail à l'Adresse_Email du Compte. +12. IF l'envoi de l'e-mail de réinitialisation n'est pas possible (service e-mail non configuré ou indisponible), THEN THE Backend SHALL journaliser le lien de réinitialisation en repli (fallback) afin que la réinitialisation reste possible, sans faire échouer la demande. +13. WHEN l'utilisateur suit un Jeton_Reinitialisation valide et fournit un nouveau Mot_De_Passe d'au moins 8 caractères, THE Backend SHALL remplacer le hachage du Mot_De_Passe du Compte et invalider le Jeton_Reinitialisation. +14. IF le Jeton_Reinitialisation est expiré, déjà utilisé ou inconnu, THEN THE Backend SHALL rejeter la réinitialisation avec une erreur explicite sans modifier le Mot_De_Passe. +15. THE Backend SHALL exiger la présence d'une clé secrète de signature JWT au démarrage (variable d'environnement obligatoire) et SHALL refuser de démarrer si elle est absente. + +--- + +### Requirement 18: Trajet aller/retour + +**User Story:** En tant qu'utilisateur, je veux marquer un trajet comme aller/retour pour compter le double de la distance sans ressaisir le trajet en sens inverse, afin de simplifier la saisie et de gagner de la place à l'affichage. + +#### Acceptance Criteria + +1. WHEN l'utilisateur crée ou modifie un Trajet avec l'indicateur Aller_Retour activé, THE Backend SHALL conserver la distance simple mesurée et exposer une Distance_Effective égale au double de cette distance simple. +2. WHEN l'indicateur Aller_Retour est désactivé, THE Backend SHALL exposer une Distance_Effective égale à la distance simple mesurée. +3. THE Backend SHALL utiliser la Distance_Effective (et non la distance simple) dans tous les agrégats : total du jour, Résumé_Mensuel, Résumé_Annuel, Total_Annuel_Partiel, calculs de barème et Export_PDF. +4. THE Frontend SHALL afficher, dans la liste des Trajets, un indicateur visuel « A/R » dans une colonne dédiée en début de ligne pour chaque Trajet Aller_Retour, en maintenant l'alignement des autres colonnes. +5. WHEN l'utilisateur consulte l'infobulle d'un Trajet Aller_Retour sur la Carte, THE Frontend SHALL faire suivre la distance affichée de la mention « (A/R) ». +6. THE Backend SHALL persister l'indicateur Aller_Retour de chaque Trajet et le conserver lors des modifications ne portant pas sur ce champ. + diff --git a/.kiro/steering/spec-sync.md b/.kiro/steering/spec-sync.md new file mode 100644 index 0000000..c910613 --- /dev/null +++ b/.kiro/steering/spec-sync.md @@ -0,0 +1,38 @@ +# Synchronisation des specs avec le code + +## Règle + +Les documents de spécification du projet — `requirements.md` (exigences fonctionnelles au format EARS) et `design.md` (conception technique) — sont la source de vérité à long terme de ce projet. Ils DOIVENT rester alignés avec le code. + +À chaque évolution fonctionnelle ou technique validée par l'utilisateur (« ça fonctionne », « tu peux committer »), mettre à jour les specs concernées **dans le même commit** que le changement de code, avant de committer. + +## Portée + +- `requirements.md` : mettre à jour dès qu'un comportement observable change (nouvelle fonctionnalité, modification d'un critère d'acceptation, nouveau terme du glossaire, changement de portée comme l'ajout de l'authentification). +- `design.md` : mettre à jour dès qu'une décision d'architecture, un modèle de données, un endpoint d'API ou un composant important change. +- `tasks.md` : facultatif — c'est un journal d'implémentation, pas la source de vérité. Le mettre à jour seulement si l'utilisateur le demande. + +## Ce qui déclenche une mise à jour de spec + +- Nouvelle capacité (ex. aller/retour, authentification, consultation par mois). +- Changement de contrat d'API (nouveau champ DTO, nouvel endpoint, changement de statut HTTP). +- Nouveau modèle de données ou migration EF Core. +- Changement de règle métier (ex. tranches de barème multiples, distance effective doublée). +- Changement de portée du produit (ex. passage de mono-utilisateur sans compte à compte unique authentifié). + +## Ce qui ne déclenche PAS forcément une mise à jour + +- Ajustements purement cosmétiques (couleurs, espacements) sans exigence associée. +- Refactorisations internes sans effet observable et sans changement de décision d'architecture. + +## Format + +- `requirements.md` : conserver le format EARS (WHEN / IF / WHILE / WHERE / THE ... SHALL). Numéroter les nouvelles exigences à la suite. Ajouter au glossaire tout nouveau terme. +- `design.md` : conserver la structure existante (contraintes, décisions d'architecture, modèles, endpoints, composants). Documenter les décisions et les alternatives rejetées. +- Valider le format avec l'outil de validation de spec après édition. + +## Workflow au moment du commit + +1. Implémenter le changement de code et le faire valider par l'utilisateur. +2. Mettre à jour `requirements.md` et/ou `design.md` pour refléter le changement. +3. Committer code + specs ensemble, avec un message décrivant les deux.