- Simple indication de versement, calculée à 80% du barème sur le total - annuel partiel. Le montant réellement versé est le montant réglé que - vous saisissez. + Simple indication de versement, calculée à 80% du barème sur le mois + affiché. Le montant réellement versé est le montant réglé que vous + saisissez. +
+ ++ Projection à 100% du barème sur le total annuel partiel, sans + minoration. Cette projection ne se substitue pas au montant réel + calculé en fin d'année.
Enregistrement en cours…
+ } + ++ L'arrondi appliqué aux distances est toujours supérieur (jamais vers le bas), quelle que + soit la précision choisie. +
+ diff --git a/frontend/src/app/features/parametres-application/parametres-application.component.scss b/frontend/src/app/features/parametres-application/parametres-application.component.scss new file mode 100644 index 0000000..e1cb6df --- /dev/null +++ b/frontend/src/app/features/parametres-application/parametres-application.component.scss @@ -0,0 +1,65 @@ +.parametres-application { + padding: 1.5rem; + max-width: 720px; + margin: 0 auto; +} + +h1 { + font-size: 1.4rem; + margin-bottom: 1rem; +} + +h2 { + font-size: 1.1rem; + margin: 1.5rem 0 0.75rem; +} + +.choix-precision { + display: flex; + gap: 0.75rem; +} + +.btn-precision { + padding: 0.55rem 1.25rem; + background: #fff; + color: var(--couleur-bleu); + border: 1px solid var(--couleur-bleu); + border-radius: 6px; + cursor: pointer; + font-size: 0.95rem; + min-width: 3rem; + + &:hover:not(:disabled) { + border-color: var(--couleur-orange); + color: var(--couleur-orange); + } +} + +.btn-precision--actif { + background: var(--couleur-bleu); + color: #fff; + border-color: var(--couleur-bleu); + + &:hover:not(:disabled) { + background: var(--couleur-orange); + border-color: var(--couleur-orange); + color: #fff; + } +} + +.btn-precision:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +.enregistrement-en-cours { + margin-top: 0.75rem; + color: #777; + font-style: italic; +} + +.aide { + margin-top: 1rem; + color: #555; + font-size: 0.9rem; +} diff --git a/frontend/src/app/features/parametres-application/parametres-application.component.ts b/frontend/src/app/features/parametres-application/parametres-application.component.ts new file mode 100644 index 0000000..f3f3194 --- /dev/null +++ b/frontend/src/app/features/parametres-application/parametres-application.component.ts @@ -0,0 +1,63 @@ +import { CommonModule } from '@angular/common'; +import { Component, OnInit, inject, signal } from '@angular/core'; + +import { NotificationService } from '../../core/services/notification.service'; +import { ParametresApplicationService } from '../../core/services/parametres-application.service'; + +/** + * Paramétrage global de l'application : précision d'arrondi des distances (km). + * + * - Présélectionne la valeur courante au chargement (Exigence 2.3). + * - Applique le choix immédiatement (optimiste) puis confirme ou restaure/notifie + * en cas d'échec (Exigences 2.4, 2.5). + */ +@Component({ + selector: 'app-parametres-application', + standalone: true, + imports: [CommonModule], + templateUrl: './parametres-application.component.html', + styleUrl: './parametres-application.component.scss' +}) +export class ParametresApplicationComponent implements OnInit { + private readonly parametresService = inject(ParametresApplicationService); + private readonly notification = inject(NotificationService); + + /** Valeur sélectionnée (0, 1 ou 2 décimales). */ + readonly precisionSelectionnee = signal<0 | 1 | 2>(2); + + /** true pendant l'appel d'enregistrement. */ + readonly enregistrement = signal(false); + + /** Valeurs de précision proposées (0, 1 ou 2 décimales). */ + readonly precisionsDisponibles: readonly (0 | 1 | 2)[] = [0, 1, 2]; + + ngOnInit(): void { + this.precisionSelectionnee.set(this.parametresService.precisionDistance()); + } + + /** + * Sélectionne une nouvelle précision et l'enregistre côté serveur. + * Mise à jour optimiste du signal local, restaurée en cas d'échec (Exigence 2.5). + */ + choisir(valeur: 0 | 1 | 2): void { + if (valeur === this.precisionSelectionnee() || this.enregistrement()) { + return; + } + + const precedente = this.precisionSelectionnee(); + this.precisionSelectionnee.set(valeur); + this.enregistrement.set(true); + + this.parametresService.definirPrecisionDistance(valeur).subscribe({ + next: () => { + this.enregistrement.set(false); + this.notification.success('Précision des distances enregistrée.'); + }, + error: () => { + this.enregistrement.set(false); + this.precisionSelectionnee.set(precedente); + this.notification.error("La modification n'a pas pu être enregistrée."); + } + }); + } +} diff --git a/frontend/src/app/features/trajet-list/trajet-list.component.ts b/frontend/src/app/features/trajet-list/trajet-list.component.ts index 7fba7ca..928d356 100644 --- a/frontend/src/app/features/trajet-list/trajet-list.component.ts +++ b/frontend/src/app/features/trajet-list/trajet-list.component.ts @@ -16,6 +16,7 @@ import { import { ReordonnerTrajets, Trajet, TrajetPosition } from '../../core/models'; import { NotificationService } from '../../core/services/notification.service'; +import { ParametresApplicationService } from '../../core/services/parametres-application.service'; import { TrajetService } from '../../core/services/trajet.service'; import { ServeurIndisponibleError } from '../../core/interceptors/timeout.interceptor'; import { ConfirmDialogComponent } from '../../shared/components/confirm-dialog/confirm-dialog.component'; @@ -87,6 +88,7 @@ export interface GroupeJour { export class TrajetListComponent { private readonly trajetService = inject(TrajetService); private readonly notifications = inject(NotificationService); + private readonly parametresService = inject(ParametresApplicationService); /** * Trajets du mois. Ils sont regroupés par journée (`groupes`), chaque groupe @@ -295,12 +297,12 @@ export class TrajetListComponent { return index === groupe.trajets.length - 1; } - /** Distance formatée à 2 décimales, ou « — » si non calculée (Exigence 2.4). */ + /** Distance formatée selon la précision courante, ou « — » si non calculée (Exigence 2.4). */ distanceAffichee(trajet: Trajet): string { if (trajet.distance == null) { return '—'; } - return `${trajet.distance.toFixed(2)} km`; + return `${trajet.distance.toFixed(this.parametresService.precisionDistance())} km`; } /** Identité stable pour le suivi de la liste (drag & drop). */ diff --git a/frontend/src/app/shared/utils/arrondir-distance.ts b/frontend/src/app/shared/utils/arrondir-distance.ts new file mode 100644 index 0000000..3c4b7dc --- /dev/null +++ b/frontend/src/app/shared/utils/arrondir-distance.ts @@ -0,0 +1,21 @@ +/** + * Arrondit une distance brute (km) au supérieur (ceiling) à la précision donnée. + * Miroir exact de PrecisionDistanceService.ArrondirDistance côté backend. + */ +export function arrondirDistance(distanceBrute: number, precision: 0 | 1 | 2): number { + const facteur = Math.pow(10, precision); + return Math.ceil(distanceBrute * facteur) / facteur; +} + +/** Distance_Effective_Arrondie : arrondi de la distance simple PUIS doublement si aller/retour. */ +export function distanceEffectiveArrondie( + distanceBrute: number | null, + allerRetour: boolean, + precision: 0 | 1 | 2 +): number | null { + if (distanceBrute == null) { + return null; + } + const arrondie = arrondirDistance(distanceBrute, precision); + return allerRetour ? arrondie * 2 : arrondie; +}