Connexion « Se souvenir de moi » et ajustements panneau Totaux

- Login : case à cocher « Se souvenir de moi » sous le champ email ;
  mémorisation de l'identifiant (email uniquement) dans le localStorage
  du navigateur, pré-remplissage à la visite suivante sur le même
  navigateur, effacement si décochée ; erreurs de stockage ignorées
- Panneau Totaux : « Total du mois » en gras et « Total annuel partiel »
  non gras ; « Remboursement indicatif » renommé « Remboursement
  indicatif pour ce mois » ; « Montant prévisionnel annuel » non gras
- Steering : nouvelle règle imposant les réponses en français
- Mise à jour requirements.md (Req 17 + glossaire), design-requirements.md
  (Req 17 technique) et design.md (LoginComponent, PanneauTotaux)
This commit is contained in:
2026-10-03 10:02:21 +02:00
parent 3b4050c127
commit 5b72e21c9d
8 changed files with 110 additions and 10 deletions
@@ -236,6 +236,11 @@ Le document est organisé **en miroir** des exigences de `requirements.md` : cha
6. 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. 6. 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.
7. WHEN l'utilisateur suit un Jeton_Reinitialisation valide et fournit un nouveau Mot_De_Passe valide, THE Backend SHALL remplacer le hachage du Mot_De_Passe du Compte et invalider le Jeton_Reinitialisation. 7. WHEN l'utilisateur suit un Jeton_Reinitialisation valide et fournit un nouveau Mot_De_Passe valide, THE Backend SHALL remplacer le hachage du Mot_De_Passe du Compte et invalider le Jeton_Reinitialisation.
8. 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. 8. 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.
9. WHEN la case « Se souvenir de moi » est cochée lors d'une connexion réussie, THE LoginComponent SHALL écrire l'Adresse_Email saisie dans le `localStorage` du navigateur sous la clé `schotziad.login.emailMemorise` via `setItem`, et SHALL n'y stocker que l'Adresse_Email (jamais le Mot_De_Passe).
10. WHEN la case « Se souvenir de moi » est décochée lors d'une connexion réussie, THE LoginComponent SHALL supprimer la clé `schotziad.login.emailMemorise` du `localStorage` du navigateur via `removeItem`.
11. WHEN le LoginComponent est initialisé (`ngOnInit`), IF la clé `schotziad.login.emailMemorise` est présente et non vide dans le `localStorage`, THEN THE LoginComponent SHALL pré-remplir le champ Adresse_Email avec cette valeur et cocher la case « Se souvenir de moi ».
12. WHERE l'Identifiant_Mémorisé est conservé dans le `localStorage` du navigateur, THE LoginComponent SHALL s'appuyer sur la portée propre du `localStorage` (limitée à l'origine — scheme, host et port — et au navigateur courant) pour réaliser l'absence de pré-remplissage sur une autre machine ou un autre navigateur tant qu'aucune connexion « se souvenir » n'y a eu lieu.
13. IF un accès au `localStorage` échoue (navigation privée, quota dépassé ou stockage désactivé), THEN THE LoginComponent SHALL intercepter l'erreur et l'ignorer silencieusement, la mémorisation de l'Identifiant_Mémorisé restant sans effet sans faire échouer la connexion ni le chargement de la page de connexion.
--- ---
+4 -3
View File
@@ -328,8 +328,9 @@ src/
- 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) - 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, calculés par sommation de distances déjà arrondies selon Precision_Distance (voir spec `precision-distances-km`, Requirement 5) - Tous les totaux affichés à 2 décimales, calculés par sommation de distances déjà arrondies selon Precision_Distance (voir spec `precision-distances-km`, Requirement 5)
- Affiche le libellé **« Remboursement indicatif »** (renommé, anciennement « Indication de versement (80%) ») présentant le Montant_Previsionnel_Mensuel (80%, calculé sur le seul mois affiché) et, en dessous, une ligne « Montant prévisionnel annuel » présentant le Montant_Previsionnel_Annuel (100%, sur le Total_Annuel_Partiel) — détail complet du calcul dans la spec `precision-distances-km`, Requirements 7 et 8 - Affiche le libellé **« Remboursement indicatif pour ce mois »** (renommé, anciennement « Indication de versement (80%) ») présentant le Montant_Previsionnel_Mensuel (80%, calculé sur le seul mois affiché) et, en dessous, une ligne « Montant prévisionnel annuel » présentant le Montant_Previsionnel_Annuel (100%, sur le Total_Annuel_Partiel) — détail complet du calcul dans la spec `precision-distances-km`, Requirements 7 et 8
- **Infobulles d'aide** : le texte explicatif de chacune de ces deux lignes (« Remboursement indicatif » et « Montant prévisionnel annuel »), auparavant affiché en permanence sous le montant, est retiré du flux visuel et remplacé par un bouton icône (SVG inline `help-circle`, 16×16, `aria-label` dédié) affiché juste après le libellé ; le texte explicatif est porté par l'attribut HTML `title` de ce bouton et s'affiche via l'infobulle native du navigateur au survol ou au focus clavier de l'icône - **Mise en forme (emphase)** : les informations relatives au mois affiché sont mises en avant en gras (classe `--fort`) — la ligne « Total du mois » et la ligne « Remboursement indicatif pour ce mois » ; à l'inverse, la ligne « Total annuel partiel » et la ligne « Montant prévisionnel annuel » ne sont PAS en gras
- **Infobulles d'aide** : le texte explicatif de chacune de ces deux lignes (« Remboursement indicatif pour ce mois » et « Montant prévisionnel annuel »), auparavant affiché en permanence sous le montant, est retiré du flux visuel et remplacé par un bouton icône (SVG inline `help-circle`, 16×16, `aria-label` dédié) affiché juste après le libellé ; le texte explicatif est porté par l'attribut HTML `title` de ce bouton et s'affiche via l'infobulle native du navigateur au survol ou au focus clavier de l'icône
**`ParametresApplicationComponent`** **`ParametresApplicationComponent`**
- Nouveau composant du Sous_Menu_Application de la Page_Parametrage, positionné entre « Compte » et « Véhicules » - Nouveau composant du Sous_Menu_Application de la Page_Parametrage, positionné entre « Compte » et « Véhicules »
@@ -374,7 +375,7 @@ src/
**Composants d'authentification** (`LoginComponent`, `RegisterComponent`, `ForgotPasswordComponent`, `ResetPasswordComponent`) **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. - `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é ». - `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é ». Propose également une case à cocher « Se souvenir de moi » placée sous le champ email : à une connexion réussie, si elle est cochée, l'email (identifiant uniquement, jamais le mot de passe) est conservé dans le `localStorage` du navigateur sous la clé `schotziad.login.emailMemorise` et pré-rempli (case cochée) aux visites suivantes sur le même navigateur ; si elle est décochée, la clé est supprimée. Le stockage `localStorage` étant propre à l'origine et au navigateur, aucun pré-remplissage n'a lieu sur une autre machine ou un autre navigateur, et les erreurs d'accès au stockage sont ignorées silencieusement. Détail dans `requirements.md` Requirement 17 (critères 14-18) et `design-requirements.md` Requirement 17 (critères 9-13).
- `ForgotPasswordComponent` : saisie de l'email → `POST /api/auth/forgot-password` (lien envoyé par email, ou journalisé en repli). - `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`. - `ResetPasswordComponent` : nouveau mot de passe via le jeton reçu → `POST /api/auth/reset-password`.
@@ -59,6 +59,7 @@ L'application permet de gérer un profil (identité, matricule, adresse de domic
- **Mot_De_Passe** : Le secret d'au moins 8 caractères associé au Compte, jamais conservé en clair. - **Mot_De_Passe** : Le secret d'au moins 8 caractères associé au Compte, jamais conservé en clair.
- **Session** : L'état authentifié d'un utilisateur, établi à la connexion et vérifié à chaque accès protégé. - **Session** : L'état authentifié d'un utilisateur, établi à la connexion et vérifié à chaque accès protégé.
- **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. - **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.
- **Identifiant_Mémorisé** : L'Adresse_Email de connexion conservée par le navigateur de l'utilisateur, propre à la machine et au navigateur utilisés, lorsque l'utilisateur a demandé « Se souvenir de moi » lors d'une connexion réussie ; elle sert à pré-remplir le champ Adresse_Email lors des visites ultérieures sur ce même navigateur. Le Mot_De_Passe n'en fait jamais partie.
- **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. - **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. - **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.
- **Document_Specification** : Un document de conception du projet consultable en lecture seule dans l'Application : les Exigences (requirements), la Conception technique (design) et le Plan de tâches (tasks). - **Document_Specification** : Un document de conception du projet consultable en lecture seule dans l'Application : les Exigences (requirements), la Conception technique (design) et le Plan de tâches (tasks).
@@ -447,6 +448,11 @@ L'application permet de gérer un profil (identité, matricule, adresse de domic
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. 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. 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 Mot_De_Passe du Compte et invalider le Jeton_Reinitialisation. 12. 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 Mot_De_Passe du Compte et invalider le Jeton_Reinitialisation.
13. 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. 13. 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.
14. THE Frontend SHALL afficher sur la page de connexion, sous le champ Adresse_Email, une case à cocher « Se souvenir de moi ».
15. WHEN l'utilisateur réussit une connexion alors que la case « Se souvenir de moi » est cochée, THE Frontend SHALL conserver l'Adresse_Email saisie comme Identifiant_Mémorisé sur le navigateur utilisé, et pré-remplir le champ Adresse_Email avec cet Identifiant_Mémorisé en présentant la case « Se souvenir de moi » cochée lors de la visite suivante de la page de connexion sur ce même navigateur.
16. WHILE aucune connexion « Se souvenir de moi » n'a été réalisée sur le navigateur utilisé, THE Frontend SHALL présenter la page de connexion avec le champ Adresse_Email vide et la case « Se souvenir de moi » décochée.
17. WHEN l'utilisateur réussit une connexion alors que la case « Se souvenir de moi » est décochée, THE Frontend SHALL effacer tout Identifiant_Mémorisé existant sur le navigateur utilisé, de sorte que l'Adresse_Email devra être ressaisie lors de la visite suivante.
18. THE Frontend SHALL ne jamais conserver le Mot_De_Passe au titre de la fonctionnalité « Se souvenir de moi ».
--- ---
+19
View File
@@ -0,0 +1,19 @@
# Langue des échanges
## Règle
Toujours répondre à l'utilisateur **en français**, quelle que soit la langue de la question ou du contenu technique affiché.
Cela s'applique à :
- toutes les réponses et explications dans le chat ;
- les messages de synthèse de fin de tâche ;
- les questions de clarification posées à l'utilisateur ;
- les messages de commit Git ;
- les commentaires de code rédigés pour ce projet ;
- le contenu rédactionnel des documents de spec (`requirements.md`, `design-requirements.md`, `design.md`, `tasks.md`).
## Exceptions
- Ne pas traduire les termes techniques, mots-clés de langage, noms d'API, identifiants de code, commandes shell ni le contenu littéral des fichiers (le code reste tel quel).
- Les mots-clés EARS des specs (WHEN / IF / WHILE / WHERE / THE ... SHALL) restent en anglais, conformément au format.
@@ -30,6 +30,11 @@
} }
</div> </div>
<div class="champ-case">
<input id="seSouvenir" type="checkbox" formControlName="seSouvenir" />
<label for="seSouvenir">Se souvenir de moi</label>
</div>
<div class="champ"> <div class="champ">
<label for="motDePasse">Mot de passe</label> <label for="motDePasse">Mot de passe</label>
<app-password-input <app-password-input
@@ -38,6 +38,24 @@ h1 {
border-color: #c62828; border-color: #c62828;
} }
.champ-case {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.champ-case input {
width: 1rem;
height: 1rem;
cursor: pointer;
}
.champ-case label {
cursor: pointer;
user-select: none;
}
.erreur { .erreur {
color: #c62828; color: #c62828;
margin-top: 0.25rem; margin-top: 0.25rem;
@@ -9,6 +9,9 @@ import { PasswordInputComponent } from '../../../shared/components/password-inpu
import { NotificationService } from '../../../core/services/notification.service'; import { NotificationService } from '../../../core/services/notification.service';
import { LoginDto } from '../../../core/models'; import { LoginDto } from '../../../core/models';
/** Clé localStorage de l'identifiant mémorisé via « Se souvenir de moi ». */
const CLE_EMAIL_MEMORISE = 'schotziad.login.emailMemorise';
/** /**
* Écran de connexion (`/login`) — Etape de session de l'authentification. * Écran de connexion (`/login`) — Etape de session de l'authentification.
* *
@@ -18,6 +21,14 @@ import { LoginDto } from '../../../core/models';
* pas encore configuré. Sur 401, un message générique « Identifiants invalides. » * pas encore configuré. Sur 401, un message générique « Identifiants invalides. »
* est affiché (Exigence 2.2). * est affiché (Exigence 2.2).
* *
* Case « Se souvenir de moi » : lorsqu'elle est cochée au moment d'une connexion
* réussie, l'identifiant (email) est stocké dans le `localStorage` du navigateur
* et pré-rempli aux prochaines visites sur cette même machine/navigateur. Sur un
* autre poste ou navigateur, aucun pré-remplissage n'a lieu tant qu'une connexion
* « se souvenir » n'y a pas été faite. Si la case est décochée lors d'une
* connexion, l'identifiant mémorisé est effacé et devra être ressaisi ensuite.
* Le mot de passe n'est jamais stocké.
*
* Contrainte zoneless : tout état async est exposé via des signals * Contrainte zoneless : tout état async est exposé via des signals
* (`soumissionEnCours`, `erreur`, `compteExiste`). * (`soumissionEnCours`, `erreur`, `compteExiste`).
* *
@@ -51,9 +62,12 @@ export class LoginComponent implements OnInit {
readonly compteExiste = signal(true); readonly compteExiste = signal(true);
ngOnInit(): void { ngOnInit(): void {
const emailMemorise = this.lireEmailMemorise();
this.form = this.fb.group({ this.form = this.fb.group({
email: ['', [Validators.required, Validators.email]], email: [emailMemorise ?? '', [Validators.required, Validators.email]],
motDePasse: ['', [Validators.required, Validators.minLength(8)]] motDePasse: ['', [Validators.required, Validators.minLength(8)]],
seSouvenir: [emailMemorise !== null]
}); });
this.authService.compteExiste().subscribe({ this.authService.compteExiste().subscribe({
@@ -78,8 +92,9 @@ export class LoginComponent implements OnInit {
} }
const valeur = this.form.getRawValue(); const valeur = this.form.getRawValue();
const email = (valeur.email ?? '').trim();
const dto: LoginDto = { const dto: LoginDto = {
email: (valeur.email ?? '').trim(), email,
motDePasse: valeur.motDePasse ?? '' motDePasse: valeur.motDePasse ?? ''
}; };
@@ -88,6 +103,8 @@ export class LoginComponent implements OnInit {
this.authService.login(dto).subscribe({ this.authService.login(dto).subscribe({
next: () => { next: () => {
this.soumissionEnCours.set(false); this.soumissionEnCours.set(false);
// Mémorisation de l'identifiant selon le choix « Se souvenir de moi ».
this.appliquerMemorisation(valeur.seSouvenir === true, email);
// La cible protégée oriente ensuite vers l'onboarding si nécessaire. // La cible protégée oriente ensuite vers l'onboarding si nécessaire.
this.router.navigate(['/consultation']); this.router.navigate(['/consultation']);
}, },
@@ -105,4 +122,33 @@ export class LoginComponent implements OnInit {
} }
}); });
} }
/**
* Lit l'identifiant mémorisé dans le `localStorage`, ou `null` si absent ou
* si le stockage est indisponible (mode privé, quota, etc.).
*/
private lireEmailMemorise(): string | null {
try {
const valeur = localStorage.getItem(CLE_EMAIL_MEMORISE);
return valeur && valeur.trim() !== '' ? valeur : null;
} catch {
return null;
}
}
/**
* Stocke l'identifiant si « Se souvenir de moi » est coché, sinon l'efface.
* Toute erreur d'accès au stockage est ignorée silencieusement.
*/
private appliquerMemorisation(seSouvenir: boolean, email: string): void {
try {
if (seSouvenir && email !== '') {
localStorage.setItem(CLE_EMAIL_MEMORISE, email);
} else {
localStorage.removeItem(CLE_EMAIL_MEMORISE);
}
} catch {
// Stockage indisponible : la mémorisation est simplement sans effet.
}
}
} }
@@ -10,11 +10,11 @@
<dt>Total du jour</dt> <dt>Total du jour</dt>
<dd>{{ (totaux()?.totalJour ?? 0) | number: formatKm() }} km</dd> <dd>{{ (totaux()?.totalJour ?? 0) | number: formatKm() }} km</dd>
</div> </div>
<div class="panneau-totaux__ligne"> <div class="panneau-totaux__ligne panneau-totaux__ligne--fort">
<dt>Total du mois</dt> <dt>Total du mois</dt>
<dd>{{ (totaux()?.totalMois ?? 0) | number: formatKm() }} km</dd> <dd>{{ (totaux()?.totalMois ?? 0) | number: formatKm() }} km</dd>
</div> </div>
<div class="panneau-totaux__ligne panneau-totaux__ligne--fort"> <div class="panneau-totaux__ligne">
<dt>Total annuel partiel</dt> <dt>Total annuel partiel</dt>
<dd>{{ (totaux()?.totalAnnuelPartiel ?? 0) | number: formatKm() }} km</dd> <dd>{{ (totaux()?.totalAnnuelPartiel ?? 0) | number: formatKm() }} km</dd>
</div> </div>
@@ -24,7 +24,7 @@
<div class="panneau-totaux__previsionnel"> <div class="panneau-totaux__previsionnel">
<div class="panneau-totaux__ligne panneau-totaux__ligne--fort"> <div class="panneau-totaux__ligne panneau-totaux__ligne--fort">
<span class="panneau-totaux__previsionnel-libelle"> <span class="panneau-totaux__previsionnel-libelle">
Remboursement indicatif Remboursement indicatif pour ce mois
<button <button
type="button" type="button"
class="panneau-totaux__aide" class="panneau-totaux__aide"
@@ -44,7 +44,7 @@
</div> </div>
</div> </div>
<div class="panneau-totaux__previsionnel"> <div class="panneau-totaux__previsionnel">
<div class="panneau-totaux__ligne panneau-totaux__ligne--fort"> <div class="panneau-totaux__ligne">
<span class="panneau-totaux__previsionnel-libelle"> <span class="panneau-totaux__previsionnel-libelle">
Montant prévisionnel annuel Montant prévisionnel annuel
<button <button