feat: réutilisation des coordonnées déjà géocodées (domicile, insertion de trajets)

Expose les coordonnées déjà géocodées dans ProfilDto
(DomicileLatitude/Longitude) et TrajetDto (DepartLatitude/Longitude,
ArriveeLatitude/Longitude) afin que le Frontend les réutilise sans
déclencher un nouveau géocodage BAN :

- Bouton « Utiliser mon domicile » (trajet-form) : utilise désormais
  les coordonnées du Profil au lieu de forcer un re-géocodage.
- Actions « insérer avant/après » (trajet-list) : transmettent les
  coordonnées déjà connues du point de fin repris au formulaire
  d'insertion.

Le résumé mensuel (ResumeService) résout les coordonnées via le
Cache_Adresses pour chaque trajet (nouvelle méthode
ConstruireResumeMensuelAvecCoordonneesAsync) ; le résumé annuel reste
inchangé pour éviter des lookups de cache inutiles sur une vue qui
n'affiche pas le détail des trajets.

Met à jour design.md et design-requirements.md (Requirements 10 et 11)
pour refléter le nouveau contrat d'API.
This commit is contained in:
2026-10-13 20:52:04 +02:00
parent 7695deae10
commit d015ca1c98
13 changed files with 170 additions and 35 deletions
@@ -151,6 +151,7 @@ Le document est organisé **en miroir** des exigences de `requirements.md` : cha
1. IF le matricule reçu dépasse 8 caractères, THEN THE Backend SHALL rejeter le matricule avec une erreur HTTP 400. 1. IF le matricule reçu dépasse 8 caractères, THEN THE Backend SHALL rejeter le matricule avec une erreur HTTP 400.
2. WHEN l'utilisateur sauvegarde une Adresse_Domicile, THE Backend SHALL géocoder l'adresse via l'API BAN pour obtenir ses coordonnées et les stocker dans le Profil ainsi que dans le Cache_Adresses ; si le géocodage BAN échoue, THE Backend SHALL persister l'Adresse_Domicile textuelle sans coordonnées. 2. WHEN l'utilisateur sauvegarde une Adresse_Domicile, THE Backend SHALL géocoder l'adresse via l'API BAN pour obtenir ses coordonnées et les stocker dans le Profil ainsi que dans le Cache_Adresses ; si le géocodage BAN échoue, THE Backend SHALL persister l'Adresse_Domicile textuelle sans coordonnées.
3. 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. 3. 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.
4. WHEN le Backend expose le Profil via `GET /api/profil` ou `PUT /api/profil`, THE Backend SHALL inclure dans le `ProfilDto` les coordonnées déjà géocodées de l'Adresse_Domicile (`DomicileLatitude`, `DomicileLongitude`, null si non résolues), afin que le bouton « Utiliser mon domicile » du Frontend les réutilise sans déclencher un nouveau géocodage BAN.
--- ---
@@ -162,6 +163,7 @@ Le document est organisé **en miroir** des exigences de `requirements.md` : cha
1. WHEN l'utilisateur sélectionne une Adresse_Nommee dans le formulaire d'insertion pour un champ adresse, THE Frontend SHALL associer les coordonnées géographiques (latitude, longitude) de l'Adresse_Nommee pour transmission au Backend. 1. WHEN l'utilisateur sélectionne une Adresse_Nommee dans le formulaire d'insertion pour un champ adresse, THE Frontend SHALL associer les coordonnées géographiques (latitude, longitude) de l'Adresse_Nommee pour transmission au Backend.
2. WHERE l'insertion utilise l'autocomplétion pour une nouvelle adresse, THE Frontend SHALL s'appuyer sur l'API BAN pour proposer les suggestions et récupérer les coordonnées, selon les mêmes modalités techniques que le formulaire de saisie (voir Requirement 1). 2. WHERE l'insertion utilise l'autocomplétion pour une nouvelle adresse, THE Frontend SHALL s'appuyer sur l'API BAN pour proposer les suggestions et récupérer les coordonnées, selon les mêmes modalités techniques que le formulaire de saisie (voir Requirement 1).
3. WHEN le Backend expose un Trajet via `TrajetDto`, THE Backend SHALL inclure les coordonnées déjà géocodées de ses adresses de départ et d'arrivée (`DepartLatitude`, `DepartLongitude`, `ArriveeLatitude`, `ArriveeLongitude`, null si non résolues), afin que les actions « insérer avant » et « insérer après » du Frontend reprenant le point de fin d'un Trajet existant transmettent ces coordonnées au formulaire d'insertion sans déclencher de nouveau géocodage BAN.
--- ---
+18 -6
View File
@@ -239,8 +239,8 @@ src/
- 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) - 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, departLatitude?, departLongitude?, arriveeLatitude?, arriveeLongitude? }` pour pré-remplir les champs depuis le Trajet_Précédent / Trajet_Suivant ; les champs restent modifiables. Lorsque les coordonnées du point repris sont fournies (déjà géocodées), elles sont mémorisées comme pour une sélection BAN, ce qui évite un re-géocodage côté Backend
- Boutons "Utiliser mon domicile" à côté de chaque champ adresse ; pour l'arrivée, si le Commentaire est vide, il est pré-rempli avec « Retour domicile » (jamais de remplacement d'un commentaire déjà saisi) - Boutons "Utiliser mon domicile" à côté de chaque champ adresse ; les coordonnées du domicile (exposées par `ProfilDto.DomicileLatitude/Longitude` si déjà géocodées) sont transmises avec l'adresse, évitant un re-géocodage BAN ; pour l'arrivée, si le Commentaire est vide, il est pré-rempli avec « Retour domicile » (jamais de remplacement d'un commentaire déjà saisi)
- Date par défaut : en mode insertion, la date de la journée du trajet source (`prefill.date`) prime sur la date de la Carte (`dateParDefaut`) - Date par défaut : en mode insertion, la date de la journée du trajet source (`prefill.date`) prime sur la date de la Carte (`dateParDefaut`)
- Timeout de 10 secondes via `TimeoutInterceptor` - Timeout de 10 secondes via `TimeoutInterceptor`
- **Autocomplétion BAN** : appel à `https://api-adresse.data.gouv.fr/search/?q={saisie}&limit=5` dès 3 caractères saisis ; chaque suggestion affiche le libellé + code postal + commune ; à la sélection d'une suggestion, les coordonnées `(lat, lon)` sont stockées dans l'état local du composant et transmises avec le formulaire - **Autocomplétion BAN** : appel à `https://api-adresse.data.gouv.fr/search/?q={saisie}&limit=5` dès 3 caractères saisis ; chaque suggestion affiche le libellé + code postal + commune ; à la sélection d'une suggestion, les coordonnées `(lat, lon)` sont stockées dans l'état local du composant et transmises avec le formulaire
@@ -263,8 +263,8 @@ src/
- Affiche TOUS les trajets du mois, regroupés par journée : chaque groupe/jour a un **en-tête repliable** (jour de la semaine avant la date, ex. « Lundi 15/09/2026 », puis nombre de trajets) ; par défaut tout est replié sauf le dernier jour du mois ayant des trajets. - Affiche TOUS les trajets du mois, regroupés par journée : chaque groupe/jour a un **en-tête repliable** (jour de la semaine avant la date, ex. « Lundi 15/09/2026 », puis nombre de trajets) ; par défaut tout est replié sauf le dernier jour du mois ayant des trajets.
- **Bouton dédié « + » à droite de l'en-tête** (`Output ajouterSurJournee: string`) : ouvre le formulaire de création avec la date de CETTE journée, sans dépendre de la journée affichée sur la Carte (pilotée par le survol) — évite qu'un déplacement de souris à travers plusieurs journées ouvertes ne fasse dériver la date visée en se dirigeant vers le bouton principal d'ajout. Le conteneur fixe `date` sur cette valeur avant d'ouvrir la création (`ouvrirCreationSur`). - **Bouton dédié « + » à droite de l'en-tête** (`Output ajouterSurJournee: string`) : ouvre le formulaire de création avec la date de CETTE journée, sans dépendre de la journée affichée sur la Carte (pilotée par le survol) — évite qu'un déplacement de souris à travers plusieurs journées ouvertes ne fasse dériver la date visée en se dirigeant vers le bouton principal d'ajout. Le conteneur fixe `date` sur cette valeur avant d'ouvrir la création (`ouvrirCreationSur`).
- Ligne par trajet (triée par `Position` croissante au sein du jour) : les **boutons d'action** sont placés **en début de ligne** (après la poignée de glisser-déposer, avant la colonne A/R) et **alignés à droite au sein de leur colonne** pour conserver l'alignement vertical du bouton « insérer après » ; puis colonne dédiée **A/R** pour les allers/retours, commentaire (si présent), adresses (taille réduite), distance effective - Ligne par trajet (triée par `Position` croissante au sein du jour) : les **boutons d'action** sont placés **en début de ligne** (après la poignée de glisser-déposer, avant la colonne A/R) et **alignés à droite au sein de leur colonne** pour conserver l'alignement vertical du bouton « insérer après » ; puis colonne dédiée **A/R** pour les allers/retours, commentaire (si présent), adresses (taille réduite), distance effective
- Actions par trajet (regroupées en début de ligne) : modifier (→ `TrajetFormComponent` pré-rempli en < 500 ms), supprimer (→ dialog de confirmation), **insérer avant/après** — l'insérer après ouvre `TrajetFormComponent` en mode insertion avec `adresseDepart` = point de fin du trajet courant (son arrivée, ou son **départ** si le trajet est un aller/retour), la **date de la journée** du trajet, `positionApres` = Position du trajet courant - Actions par trajet (regroupées en début de ligne) : modifier (→ `TrajetFormComponent` pré-rempli en < 500 ms), supprimer (→ dialog de confirmation), **insérer avant/après** — l'insérer après ouvre `TrajetFormComponent` en mode insertion avec `adresseDepart` = point de fin du trajet courant (son arrivée, ou son **départ** si le trajet est un aller/retour), la **date de la journée** du trajet, `positionApres` = Position du trajet courant. Les coordonnées du point de fin repris (exposées par `TrajetDto.DepartLatitude/Longitude` ou `ArriveeLatitude/Longitude` selon le sens) sont transmises dans le pré-remplissage pour éviter un re-géocodage BAN de cette adresse déjà connue
- Si le trajet n'est pas le dernier de la journée : action **insérer avant le suivant** disponible, pré-remplissant l'adresse de départ avec le point de fin du trajet courant (départ si aller/retour) et l'adresse d'arrivée avec le départ du trajet suivant, en 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 l'adresse de départ avec le point de fin du trajet courant (départ si aller/retour) et l'adresse d'arrivée avec le départ du trajet suivant (et leurs coordonnées déjà géocodées, selon le même principe), en calculant la position d'insertion `(P_courant + P_suivant) / 2`
- Méthode publique `ouvrirJour(dateIso)` : déplie une journée donnée (appelée par le conteneur après ajout/insertion pour rendre le trajet visible) ; `ouvrirToutesLesJournees()` déplie tous les groupes ; `aDesJourneesFermees()` conditionne l'affichage du bouton « Ouvrir toutes les journées » - Méthode publique `ouvrirJour(dateIso)` : déplie une journée donnée (appelée par le conteneur après ajout/insertion pour rendre le trajet visible) ; `ouvrirToutesLesJournees()` déplie tous les groupes ; `aDesJourneesFermees()` conditionne l'affichage du bouton « Ouvrir toutes les journées »
- `@Input jourAffiche` : la journée affichée sur la Carte ; le groupe correspondant est mis en surbrillance (`estJourAffiche`) - `@Input jourAffiche` : la journée affichée sur la Carte ; le groupe correspondant est mis en surbrillance (`estJourAffiche`)
- **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` - **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`
@@ -1019,7 +1019,15 @@ public record TrajetDto(
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 bool AllerRetour, // information aller/retour conservée par trajet
// Coordonnées déjà géocodées (Cache_Adresses pour le résumé mensuel ; résultat du calcul de
// distance pour la création/modification), exposées pour que le Frontend les réutilise SANS
// re-géocoder (ex. « Insérer avant/après » qui reprend l'adresse d'un trajet existant). null
// si l'adresse concernée n'a pas de coordonnées connues.
double? DepartLatitude = null,
double? DepartLongitude = null,
double? ArriveeLatitude = null,
double? ArriveeLongitude = null
); );
// Réordonnancement manuel des trajets d'une journée // Réordonnancement manuel des trajets d'une journée
@@ -1097,7 +1105,11 @@ public record ProfilDto(
string? Prenom, string? Prenom,
string? Matricule, string? Matricule,
string? AdresseDomicile, string? AdresseDomicile,
bool DomicileGeocode bool DomicileGeocode,
// Coordonnées du domicile déjà géocodées (null si non résolues), exposées pour que le
// Frontend les réutilise lors du bouton « Utiliser mon domicile » SANS re-géocoder.
double? DomicileLatitude,
double? DomicileLongitude
); );
public record SaveProfilDto( public record SaveProfilDto(
@@ -6,7 +6,12 @@ public record ProfilDto(
string? Prenom, string? Prenom,
string? Matricule, string? Matricule,
string? AdresseDomicile, string? AdresseDomicile,
bool DomicileGeocode bool DomicileGeocode,
// Coordonnées du domicile déjà géocodées (null si non résolues). Exposées au
// frontend pour qu'il puisse les transmettre lors de la saisie d'un trajet
// (bouton « Utiliser mon domicile »), évitant un re-géocodage BAN côté backend.
double? DomicileLatitude,
double? DomicileLongitude
); );
public record SaveProfilDto( public record SaveProfilDto(
@@ -43,7 +43,14 @@ public record TrajetDto(
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 bool AllerRetour, // information aller/retour conservée par trajet
// Coordonnées déjà géocodées (via Cache_Adresses ou calcul de distance), exposées pour
// permettre leur réutilisation sans re-géocodage (ex. bouton « Insérer avant/après » qui
// reprend l'adresse d'un trajet existant). null si l'adresse n'a pas de coordonnées connues.
double? DepartLatitude = null,
double? DepartLongitude = null,
double? ArriveeLatitude = null,
double? ArriveeLongitude = null
); );
// Trajet enrichi pour la carte // Trajet enrichi pour la carte
@@ -145,5 +145,6 @@ public class ProfilService : IProfilService
} }
private static ProfilDto ToDto(Profil p) => private static ProfilDto ToDto(Profil p) =>
new(p.Nom, p.Prenom, p.Matricule, p.AdresseDomicile, p.DomicileGeocode); new(p.Nom, p.Prenom, p.Matricule, p.AdresseDomicile, p.DomicileGeocode,
p.DomicileLatitude, p.DomicileLongitude);
} }
@@ -16,11 +16,16 @@ public class ResumeService : IResumeService
private readonly AppDbContext _context; private readonly AppDbContext _context;
private readonly IPrecisionDistanceService _precisionDistanceService; private readonly IPrecisionDistanceService _precisionDistanceService;
private readonly IAdresseCacheService _adresseCacheService;
public ResumeService(AppDbContext context, IPrecisionDistanceService precisionDistanceService) public ResumeService(
AppDbContext context,
IPrecisionDistanceService precisionDistanceService,
IAdresseCacheService adresseCacheService)
{ {
_context = context; _context = context;
_precisionDistanceService = precisionDistanceService; _precisionDistanceService = precisionDistanceService;
_adresseCacheService = adresseCacheService;
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -33,7 +38,10 @@ public class ResumeService : IResumeService
var precision = await _precisionDistanceService.ObtenirAsync(cancellationToken); var precision = await _precisionDistanceService.ObtenirAsync(cancellationToken);
var trajets = await ChargerTrajetsDuMoisAsync(annee, mois, vehiculeId, cancellationToken); var trajets = await ChargerTrajetsDuMoisAsync(annee, mois, vehiculeId, cancellationToken);
return ConstruireResumeMensuel(annee, mois, trajets, precision); // Coordonnées incluses : ce résumé alimente l'écran de consultation/saisie des Trajets,
// dont l'insertion (« Insérer avant/après ») réutilise les coordonnées déjà géocodées
// pour éviter un re-géocodage BAN.
return await ConstruireResumeMensuelAvecCoordonneesAsync(annee, mois, trajets, precision, cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -115,6 +123,33 @@ public class ResumeService : IResumeService
return new ResumeMensuelDto(annee, mois, trajetsDto, totalKm); return new ResumeMensuelDto(annee, mois, trajetsDto, totalKm);
} }
/// <summary>
/// Variante de <see cref="ConstruireResumeMensuel"/> enrichissant chaque <see cref="TrajetDto"/>
/// des coordonnées de ses adresses, résolues via le Cache_Adresses (même mécanisme que
/// <c>TrajetsController.GetJournee</c>). Réservée au résumé MENSUEL (écran de consultation/
/// saisie des Trajets, dont l'insertion réutilise ces coordonnées) : le résumé ANNUEL continue
/// d'utiliser <see cref="ConstruireResumeMensuel"/> sans lookup, les totaux annuels n'ayant pas
/// besoin du détail des coordonnées de chaque trajet (évite 2×N lookups cache inutiles).
/// </summary>
private async Task<ResumeMensuelDto> ConstruireResumeMensuelAvecCoordonneesAsync(
int annee, int mois, IReadOnlyList<Trajet> trajets, int precision, CancellationToken cancellationToken)
{
var distancesArrondies = trajets.Select(t => (t, distance: DistanceEffectiveArrondie(t, precision))).ToList();
var trajetsDto = new List<TrajetDto>(distancesArrondies.Count);
foreach (var (t, distance) in distancesArrondies)
{
var depart = await _adresseCacheService.ObtenirParAdresseAsync(t.AdresseDepart, cancellationToken);
var arrivee = await _adresseCacheService.ObtenirParAdresseAsync(t.AdresseArrivee, cancellationToken);
trajetsDto.Add(ToDto(t, distance, depart?.Latitude, depart?.Longitude, arrivee?.Latitude, arrivee?.Longitude));
}
var totalKm = distancesArrondies.Sum(x => x.distance ?? 0m);
return new ResumeMensuelDto(annee, mois, trajetsDto, totalKm);
}
/// <summary> /// <summary>
/// Calcule la Distance_Effective_Arrondie d'un trajet : arrondi (Precision_Distance) appliqué /// Calcule la Distance_Effective_Arrondie d'un trajet : arrondi (Precision_Distance) appliqué
/// à la distance SIMPLE brute AVANT doublement aller/retour (Exigence 4.1, 4.2, 4.3). null si la /// à la distance SIMPLE brute AVANT doublement aller/retour (Exigence 4.1, 4.2, 4.3). null si la
@@ -149,7 +184,13 @@ public class ResumeService : IResumeService
} }
} }
private static TrajetDto ToDto(Trajet t, decimal? distanceEffectiveArrondie) => private static TrajetDto ToDto(
Trajet t,
decimal? distanceEffectiveArrondie,
double? departLatitude = null,
double? departLongitude = null,
double? arriveeLatitude = null,
double? arriveeLongitude = null) =>
new( new(
t.Id, t.Id,
t.Date, t.Date,
@@ -162,5 +203,9 @@ public class ResumeService : IResumeService
t.Position, t.Position,
t.VehiculeId, t.VehiculeId,
t.PuissanceFiscale, t.PuissanceFiscale,
t.AllerRetour); t.AllerRetour,
departLatitude,
departLongitude,
arriveeLatitude,
arriveeLongitude);
} }
@@ -98,7 +98,7 @@ public class TrajetService : ITrajetService
} }
return new TrajetCreationResult( return new TrajetCreationResult(
Trajet: ToDto(trajet), Trajet: ToDto(trajet, distanceResult),
Avertissement: distanceResult.Avertissement, Avertissement: distanceResult.Avertissement,
Message: distanceResult.Avertissement ? distanceResult.Message : null); Message: distanceResult.Avertissement ? distanceResult.Message : null);
} }
@@ -224,7 +224,7 @@ public class TrajetService : ITrajetService
} }
return new TrajetUpdateResult( return new TrajetUpdateResult(
Trajet: ToDto(trajet), Trajet: ToDto(trajet, distanceResult),
Avertissement: avertissement, Avertissement: avertissement,
Message: messageAvertissement); Message: messageAvertissement);
} }
@@ -438,7 +438,15 @@ public class TrajetService : ITrajetService
return parsed; return parsed;
} }
private static TrajetDto ToDto(Trajet t) => /// <summary>
/// Construit le <see cref="TrajetDto"/> de retour, enrichi des coordonnées déjà résolues
/// par <paramref name="distanceResult"/> (calcul de distance effectué dans le même appel),
/// afin que le frontend puisse les réutiliser sans re-géocoder (ex. insertion d'un trajet
/// reprenant cette adresse). <c>null</c> si aucun calcul n'a eu lieu dans cet appel (adresses
/// inchangées à la modification) : aucune régression, ce DTO n'étant pas la source utilisée
/// pour l'insertion (voir ResumeService, qui résout les coordonnées via le Cache_Adresses).
/// </summary>
private static TrajetDto ToDto(Trajet t, DistanceCalculationResult? distanceResult = null) =>
new( new(
t.Id, t.Id,
t.Date, t.Date,
@@ -450,5 +458,9 @@ public class TrajetService : ITrajetService
t.Position, t.Position,
t.VehiculeId, t.VehiculeId,
t.PuissanceFiscale, t.PuissanceFiscale,
t.AllerRetour); t.AllerRetour,
distanceResult?.DepartLatitude,
distanceResult?.DepartLongitude,
distanceResult?.ArriveeLatitude,
distanceResult?.ArriveeLongitude);
} }
@@ -43,7 +43,11 @@ public class PdfServiceIntegrationTests : IDisposable
var precisionDistanceService = new PrecisionDistanceService(_context); var precisionDistanceService = new PrecisionDistanceService(_context);
var baremeService = new BaremeService(_context); var baremeService = new BaremeService(_context);
var resumeService = new ResumeService(_context, precisionDistanceService);
var adresseCacheRepository = new AdresseCacheRepository(_context);
var adresseCacheService = new AdresseCacheService(adresseCacheRepository, _context);
var resumeService = new ResumeService(_context, precisionDistanceService, adresseCacheService);
var montantRegleService = new MontantRegleService(_context, baremeService, precisionDistanceService); var montantRegleService = new MontantRegleService(_context, baremeService, precisionDistanceService);
var remboursementService = new RemboursementService( var remboursementService = new RemboursementService(
_context, baremeService, montantRegleService, precisionDistanceService); _context, baremeService, montantRegleService, precisionDistanceService);
@@ -52,8 +56,6 @@ public class PdfServiceIntegrationTests : IDisposable
var vehiculeService = new VehiculeService(vehiculeRepository, _context); var vehiculeService = new VehiculeService(vehiculeRepository, _context);
var totauxService = new TotauxService(_context, vehiculeService, precisionDistanceService); var totauxService = new TotauxService(_context, vehiculeService, precisionDistanceService);
var adresseCacheRepository = new AdresseCacheRepository(_context);
var adresseCacheService = new AdresseCacheService(adresseCacheRepository, _context);
var banClientMock = new Mock<IBANClient>(); var banClientMock = new Mock<IBANClient>();
var profilService = new ProfilService( var profilService = new ProfilService(
_context, banClientMock.Object, adresseCacheService, NullLogger<ProfilService>.Instance); _context, banClientMock.Object, adresseCacheService, NullLogger<ProfilService>.Instance);
@@ -44,7 +44,9 @@ public sealed class PrecisionDistanceTestHarness : IDisposable
PrecisionDistanceService = new SchotzIadKmTracker.Api.Services.PrecisionDistanceService(Context); PrecisionDistanceService = new SchotzIadKmTracker.Api.Services.PrecisionDistanceService(Context);
BaremeService = new SchotzIadKmTracker.Api.Services.BaremeService(Context); BaremeService = new SchotzIadKmTracker.Api.Services.BaremeService(Context);
ResumeService = new SchotzIadKmTracker.Api.Services.ResumeService(Context, PrecisionDistanceService); var adresseCacheRepository = new SchotzIadKmTracker.Api.Repositories.AdresseCacheRepository(Context);
var adresseCacheService = new SchotzIadKmTracker.Api.Services.AdresseCacheService(adresseCacheRepository, Context);
ResumeService = new SchotzIadKmTracker.Api.Services.ResumeService(Context, PrecisionDistanceService, adresseCacheService);
MontantRegleService = new SchotzIadKmTracker.Api.Services.MontantRegleService( MontantRegleService = new SchotzIadKmTracker.Api.Services.MontantRegleService(
Context, BaremeService, PrecisionDistanceService); Context, BaremeService, PrecisionDistanceService);
} }
@@ -7,6 +7,10 @@ export interface Profil {
matricule: string | null; matricule: string | null;
adresseDomicile: string | null; adresseDomicile: string | null;
domicileGeocode: boolean; domicileGeocode: boolean;
// Coordonnées du domicile déjà géocodées (null si non résolues). Permettent de
// transmettre les coordonnées lors de « Utiliser mon domicile » sans re-géocoder.
domicileLatitude: number | null;
domicileLongitude: number | null;
} }
/** Création / mise à jour du profil — miroir de SaveProfilDto. */ /** Création / mise à jour du profil — miroir de SaveProfilDto. */
@@ -44,6 +44,12 @@ export interface Trajet {
vehiculeId: number; vehiculeId: number;
puissanceFiscale: number; puissanceFiscale: number;
allerRetour: boolean; // information aller/retour conservée par trajet allerRetour: boolean; // information aller/retour conservée par trajet
// Coordonnées déjà géocodées (via le Cache_Adresses), si connues ; null sinon. Permettent de
// réutiliser l'adresse d'un trajet existant (ex. « Insérer avant/après ») sans re-géocoder.
departLatitude?: number | null;
departLongitude?: number | null;
arriveeLatitude?: number | null;
arriveeLongitude?: number | null;
} }
/** /**
@@ -73,6 +73,15 @@ export interface TrajetInsertionPrefill {
* le nouveau trajet soit rattaché à la BONNE journée (Exigences 11.1–11.4). * le nouveau trajet soit rattaché à la BONNE journée (Exigences 11.1–11.4).
*/ */
date?: string | null; date?: string | null;
/**
* Coordonnées du point de fin repris (adresse de départ/arrivée du Trajet_Précédent
* ou Trajet_Suivant), si déjà géocodées. Transmises pour éviter un re-géocodage BAN
* côté backend lors de la création du trajet inséré.
*/
departLatitude?: number | null;
departLongitude?: number | null;
arriveeLatitude?: number | null;
arriveeLongitude?: number | null;
} }
/** Identifie lequel des deux champs adresse est concerné. */ /** Identifie lequel des deux champs adresse est concerné. */
@@ -427,9 +436,23 @@ export class TrajetFormComponent implements OnInit, OnDestroy {
// Backend) : source fiable, pas d'avertissement de coordonnées manquantes. // Backend) : source fiable, pas d'avertissement de coordonnées manquantes.
if (this.prefill.adresseDepart?.trim()) { if (this.prefill.adresseDepart?.trim()) {
this.sourceFiableDepart.set(true); this.sourceFiableDepart.set(true);
// Coordonnées déjà géocodées transmises par le conteneur (trajet-list) si connues :
// on les mémorise pour éviter un re-géocodage BAN côté backend.
if (this.prefill.departLatitude != null && this.prefill.departLongitude != null) {
this.coordDepart.set({
latitude: this.prefill.departLatitude,
longitude: this.prefill.departLongitude
});
}
} }
if (this.prefill.adresseArrivee?.trim()) { if (this.prefill.adresseArrivee?.trim()) {
this.sourceFiableArrivee.set(true); this.sourceFiableArrivee.set(true);
if (this.prefill.arriveeLatitude != null && this.prefill.arriveeLongitude != null) {
this.coordArrivee.set({
latitude: this.prefill.arriveeLatitude,
longitude: this.prefill.arriveeLongitude
});
}
} }
if (this.prefill.vehiculeId != null) { if (this.prefill.vehiculeId != null) {
this.vehiculeId = this.prefill.vehiculeId; this.vehiculeId = this.prefill.vehiculeId;
@@ -509,20 +532,20 @@ export class TrajetFormComponent implements OnInit, OnDestroy {
if (!adresse) { if (!adresse) {
return; return;
} }
// On remplit le libellé du domicile ; les coordonnées seront résolues côté // On remplit le libellé du domicile ET on transmet ses coordonnées déjà
// backend via le Cache_Adresses ou la BAN (Exigence 2.4). On ne dispose pas // géocodées (exposées dans le Profil), afin d'éviter un re-géocodage BAN côté
// des coordonnées du domicile dans le profil exposé au frontend. // backend à chaque trajet. Si le domicile n'a pas été géocodé (coordonnées
const controle = champ === 'depart' ? this.departCtrl : this.arriveeCtrl; // absentes), on retombe sur null : le Backend résoudra alors l'adresse (Exigence 2.4).
controle.setValue(adresse); const profil = this.profil();
controle.markAsTouched(); const coord =
if (champ === 'depart') { profil?.domicileLatitude != null && profil?.domicileLongitude != null
this.coordDepart.set(null); ? { latitude: profil.domicileLatitude, longitude: profil.domicileLongitude }
this.sourceFiableDepart.set(true); : null;
this.suggestionsDepart.set([]); // `appliquerAdresse` remplit le champ, mémorise les coordonnées, marque la source
} else { // comme fiable et vide les suggestions du champ concerné.
this.coordArrivee.set(null); this.appliquerAdresse(champ, adresse, coord);
this.sourceFiableArrivee.set(true);
this.suggestionsArrivee.set([]); if (champ === 'arrivee') {
// Confort de saisie : un trajet dont l'arrivée est le domicile est typiquement // Confort de saisie : un trajet dont l'arrivée est le domicile est typiquement
// un retour. On pré-remplit le commentaire avec « Retour domicile » s'il est // un retour. On pré-remplit le commentaire avec « Retour domicile » s'il est
// encore vide (on ne remplace jamais un commentaire déjà saisi). // encore vide (on ne remplace jamais un commentaire déjà saisi).
@@ -343,9 +343,15 @@ export class TrajetListComponent {
*/ */
insererApres(trajet: Trajet): void { insererApres(trajet: Trajet): void {
const pointDeFin = trajet.allerRetour ? trajet.adresseDepart : trajet.adresseArrivee; const pointDeFin = trajet.allerRetour ? trajet.adresseDepart : trajet.adresseArrivee;
// Coordonnées du point de fin (déjà géocodées si connues) : évite un re-géocodage
// BAN côté backend pour une adresse déjà résolue.
const latitudeFin = trajet.allerRetour ? trajet.departLatitude : trajet.arriveeLatitude;
const longitudeFin = trajet.allerRetour ? trajet.departLongitude : trajet.arriveeLongitude;
this.insertAfter.emit({ this.insertAfter.emit({
prefill: { prefill: {
adresseDepart: pointDeFin, adresseDepart: pointDeFin,
departLatitude: latitudeFin,
departLongitude: longitudeFin,
vehiculeId: trajet.vehiculeId, vehiculeId: trajet.vehiculeId,
// Rattache l'insertion à la JOURNÉE du trajet concerné (et non à la date de // Rattache l'insertion à la JOURNÉE du trajet concerné (et non à la date de
// la carte), pour éviter les erreurs de saisie de date. // la carte), pour éviter les erreurs de saisie de date.
@@ -369,11 +375,19 @@ export class TrajetListComponent {
// Point de fin du trajet courant : son arrivée, ou son départ s'il est aller/retour // Point de fin du trajet courant : son arrivée, ou son départ s'il est aller/retour
// (le véhicule est alors revenu à son point d'origine). // (le véhicule est alors revenu à son point d'origine).
const finCourant = courant.allerRetour ? courant.adresseDepart : courant.adresseArrivee; const finCourant = courant.allerRetour ? courant.adresseDepart : courant.adresseArrivee;
// Coordonnées du point de fin du trajet courant et du départ du trajet suivant
// (déjà géocodées si connues) : évite un re-géocodage BAN côté backend.
const latitudeFinCourant = courant.allerRetour ? courant.departLatitude : courant.arriveeLatitude;
const longitudeFinCourant = courant.allerRetour ? courant.departLongitude : courant.arriveeLongitude;
this.insertBetween.emit({ this.insertBetween.emit({
prefill: { prefill: {
adresseDepart: finCourant, adresseDepart: finCourant,
departLatitude: latitudeFinCourant,
departLongitude: longitudeFinCourant,
// Le trajet suivant part toujours de son adresse de départ (indépendant de l'A/R). // Le trajet suivant part toujours de son adresse de départ (indépendant de l'A/R).
adresseArrivee: suivant.adresseDepart, adresseArrivee: suivant.adresseDepart,
arriveeLatitude: suivant.departLatitude,
arriveeLongitude: suivant.departLongitude,
vehiculeId: courant.vehiculeId, vehiculeId: courant.vehiculeId,
// Journée du trajet courant (insertion au sein de la même journée). // Journée du trajet courant (insertion au sein de la même journée).
date: (courant.date ?? '').slice(0, 10) date: (courant.date ?? '').slice(0, 10)