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
@@ -30,6 +30,11 @@
}
</div>
<div class="champ-case">
<input id="seSouvenir" type="checkbox" formControlName="seSouvenir" />
<label for="seSouvenir">Se souvenir de moi</label>
</div>
<div class="champ">
<label for="motDePasse">Mot de passe</label>
<app-password-input
@@ -38,6 +38,24 @@ h1 {
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 {
color: #c62828;
margin-top: 0.25rem;
@@ -9,6 +9,9 @@ import { PasswordInputComponent } from '../../../shared/components/password-inpu
import { NotificationService } from '../../../core/services/notification.service';
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.
*
@@ -18,6 +21,14 @@ import { LoginDto } from '../../../core/models';
* pas encore configuré. Sur 401, un message générique « Identifiants invalides. »
* 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
* (`soumissionEnCours`, `erreur`, `compteExiste`).
*
@@ -51,9 +62,12 @@ export class LoginComponent implements OnInit {
readonly compteExiste = signal(true);
ngOnInit(): void {
const emailMemorise = this.lireEmailMemorise();
this.form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
motDePasse: ['', [Validators.required, Validators.minLength(8)]]
email: [emailMemorise ?? '', [Validators.required, Validators.email]],
motDePasse: ['', [Validators.required, Validators.minLength(8)]],
seSouvenir: [emailMemorise !== null]
});
this.authService.compteExiste().subscribe({
@@ -78,8 +92,9 @@ export class LoginComponent implements OnInit {
}
const valeur = this.form.getRawValue();
const email = (valeur.email ?? '').trim();
const dto: LoginDto = {
email: (valeur.email ?? '').trim(),
email,
motDePasse: valeur.motDePasse ?? ''
};
@@ -88,6 +103,8 @@ export class LoginComponent implements OnInit {
this.authService.login(dto).subscribe({
next: () => {
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.
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>
<dd>{{ (totaux()?.totalJour ?? 0) | number: formatKm() }} km</dd>
</div>
<div class="panneau-totaux__ligne">
<div class="panneau-totaux__ligne panneau-totaux__ligne--fort">
<dt>Total du mois</dt>
<dd>{{ (totaux()?.totalMois ?? 0) | number: formatKm() }} km</dd>
</div>
<div class="panneau-totaux__ligne panneau-totaux__ligne--fort">
<div class="panneau-totaux__ligne">
<dt>Total annuel partiel</dt>
<dd>{{ (totaux()?.totalAnnuelPartiel ?? 0) | number: formatKm() }} km</dd>
</div>
@@ -24,7 +24,7 @@
<div class="panneau-totaux__previsionnel">
<div class="panneau-totaux__ligne panneau-totaux__ligne--fort">
<span class="panneau-totaux__previsionnel-libelle">
Remboursement indicatif
Remboursement indicatif pour ce mois
<button
type="button"
class="panneau-totaux__aide"
@@ -44,7 +44,7 @@
</div>
</div>
<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">
Montant prévisionnel annuel
<button