UX consultation, trajets aller/retour et rafraîchissement des totaux

- Totaux : rafraîchissement immédiat du panneau (jour/mois/année + montant
  prévisionnel) après création/modification/suppression/réordonnancement d'un
  trajet, sans changer de jour (ViewChild + méthode publique rafraichir()).
- Trajets Aller/Retour : case à cocher au formulaire ; la distance comptée est
  doublée (distance simple stockée + booléen AllerRetour, distance effective
  utilisée dans tous les agrégats : totaux, résumés, barème, régularisation, PDF).
  Migration TrajetAllerRetour. Badge « A/R » dans une colonne dédiée en début de
  ligne pour garder date/adresses alignées.
- Mise en page de la page Trajets : vue à hauteur fixe où seule la liste défile ;
  colonne gauche figée (sélecteur de date + navigation, totaux, carte) et bande
  fixe (véhicule + bouton d'ajout) au-dessus de la liste. Layout applicatif global
  en pleine hauteur (app.component).
- run-dev.sh : chargement de la clé JWT depuis .env (ou secret de dev aléatoire) et
  Auth__FrontendBaseUrl sur le frontend dev ; rappel du lien de reset journalisé.

Build backend (solution) et frontend (production) OK.
This commit is contained in:
2026-09-15 12:33:10 +02:00
parent dd36bbc425
commit 09c24d5f59
24 changed files with 759 additions and 109 deletions
@@ -104,11 +104,13 @@ public class TrajetsController : ControllerBase
trajet.Date,
trajet.AdresseDepart,
trajet.AdresseArrivee,
trajet.Distance,
// Distance EFFECTIVE (doublée si aller/retour) exposée au frontend.
trajet.DistanceEffective,
trajet.Commentaire,
trajet.Position,
trajet.VehiculeId,
trajet.PuissanceFiscale));
trajet.PuissanceFiscale,
trajet.AllerRetour));
}
/// <summary>
@@ -227,12 +229,14 @@ public class TrajetsController : ControllerBase
trajet.Date,
trajet.AdresseDepart,
trajet.AdresseArrivee,
trajet.Distance ?? 0m,
// Distance EFFECTIVE (doublée si aller/retour), cohérente avec la liste.
trajet.DistanceEffective ?? 0m,
depart?.Latitude,
depart?.Longitude,
arrivee?.Latitude,
arrivee?.Longitude,
geometrie));
geometrie,
trajet.AllerRetour));
}
return Ok(resultats);
@@ -0,0 +1,393 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SchotzIadKmTracker.Api.Data;
#nullable disable
namespace SchotzIadKmTracker.Api.Data.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260915073357_TrajetAllerRetour")]
partial class TrajetAllerRetour
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.8");
modelBuilder.Entity("SchotzIadKmTracker.Api.Models.Entities.AdresseCache", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Adresse")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("DerniereUtilisation")
.HasColumnType("TEXT");
b.Property<string>("GeometrieJson")
.HasColumnType("TEXT");
b.Property<double>("Latitude")
.HasColumnType("REAL");
b.Property<double>("Longitude")
.HasColumnType("REAL");
b.HasKey("Id");
b.HasIndex("Adresse")
.IsUnique();
b.ToTable("AdresseCache", (string)null);
});
modelBuilder.Entity("SchotzIadKmTracker.Api.Models.Entities.AdresseNommee", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Adresse")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Denomination")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("DenominationNormalisee")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<bool>("GeocodageReussi")
.HasColumnType("INTEGER");
b.Property<double?>("Latitude")
.HasColumnType("REAL");
b.Property<double?>("Longitude")
.HasColumnType("REAL");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex(new[] { "DenominationNormalisee" }, "UX_AdresseNommee_Denomination")
.IsUnique();
b.ToTable("AdressesNommees", (string)null);
});
modelBuilder.Entity("SchotzIadKmTracker.Api.Models.Entities.Compte", b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("MotDePasseHash")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex(new[] { "Email" }, "UX_Comptes_Email")
.IsUnique();
b.ToTable("Comptes", (string)null);
});
modelBuilder.Entity("SchotzIadKmTracker.Api.Models.Entities.MontantRegle", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Annee")
.HasColumnType("INTEGER");
b.Property<int>("Mois")
.HasColumnType("INTEGER");
b.Property<decimal>("Montant")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<int>("VehiculeId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("VehiculeId");
b.HasIndex(new[] { "Annee", "Mois", "VehiculeId" }, "UX_MontantsRegles_Periode_Vehicule")
.IsUnique();
b.ToTable("MontantsRegles", null, t =>
{
t.HasCheckConstraint("CK_MontantsRegles_Mois", "\"Mois\" BETWEEN 1 AND 12");
t.HasCheckConstraint("CK_MontantsRegles_Montant", "\"Montant\" >= 0");
});
});
modelBuilder.Entity("SchotzIadKmTracker.Api.Models.Entities.Profil", b =>
{
b.Property<int>("Id")
.HasColumnType("INTEGER");
b.Property<string>("AdresseDomicile")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<bool>("DomicileGeocode")
.HasColumnType("INTEGER");
b.Property<double?>("DomicileLatitude")
.HasColumnType("REAL");
b.Property<double?>("DomicileLongitude")
.HasColumnType("REAL");
b.Property<string>("Matricule")
.HasMaxLength(8)
.HasColumnType("TEXT");
b.Property<string>("Nom")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("Prenom")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex(new[] { "Id" }, "UX_Profil_Singleton")
.IsUnique();
b.ToTable("Profil", (string)null);
});
modelBuilder.Entity("SchotzIadKmTracker.Api.Models.Entities.ResetPasswordToken", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("Expiration")
.HasColumnType("TEXT");
b.Property<string>("TokenHash")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime?>("UsedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex(new[] { "Expiration" }, "IX_ResetTokens_Expiration");
b.HasIndex(new[] { "TokenHash" }, "UX_ResetTokens_TokenHash")
.IsUnique();
b.ToTable("ResetPasswordTokens", (string)null);
});
modelBuilder.Entity("SchotzIadKmTracker.Api.Models.Entities.Trajet", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AdresseArrivee")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("AdresseDepart")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<bool>("AllerRetour")
.HasColumnType("INTEGER");
b.Property<string>("Commentaire")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateOnly>("Date")
.HasColumnType("TEXT");
b.Property<decimal?>("Distance")
.HasColumnType("TEXT");
b.Property<string>("GeometrieJson")
.HasColumnType("TEXT");
b.Property<decimal>("Position")
.HasColumnType("TEXT");
b.Property<int>("PuissanceFiscale")
.HasColumnType("INTEGER");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<int>("VehiculeId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex(new[] { "Date" }, "IX_Trajets_Date");
b.HasIndex(new[] { "Date", "Position" }, "IX_Trajets_Date_Position");
b.HasIndex(new[] { "VehiculeId" }, "IX_Trajets_Vehicule");
b.ToTable("Trajets", null, t =>
{
t.HasCheckConstraint("CK_Trajets_Distance", "\"Distance\" IS NULL OR \"Distance\" > 0");
t.HasCheckConstraint("CK_Trajets_PuissanceFiscale", "\"PuissanceFiscale\" > 0");
});
});
modelBuilder.Entity("SchotzIadKmTracker.Api.Models.Entities.TrancheBareme", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Annee")
.HasColumnType("INTEGER");
b.Property<decimal>("Base")
.HasColumnType("TEXT");
b.Property<decimal>("Coefficient")
.HasColumnType("TEXT");
b.Property<decimal?>("KmMax")
.HasColumnType("TEXT");
b.Property<decimal>("KmMin")
.HasColumnType("TEXT");
b.Property<int>("PuissanceFiscale")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex(new[] { "Annee", "PuissanceFiscale" }, "IX_TranchesBareme_Annee_PF");
b.ToTable("TranchesBareme", null, t =>
{
t.HasCheckConstraint("CK_TranchesBareme_Km", "\"KmMax\" IS NULL OR CAST(\"KmMin\" AS REAL) < CAST(\"KmMax\" AS REAL)");
});
});
modelBuilder.Entity("SchotzIadKmTracker.Api.Models.Entities.Vehicule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateOnly>("DateEffet")
.HasColumnType("TEXT");
b.Property<string>("Denomination")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("Plaque")
.IsRequired()
.HasMaxLength(15)
.HasColumnType("TEXT");
b.Property<int>("PuissanceFiscale")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex(new[] { "DateEffet" }, "IX_Vehicules_DateEffet");
b.ToTable("Vehicules", null, t =>
{
t.HasCheckConstraint("CK_Vehicules_PuissanceFiscale", "\"PuissanceFiscale\" > 0");
});
});
modelBuilder.Entity("SchotzIadKmTracker.Api.Models.Entities.MontantRegle", b =>
{
b.HasOne("SchotzIadKmTracker.Api.Models.Entities.Vehicule", "Vehicule")
.WithMany()
.HasForeignKey("VehiculeId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Vehicule");
});
modelBuilder.Entity("SchotzIadKmTracker.Api.Models.Entities.Trajet", b =>
{
b.HasOne("SchotzIadKmTracker.Api.Models.Entities.Vehicule", "Vehicule")
.WithMany("Trajets")
.HasForeignKey("VehiculeId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Vehicule");
});
modelBuilder.Entity("SchotzIadKmTracker.Api.Models.Entities.Vehicule", b =>
{
b.Navigation("Trajets");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SchotzIadKmTracker.Api.Data.Migrations
{
/// <inheritdoc />
public partial class TrajetAllerRetour : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "AllerRetour",
table: "Trajets",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AllerRetour",
table: "Trajets");
}
}
}
@@ -242,6 +242,9 @@ namespace SchotzIadKmTracker.Api.Data.Migrations
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<bool>("AllerRetour")
.HasColumnType("INTEGER");
b.Property<string>("Commentaire")
.HasMaxLength(500)
.HasColumnType("TEXT");
@@ -10,7 +10,8 @@ public record CreateTrajetDto(
double? ArriveeLatitude, // null si adresse saisie manuellement sans BAN
double? ArriveeLongitude,
string? Commentaire, // optionnel, max 500 car.
int? VehiculeId // optionnel ; null → véhicule courant à la date du trajet
int? VehiculeId, // optionnel ; null → véhicule courant à la date du trajet
bool AllerRetour = false // true → distance comptée doublée (aller/retour)
);
// Mise à jour
@@ -23,7 +24,8 @@ public record UpdateTrajetDto(
double? ArriveeLatitude,
double? ArriveeLongitude,
string? Commentaire,
int? VehiculeId // optionnel ; null → véhicule courant à la date du trajet
int? VehiculeId, // optionnel ; null → véhicule courant à la date du trajet
bool AllerRetour = false // true → distance comptée doublée (aller/retour)
);
// Réponse trajet
@@ -32,11 +34,16 @@ public record TrajetDto(
DateOnly Date,
string AdresseDepart,
string AdresseArrivee,
decimal? Distance, // km, 2 décimales ; null si la distance n'a pas pu être calculée (Exigence 2.4)
// IMPORTANT : ce champ expose la DISTANCE EFFECTIVE (distance comptée), c'est-à-dire la
// distance simple doublée lorsque AllerRetour vaut true. Le doublement est appliqué à la
// source du DTO afin que la liste, la carte et le PDF affichent directement la distance
// comptée sans logique supplémentaire. null si la distance n'a pas pu être calculée (Exigence 2.4).
decimal? Distance,
string? Commentaire,
decimal Position, // rang dans la journée
int VehiculeId,
int PuissanceFiscale
int PuissanceFiscale,
bool AllerRetour // information aller/retour conservée par trajet
);
// Trajet enrichi pour la carte
@@ -45,12 +52,14 @@ public record TrajetCarteDto(
DateOnly Date,
string AdresseDepart,
string AdresseArrivee,
// Distance EFFECTIVE (doublée si AllerRetour), pour cohérence avec l'affichage de la liste.
decimal Distance,
double? DepartLatitude,
double? DepartLongitude,
double? ArriveeLatitude,
double? ArriveeLongitude,
string? GeometrieJson // GeoJSON LineString ou null
string? GeometrieJson, // GeoJSON LineString ou null
bool AllerRetour = false // information aller/retour conservée par trajet
);
// Réordonnancement manuel des trajets d'une journée
@@ -25,6 +25,23 @@ public class Trajet
/// </summary>
public decimal? Distance { get; set; }
/// <summary>
/// Indique un trajet aller/retour. Lorsqu'il vaut <c>true</c>, la distance COMPTÉE est le
/// double de la distance simple (aller) stockée dans <see cref="Distance"/> ; la ligne de
/// trajet reste unique. Valeur par défaut <c>false</c> : les trajets existants conservent
/// leur comportement (distance simple).
/// </summary>
public bool AllerRetour { get; set; }
/// <summary>
/// Distance comptée (effective), doublée lorsque <see cref="AllerRetour"/> vaut <c>true</c>.
/// Propriété calculée NON MAPPÉE : elle n'est pas persistée mais dérivée de
/// <see cref="Distance"/> et <see cref="AllerRetour"/>. <c>null</c> lorsque la distance simple
/// est inconnue (non calculable), afin de préserver le comportement existant dans les sommes.
/// </summary>
[System.ComponentModel.DataAnnotations.Schema.NotMapped]
public decimal? DistanceEffective => Distance.HasValue ? Distance.Value * (AllerRetour ? 2m : 1m) : (decimal?)null;
/// <summary>Commentaire optionnel (0–500 caractères).</summary>
[MaxLength(500)]
public string? Commentaire { get; set; }
@@ -94,9 +94,11 @@ public class MontantRegleService : IMontantRegleService
.Where(t => t.VehiculeId == vehiculeId
&& t.Date.Year == annee
&& t.Date.Month <= mois)
.Select(t => t.Distance)
// On ramène la distance simple et l'indicateur aller/retour, puis on somme la
// distance EFFECTIVE (doublée si A/R) côté client (null → 0).
.Select(t => new { t.Distance, t.AllerRetour })
.ToListAsync(cancellationToken);
var totalAnnuelPartiel = distancesPartielles.Sum(d => d ?? 0m);
var totalAnnuelPartiel = distancesPartielles.Sum(x => x.Distance.GetValueOrDefault() * (x.AllerRetour ? 2m : 1m));
// Barème plein appliqué au total partiel (sans minoration).
var bareme = await _baremeService.CalculerMontantAsync(
@@ -423,7 +423,11 @@ public class PdfService : IPdfService
foreach (var trajet in modele.Trajets)
{
var motif = string.IsNullOrWhiteSpace(trajet.Commentaire) ? "—" : trajet.Commentaire!;
var itineraire = $"{trajet.AdresseDepart} → {trajet.AdresseArrivee}";
// Suffixe « (A/R) » pour les trajets aller/retour ; la distance affichée est
// déjà la distance effective (doublée) portée par le TrajetDto.
var itineraire = trajet.AllerRetour
? $"{trajet.AdresseDepart} → {trajet.AdresseArrivee} (A/R)"
: $"{trajet.AdresseDepart} → {trajet.AdresseArrivee}";
var km = trajet.Distance ?? 0m;
table.Cell().Element(Cellule).Text(trajet.Date.ToString("dd/MM/yyyy", FrCulture));
@@ -38,12 +38,14 @@ public class RemboursementService : IRemboursementService
var trajetsAnnee = await _context.Trajets
.AsNoTracking()
.Where(t => t.VehiculeId == vehiculeId && t.Date.Year == annee)
.Select(t => new { t.Date.Month, t.Distance })
// On ramène la distance simple et l'indicateur aller/retour, puis on agrège la
// distance EFFECTIVE (doublée si A/R) côté client.
.Select(t => new { t.Date.Month, t.Distance, t.AllerRetour })
.ToListAsync(cancellationToken);
var kmMoisMap = trajetsAnnee
.GroupBy(t => t.Month)
.ToDictionary(g => g.Key, g => g.Sum(t => t.Distance ?? 0m));
.ToDictionary(g => g.Key, g => g.Sum(t => (t.Distance ?? 0m) * (t.AllerRetour ? 2m : 1m)));
// Total kilométrique annuel COMPLET (détermine la tranche — Exigence 14.5, Property 27).
var totalKmAnnuel = kmMoisMap.Values.Sum();
@@ -98,7 +98,8 @@ public class ResumeService : IResumeService
private static ResumeMensuelDto ConstruireResumeMensuel(int annee, int mois, IReadOnlyList<Trajet> trajets)
{
var trajetsDto = trajets.Select(ToDto).ToList();
var totalKm = Arrondir(trajets.Sum(t => t.Distance ?? 0m));
// Total = somme des distances EFFECTIVES (doublées si aller/retour), null → 0 (Property 1).
var totalKm = Arrondir(trajets.Sum(t => t.DistanceEffective ?? 0m));
return new ResumeMensuelDto(annee, mois, trajetsDto, totalKm);
}
@@ -130,9 +131,11 @@ public class ResumeService : IResumeService
t.Date,
t.AdresseDepart,
t.AdresseArrivee,
t.Distance,
// Distance EFFECTIVE (doublée si aller/retour), alignée sur le TrajetDto commun.
t.DistanceEffective,
t.Commentaire,
t.Position,
t.VehiculeId,
t.PuissanceFiscale);
t.PuissanceFiscale,
t.AllerRetour);
}
@@ -29,24 +29,30 @@ public class TotauxService : ITotauxService
var trajetsAnnee = await _context.Trajets
.AsNoTracking()
.Where(t => t.VehiculeId == vehiculeCible && t.Date.Year == date.Year)
.Select(t => new { t.Date, Distance = (decimal?)t.Distance ?? 0m })
// Projection : on ramène la distance simple et l'indicateur aller/retour, puis on
// calcule la distance EFFECTIVE (doublée si A/R) côté client lors des sommes.
.Select(t => new { t.Date, t.Distance, t.AllerRetour })
.ToListAsync(cancellationToken);
// TotalJour : somme des distances de la journée sélectionnée (Exigence 12.7).
// Distance comptée (effective) : distance simple × 2 si aller/retour, null → 0.
static decimal Effective(decimal? distance, bool allerRetour) =>
distance.GetValueOrDefault() * (allerRetour ? 2m : 1m);
// TotalJour : somme des distances effectives de la journée sélectionnée (Exigence 12.7).
var totalJour = trajetsAnnee
.Where(t => t.Date == date)
.Sum(t => t.Distance);
.Sum(t => Effective(t.Distance, t.AllerRetour));
// TotalMois : somme des distances du mois de la date sélectionnée (Exigence 12.7).
// TotalMois : somme des distances effectives du mois de la date sélectionnée (Exigence 12.7).
var totalMois = trajetsAnnee
.Where(t => t.Date.Month == date.Month)
.Sum(t => t.Distance);
.Sum(t => Effective(t.Distance, t.AllerRetour));
// Total_Annuel_Partiel : cumul de l'année jusqu'au mois sélectionné INCLUS,
// exclusion des mois strictement postérieurs (Exigence 12.8, Property 17).
var totalAnnuelPartiel = trajetsAnnee
.Where(t => t.Date.Month <= date.Month)
.Sum(t => t.Distance);
.Sum(t => Effective(t.Distance, t.AllerRetour));
return new TotauxDto(
Date: date,
@@ -49,6 +49,9 @@ public class TrajetService : ITrajetService
AdresseDepart = dto.AdresseDepart,
AdresseArrivee = dto.AdresseArrivee,
Commentaire = dto.Commentaire,
// Aller/retour : recopie du choix de saisie ; le doublement de la distance est dérivé
// (DistanceEffective) et ne modifie pas la distance simple stockée.
AllerRetour = dto.AllerRetour,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
};
@@ -184,6 +187,9 @@ public class TrajetService : ITrajetService
trajet.AdresseDepart = dto.AdresseDepart;
trajet.AdresseArrivee = dto.AdresseArrivee;
trajet.Commentaire = dto.Commentaire;
// Aller/retour : champ simple, modifiable même sans changement d'adresse (aucun
// recalcul ORS nécessaire, le doublement est dérivé de DistanceEffective).
trajet.AllerRetour = dto.AllerRetour;
trajet.UpdatedAt = DateTime.UtcNow;
if (trajet.Date != nouvelleDate)
@@ -438,9 +444,11 @@ public class TrajetService : ITrajetService
t.Date,
t.AdresseDepart,
t.AdresseArrivee,
t.Distance,
// Distance EFFECTIVE (doublée si aller/retour) exposée au frontend.
t.DistanceEffective,
t.Commentaire,
t.Position,
t.VehiculeId,
t.PuissanceFiscale);
t.PuissanceFiscale,
t.AllerRetour);
}