- 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.
245 lines
10 KiB
C#
245 lines
10 KiB
C#
using System.Globalization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using SchotzIadKmTracker.Api.Models.DTOs;
|
|
using SchotzIadKmTracker.Api.Repositories;
|
|
using SchotzIadKmTracker.Api.Services;
|
|
using SchotzIadKmTracker.Api.Services.Exceptions;
|
|
|
|
namespace SchotzIadKmTracker.Api.Controllers;
|
|
|
|
/// <summary>
|
|
/// Endpoints CRUD des Trajets, réordonnancement manuel et récupération des trajets d'une
|
|
/// journée enrichis des coordonnées et géométries issues du Cache_Adresses (Exigences 1.3,
|
|
/// 5.5, 5.6, 8.7, 9.3).
|
|
/// </summary>
|
|
[ApiController]
|
|
[Route("api/trajets")]
|
|
public class TrajetsController : ControllerBase
|
|
{
|
|
/// <summary>
|
|
/// En-tête de réponse portant l'avertissement lorsqu'un Trajet a été enregistré sans
|
|
/// distance calculable (Exigence 2.4). Le corps reste le <see cref="TrajetDto"/> attendu.
|
|
/// </summary>
|
|
private const string WarningHeader = "X-Trajet-Warning";
|
|
|
|
private readonly ITrajetService _trajetService;
|
|
private readonly ITrajetRepository _trajetRepository;
|
|
private readonly IAdresseCacheService _adresseCacheService;
|
|
|
|
public TrajetsController(
|
|
ITrajetService trajetService,
|
|
ITrajetRepository trajetRepository,
|
|
IAdresseCacheService adresseCacheService)
|
|
{
|
|
_trajetService = trajetService;
|
|
_trajetRepository = trajetRepository;
|
|
_adresseCacheService = adresseCacheService;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Crée un Trajet (Exigence 1.3). Retourne 201 + <see cref="TrajetDto"/>. Si la distance n'a
|
|
/// pas pu être calculée faute de coordonnées, la réponse reste 201 et un avertissement est
|
|
/// ajouté dans l'en-tête <c>X-Trajet-Warning</c> (Exigence 2.4).
|
|
/// </summary>
|
|
[HttpPost]
|
|
[ProducesResponseType(typeof(TrajetDto), StatusCodes.Status201Created)]
|
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<ActionResult<TrajetDto>> Create(
|
|
[FromBody] CreateTrajetDto dto,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
var resultat = await _trajetService.CreateAsync(dto, cancellationToken);
|
|
|
|
if (resultat.Avertissement && !string.IsNullOrWhiteSpace(resultat.Message))
|
|
{
|
|
Response.Headers[WarningHeader] = resultat.Message;
|
|
}
|
|
|
|
return CreatedAtAction(
|
|
nameof(GetById),
|
|
new { id = resultat.Trajet.Id },
|
|
resultat.Trajet);
|
|
}
|
|
catch (VehiculeNotFoundException ex)
|
|
{
|
|
// Aucun véhicule applicable à la date et aucun VehiculeId fourni.
|
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Recalcule la distance de TOUS les trajets via OpenRouteService (BAN + ORS), indépendamment
|
|
/// d'un changement d'adresse. Sert à corriger des distances calculées en mode dégradé (ex. clé
|
|
/// ORS invalide → distances à vol d'oiseau) une fois ORS de nouveau opérationnel.
|
|
/// Retourne 200 + le nombre de trajets recalculés.
|
|
/// </summary>
|
|
[HttpPost("recalculer-distances")]
|
|
[ProducesResponseType(typeof(RecalculDistancesResultDto), StatusCodes.Status200OK)]
|
|
public async Task<ActionResult<RecalculDistancesResultDto>> RecalculerDistances(
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var nombre = await _trajetService.RecalculerToutesLesDistancesAsync(cancellationToken);
|
|
return Ok(new RecalculDistancesResultDto(nombre));
|
|
}
|
|
|
|
/// <summary>Récupère un Trajet par identifiant. 404 s'il est introuvable.</summary>
|
|
[HttpGet("{id:int}")]
|
|
[ProducesResponseType(typeof(TrajetDto), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<ActionResult<TrajetDto>> GetById(int id, CancellationToken cancellationToken)
|
|
{
|
|
var trajet = await _trajetRepository.GetByIdAsync(id, cancellationToken);
|
|
if (trajet is null)
|
|
{
|
|
return Problem(
|
|
detail: $"Le trajet {id} n'existe pas.",
|
|
statusCode: StatusCodes.Status404NotFound);
|
|
}
|
|
|
|
return Ok(new TrajetDto(
|
|
trajet.Id,
|
|
trajet.Date,
|
|
trajet.AdresseDepart,
|
|
trajet.AdresseArrivee,
|
|
// Distance EFFECTIVE (doublée si aller/retour) exposée au frontend.
|
|
trajet.DistanceEffective,
|
|
trajet.Commentaire,
|
|
trajet.Position,
|
|
trajet.VehiculeId,
|
|
trajet.PuissanceFiscale,
|
|
trajet.AllerRetour));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Modifie un Trajet (Exigences 5.5, 5.6). Retourne 200 + <see cref="TrajetDto"/>. Un
|
|
/// avertissement de distance non calculable est porté par l'en-tête <c>X-Trajet-Warning</c>.
|
|
/// 404 si le Trajet est introuvable ; 503 si le recalcul de distance échoue (Exigence 5.8).
|
|
/// </summary>
|
|
[HttpPut("{id:int}")]
|
|
[ProducesResponseType(typeof(TrajetDto), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
|
public async Task<ActionResult<TrajetDto>> Update(
|
|
int id,
|
|
[FromBody] UpdateTrajetDto dto,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
var resultat = await _trajetService.UpdateAsync(id, dto, cancellationToken);
|
|
|
|
if (resultat.Avertissement && !string.IsNullOrWhiteSpace(resultat.Message))
|
|
{
|
|
Response.Headers[WarningHeader] = resultat.Message;
|
|
}
|
|
|
|
return Ok(resultat.Trajet);
|
|
}
|
|
catch (TrajetNotFoundException ex)
|
|
{
|
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
|
}
|
|
catch (VehiculeNotFoundException ex)
|
|
{
|
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
|
}
|
|
catch (DistanceRecalculException ex)
|
|
{
|
|
// Échec du recalcul sur des coordonnées connues : modification annulée (rollback),
|
|
// données existantes préservées (Exigence 5.8). Mappé sur 503 conformément à la table
|
|
// d'erreurs de la conception (DistanceCalculationException → 503).
|
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status503ServiceUnavailable);
|
|
}
|
|
}
|
|
|
|
/// <summary>Supprime un Trajet (Exigence 5.5). 204 si succès, 404 s'il est introuvable (5.6).</summary>
|
|
[HttpDelete("{id:int}")]
|
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<IActionResult> Delete(int id, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
await _trajetService.DeleteAsync(id, cancellationToken);
|
|
return NoContent();
|
|
}
|
|
catch (TrajetNotFoundException ex)
|
|
{
|
|
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Réordonne manuellement les trajets d'une journée : met à jour uniquement leurs positions
|
|
/// sans altérer les autres champs (Exigence 11.9). 204 en cas de succès.
|
|
/// </summary>
|
|
[HttpPatch("reordonner")]
|
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
public async Task<IActionResult> Reordonner(
|
|
[FromBody] ReordonnerTrajetsDto dto,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
await _trajetService.ReordonnerAsync(dto, cancellationToken);
|
|
return NoContent();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retourne les trajets d'une journée enrichis des coordonnées et géométries connues via le
|
|
/// Cache_Adresses (Exigences 8.7, 9.3). Le paramètre <paramref name="date"/> est attendu au
|
|
/// format ISO <c>yyyy-MM-dd</c> pour la sûreté du routage. Les coordonnées/géométries absentes
|
|
/// du cache restent à <c>null</c>.
|
|
/// </summary>
|
|
[HttpGet("journee/{date}")]
|
|
[ProducesResponseType(typeof(IReadOnlyList<TrajetCarteDto>), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
public async Task<ActionResult<IReadOnlyList<TrajetCarteDto>>> GetJournee(
|
|
string date,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (!DateOnly.TryParseExact(date, "yyyy-MM-dd", CultureInfo.InvariantCulture,
|
|
DateTimeStyles.None, out var jour))
|
|
{
|
|
return Problem(
|
|
detail: $"La date '{date}' est invalide. Format attendu : yyyy-MM-dd.",
|
|
statusCode: StatusCodes.Status400BadRequest);
|
|
}
|
|
|
|
var trajets = await _trajetRepository.GetByDateAsync(jour, cancellationToken);
|
|
|
|
var resultats = new List<TrajetCarteDto>(trajets.Count);
|
|
foreach (var trajet in trajets)
|
|
{
|
|
var depart = await _adresseCacheService.ObtenirParAdresseAsync(
|
|
trajet.AdresseDepart, cancellationToken);
|
|
var arrivee = await _adresseCacheService.ObtenirParAdresseAsync(
|
|
trajet.AdresseArrivee, cancellationToken);
|
|
|
|
// Géométrie de l'itinéraire : portée par le Trajet lui-même (paire départ→arrivée),
|
|
// seule source correcte. Fallback sur le cache d'adresses pour d'éventuels trajets
|
|
// hérités dont la géométrie n'a pas encore été recalculée (rétrocompatibilité).
|
|
var geometrie = trajet.GeometrieJson ?? depart?.GeometrieJson ?? arrivee?.GeometrieJson;
|
|
|
|
resultats.Add(new TrajetCarteDto(
|
|
trajet.Id,
|
|
trajet.Date,
|
|
trajet.AdresseDepart,
|
|
trajet.AdresseArrivee,
|
|
// Distance EFFECTIVE (doublée si aller/retour), cohérente avec la liste.
|
|
trajet.DistanceEffective ?? 0m,
|
|
depart?.Latitude,
|
|
depart?.Longitude,
|
|
arrivee?.Latitude,
|
|
arrivee?.Longitude,
|
|
geometrie,
|
|
trajet.AllerRetour));
|
|
}
|
|
|
|
return Ok(resultats);
|
|
}
|
|
}
|