Renommage : TrajetKmTracker -> SchotzIadKmTracker (backend, specs, frontend)
- Backend : projets, solution, namespaces et références TrajetKmTracker.Api/.Tests -> SchotzIadKmTracker.Api/.Tests (dossiers, .csproj, .sln, .http, namespaces C#). GUID UserSecretsId préservé. - Specs : dossier .kiro/specs/trajet-km-tracker -> schotziad-km-tracker et mise à jour des références internes (chemins, tags de propriétés). - Frontend : projet Angular trajet-km-tracker -> schotziad-km-tracker (package.json, angular.json, chemin dist, Dockerfile), titre applicatif 'SchotzIAD KM Tracker'. - Docker et scripts (backend/Dockerfile, frontend/Dockerfile, run-dev.sh) mis à jour. - Suppression du fichier résiduel package.json.bak. Backend (solution) et frontend (build production) compilent sans erreur.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SchotzIadKmTracker.Api.Models.DTOs;
|
||||
using SchotzIadKmTracker.Api.Services;
|
||||
|
||||
namespace SchotzIadKmTracker.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint d'autocomplétion locale des adresses déjà géocodées (Cache_Adresses).
|
||||
/// La recherche exige au moins 3 caractères ; en deçà, une liste vide est retournée
|
||||
/// (Exigence 9.3).
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/adresses")]
|
||||
public class AdressesController : ControllerBase
|
||||
{
|
||||
private readonly IAdresseCacheService _adresseCacheService;
|
||||
|
||||
public AdressesController(IAdresseCacheService adresseCacheService)
|
||||
{
|
||||
_adresseCacheService = adresseCacheService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recherche dans le Cache_Adresses les suggestions correspondant au fragment <paramref name="q"/>
|
||||
/// (min. 3 caractères), ordonnées de la plus récemment utilisée à la plus ancienne.
|
||||
/// </summary>
|
||||
[HttpGet("search")]
|
||||
[ProducesResponseType(typeof(IReadOnlyList<AdresseCacheDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<IReadOnlyList<AdresseCacheDto>>> Search(
|
||||
[FromQuery] string? q,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var suggestions = await _adresseCacheService.RechercherLocalAsync(
|
||||
q ?? string.Empty,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
return Ok(suggestions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SchotzIadKmTracker.Api.Models.DTOs;
|
||||
using SchotzIadKmTracker.Api.Services;
|
||||
using SchotzIadKmTracker.Api.Services.Exceptions;
|
||||
|
||||
namespace SchotzIadKmTracker.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoints de gestion du Carnet_Adresses : liste, création, modification et suppression
|
||||
/// des Adresses_Nommées (Exigence 15).
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/adresses-nommees")]
|
||||
public class AdressesNommeesController : ControllerBase
|
||||
{
|
||||
private readonly IAdresseNommeeService _service;
|
||||
|
||||
public AdressesNommeesController(IAdresseNommeeService service)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
|
||||
/// <summary>Liste toutes les Adresses_Nommées, triées par Dénomination (15.1).</summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(IReadOnlyList<AdresseNommeeDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<IReadOnlyList<AdresseNommeeDto>>> GetAll(CancellationToken cancellationToken)
|
||||
{
|
||||
var adresses = await _service.GetAllAsync(cancellationToken);
|
||||
return Ok(adresses);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Crée une Adresse_Nommee. Rejette une Dénomination invalide/dupliquée ou une adresse
|
||||
/// invalide (400).
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(AdresseNommeeDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<AdresseNommeeDto>> Create(
|
||||
[FromBody] SaveAdresseNommeeDto dto,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cree = await _service.CreateAsync(dto, cancellationToken);
|
||||
return CreatedAtAction(nameof(GetAll), null, cree);
|
||||
}
|
||||
catch (DenominationInvalideException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
catch (DenominationDupliqueeException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
catch (AdresseNommeeInvalideException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Modifie une Adresse_Nommee. Rejette une Dénomination invalide/dupliquée ou une adresse
|
||||
/// invalide (400) et une Adresse_Nommee introuvable (404).
|
||||
/// </summary>
|
||||
[HttpPut("{id:int}")]
|
||||
[ProducesResponseType(typeof(AdresseNommeeDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<AdresseNommeeDto>> Update(
|
||||
int id,
|
||||
[FromBody] SaveAdresseNommeeDto dto,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var maj = await _service.UpdateAsync(id, dto, cancellationToken);
|
||||
return Ok(maj);
|
||||
}
|
||||
catch (AdresseNommeeNotFoundException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
||||
}
|
||||
catch (DenominationInvalideException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
catch (DenominationDupliqueeException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
catch (AdresseNommeeInvalideException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Supprime une Adresse_Nommee. Les Trajets existants restent inchangés (15.14).
|
||||
/// </summary>
|
||||
[HttpDelete("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Delete(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _service.DeleteAsync(id, cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
catch (AdresseNommeeNotFoundException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SchotzIadKmTracker.Api.Models.DTOs;
|
||||
using SchotzIadKmTracker.Api.Services;
|
||||
using SchotzIadKmTracker.Api.Services.Exceptions;
|
||||
|
||||
namespace SchotzIadKmTracker.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoints de gestion du barème kilométrique : consultation par année et CRUD des tranches.
|
||||
/// La validation du non-chevauchement des tranches ([KmMin, KmMax]) par (année, puissance fiscale)
|
||||
/// est appliquée à l'ajout et à la modification (Exigences 13.1–13.2).
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/bareme")]
|
||||
public class BaremeController : ControllerBase
|
||||
{
|
||||
private readonly IBaremeService _baremeService;
|
||||
|
||||
public BaremeController(IBaremeService baremeService)
|
||||
{
|
||||
_baremeService = baremeService;
|
||||
}
|
||||
|
||||
/// <summary>Retourne toutes les tranches du barème pour l'année indiquée.</summary>
|
||||
[HttpGet("{annee:int}")]
|
||||
[ProducesResponseType(typeof(IReadOnlyList<TrancheBaremeDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<IReadOnlyList<TrancheBaremeDto>>> GetBareme(
|
||||
int annee, CancellationToken cancellationToken)
|
||||
{
|
||||
var tranches = await _baremeService.GetBaremeAsync(annee, cancellationToken);
|
||||
return Ok(tranches);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ajoute une tranche du barème. Rejette (400) si l'intervalle chevauche une tranche existante
|
||||
/// pour la même (année, puissance fiscale).
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(TrancheBaremeDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<TrancheBaremeDto>> AddTranche(
|
||||
[FromBody] SaveTrancheBaremeDto dto, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tranche = await _baremeService.AddTrancheAsync(dto, cancellationToken);
|
||||
return CreatedAtAction(nameof(GetBareme), new { annee = tranche.Annee }, tranche);
|
||||
}
|
||||
catch (TranchesBaremeChevauchantesException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Met à jour une tranche existante. Rejette (400) en cas de chevauchement, 404 si la tranche est absente.
|
||||
/// </summary>
|
||||
[HttpPut("{id:int}")]
|
||||
[ProducesResponseType(typeof(TrancheBaremeDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<TrancheBaremeDto>> UpdateTranche(
|
||||
int id, [FromBody] SaveTrancheBaremeDto dto, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tranche = await _baremeService.UpdateTrancheAsync(id, dto, cancellationToken);
|
||||
return Ok(tranche);
|
||||
}
|
||||
catch (TrancheBaremeNotFoundException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
||||
}
|
||||
catch (TranchesBaremeChevauchantesException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Années pour lesquelles un barème (au moins une tranche) est défini, triées croissantes.</summary>
|
||||
[HttpGet("annees-disponibles")]
|
||||
[ProducesResponseType(typeof(IReadOnlyList<int>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<IReadOnlyList<int>>> GetAnneesDisponibles(CancellationToken cancellationToken)
|
||||
{
|
||||
var annees = await _baremeService.GetAnneesAvecBaremeAsync(cancellationToken);
|
||||
return Ok(annees);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copie le dernier barème connu (année la plus récente antérieure ayant un barème) vers
|
||||
/// l'année cible. Remplace les tranches existantes de l'année cible (annule et remplace).
|
||||
/// Retourne le nombre de tranches copiées (0 si aucun barème antérieur n'existe).
|
||||
/// </summary>
|
||||
[HttpPost("copier/{anneeCible:int}")]
|
||||
[ProducesResponseType(typeof(CopieBaremeResultDto), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CopieBaremeResultDto>> CopierDernierBareme(
|
||||
int anneeCible, CancellationToken cancellationToken)
|
||||
{
|
||||
var nombre = await _baremeService.CopierDernierBaremeAsync(anneeCible, cancellationToken);
|
||||
return Ok(new CopieBaremeResultDto(nombre));
|
||||
}
|
||||
|
||||
/// <summary>Supprime une tranche du barème. Retourne 404 si la tranche est absente.</summary>
|
||||
[HttpDelete("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> DeleteTranche(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _baremeService.DeleteTrancheAsync(id, cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
catch (TrancheBaremeNotFoundException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using System.Net.Mime;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SchotzIadKmTracker.Api.Services;
|
||||
using SchotzIadKmTracker.Api.Services.Exceptions;
|
||||
|
||||
namespace SchotzIadKmTracker.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoints d'Export_PDF : rapport mensuel et synthèse annuelle (Exigences 7.3, 7.4, 7.5).
|
||||
/// Chaque réponse renvoie un unique fichier <c>application/pdf</c> avec l'en-tête
|
||||
/// <c>Content-Disposition: attachment</c> et le nom de fichier suggéré par le service
|
||||
/// (<c>trajets_MM_AAAA.pdf</c> pour le mensuel, <c>trajets_AAAA.pdf</c> pour l'annuel).
|
||||
///
|
||||
/// Le filtre par véhicule est optionnel : si <c>vehiculeId</c> est omis, le Véhicule_Courant
|
||||
/// est utilisé par défaut.
|
||||
///
|
||||
/// <para>
|
||||
/// Note (Exigence 7.9) : une réponse HTTP unique ne peut transporter qu'un seul fichier.
|
||||
/// Ces endpoints produisent donc le rapport du véhicule sélectionné (ou courant). La génération
|
||||
/// d'un rapport distinct par véhicule lorsqu'une période comporte plusieurs véhicules est assurée
|
||||
/// par les variantes de service <c>GenererRapports…ParVehiculeAsync</c>, réservées à un usage
|
||||
/// multi-fichiers ultérieur (p. ex. archive). Ici, un appel = un fichier filtré par véhicule.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Erreurs : un dépassement du délai de génération ou un échec de rendu lève une
|
||||
/// <see cref="PdfGenerationException"/> mappée vers <c>503 Service Unavailable</c> ; un véhicule
|
||||
/// introuvable lève une <see cref="VehiculeNotFoundException"/> mappée vers <c>404 Not Found</c>
|
||||
/// (réponses ProblemDetails).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/exports")]
|
||||
public class ExportsPdfController : ControllerBase
|
||||
{
|
||||
private readonly IPdfService _pdfService;
|
||||
private readonly IVehiculeService _vehiculeService;
|
||||
|
||||
public ExportsPdfController(IPdfService pdfService, IVehiculeService vehiculeService)
|
||||
{
|
||||
_pdfService = pdfService;
|
||||
_vehiculeService = vehiculeService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Génère et télécharge le rapport PDF mensuel d'un (année, mois) pour le véhicule indiqué
|
||||
/// (Véhicule_Courant si <paramref name="vehiculeId"/> est omis). Retourne les octets du PDF
|
||||
/// avec <c>Content-Type: application/pdf</c> et <c>Content-Disposition: attachment;
|
||||
/// filename="trajets_MM_AAAA.pdf"</c> (Exigences 7.3, 7.4).
|
||||
/// </summary>
|
||||
[HttpGet("mensuel/{annee:int}/{mois:int}")]
|
||||
[Produces(MediaTypeNames.Application.Pdf)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||
public async Task<IActionResult> ExporterMensuel(
|
||||
int annee, int mois, [FromQuery] int? vehiculeId, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var vehicule = await ResoudreVehiculeIdAsync(vehiculeId, cancellationToken);
|
||||
var pdf = await _pdfService.GenererRapportMensuelAsync(annee, mois, vehicule, cancellationToken);
|
||||
return FichierPdf(pdf);
|
||||
}
|
||||
catch (VehiculeNotFoundException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
||||
}
|
||||
catch (PdfGenerationException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Génère et télécharge la synthèse PDF annuelle d'une année pour le véhicule indiqué
|
||||
/// (Véhicule_Courant si <paramref name="vehiculeId"/> est omis). Retourne les octets du PDF
|
||||
/// avec <c>Content-Type: application/pdf</c> et <c>Content-Disposition: attachment;
|
||||
/// filename="trajets_AAAA.pdf"</c> (Exigences 7.3, 7.4).
|
||||
/// </summary>
|
||||
[HttpGet("annuel/{annee:int}")]
|
||||
[Produces(MediaTypeNames.Application.Pdf)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||
public async Task<IActionResult> ExporterAnnuel(
|
||||
int annee, [FromQuery] int? vehiculeId, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var vehicule = await ResoudreVehiculeIdAsync(vehiculeId, cancellationToken);
|
||||
var pdf = await _pdfService.GenererRapportAnnuelAsync(annee, vehicule, cancellationToken);
|
||||
return FichierPdf(pdf);
|
||||
}
|
||||
catch (VehiculeNotFoundException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
||||
}
|
||||
catch (PdfGenerationException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Résout l'identifiant du véhicule à utiliser : celui fourni s'il est présent, sinon
|
||||
/// le Véhicule_Courant (Date_Effet la plus récente <= aujourd'hui).
|
||||
/// </summary>
|
||||
private async Task<int> ResoudreVehiculeIdAsync(int? vehiculeId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (vehiculeId.HasValue)
|
||||
{
|
||||
return vehiculeId.Value;
|
||||
}
|
||||
|
||||
var courant = await _vehiculeService.GetVehiculeCourantAsync(cancellationToken);
|
||||
return courant.Id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renvoie le PDF en pièce jointe téléchargeable, en s'appuyant sur le nom de fichier
|
||||
/// suggéré par le <see cref="PdfResult"/> (<c>Content-Disposition: attachment</c>).
|
||||
/// </summary>
|
||||
private FileContentResult FichierPdf(PdfResult pdf)
|
||||
=> File(pdf.Contenu, MediaTypeNames.Application.Pdf, pdf.NomFichier);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SchotzIadKmTracker.Api.Models.DTOs;
|
||||
using SchotzIadKmTracker.Api.Services;
|
||||
|
||||
namespace SchotzIadKmTracker.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoints de navigation temporelle et de totaux du Panneau_Totaux :
|
||||
/// années/mois/jours disponibles (périodes ayant des données) et totaux
|
||||
/// jour/mois/année partiel pour une date donnée (Exigences 12.15, 12.16).
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api")]
|
||||
public class NavigationController : ControllerBase
|
||||
{
|
||||
private readonly ITotauxService _totauxService;
|
||||
|
||||
public NavigationController(ITotauxService totauxService)
|
||||
{
|
||||
_totauxService = totauxService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retourne les années comportant au moins un Trajet, complétées par l'année
|
||||
/// courante, triées croissantes (Exigence 12.15).
|
||||
/// </summary>
|
||||
[HttpGet("annees-disponibles")]
|
||||
[ProducesResponseType(typeof(IReadOnlyList<int>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<IReadOnlyList<int>>> GetAnneesDisponibles(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var annees = await _totauxService.GetAnneesDisponiblesAsync(cancellationToken);
|
||||
return Ok(annees);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retourne les mois (1..12) de l'année comportant au moins un Trajet,
|
||||
/// triés croissants (Exigence 12.16).
|
||||
/// </summary>
|
||||
[HttpGet("mois-disponibles/{annee:int}")]
|
||||
[ProducesResponseType(typeof(IReadOnlyList<int>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<IReadOnlyList<int>>> GetMoisDisponibles(
|
||||
int annee, CancellationToken cancellationToken)
|
||||
{
|
||||
var mois = await _totauxService.GetMoisDisponiblesAsync(annee, cancellationToken);
|
||||
return Ok(mois);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retourne les jours du mois de l'année comportant au moins un Trajet,
|
||||
/// triés croissants (Exigence 12.16).
|
||||
/// </summary>
|
||||
[HttpGet("jours-disponibles/{annee:int}/{mois:int}")]
|
||||
[ProducesResponseType(typeof(IReadOnlyList<int>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<IReadOnlyList<int>>> GetJoursDisponibles(
|
||||
int annee, int mois, CancellationToken cancellationToken)
|
||||
{
|
||||
var jours = await _totauxService.GetJoursDisponiblesAsync(annee, mois, cancellationToken);
|
||||
return Ok(jours);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retourne les totaux jour/mois/année partiel du Panneau_Totaux pour la date
|
||||
/// indiquée et le véhicule (Véhicule_Courant si <paramref name="vehiculeId"/> omis).
|
||||
/// La date est attendue au format ISO <c>yyyy-MM-dd</c> ; un format invalide est
|
||||
/// rejeté avec un code HTTP 400.
|
||||
/// </summary>
|
||||
[HttpGet("totaux/{date}")]
|
||||
[ProducesResponseType(typeof(TotauxDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<TotauxDto>> GetTotaux(
|
||||
string date, [FromQuery] int? vehiculeId, 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 totaux = await _totauxService.GetTotauxAsync(jour, vehiculeId, cancellationToken);
|
||||
return Ok(totaux);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SchotzIadKmTracker.Api.Models.DTOs;
|
||||
using SchotzIadKmTracker.Api.Services;
|
||||
|
||||
namespace SchotzIadKmTracker.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoints de gestion du profil utilisateur (singleton) et de son état de configuration.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/profil")]
|
||||
public class ProfilController : ControllerBase
|
||||
{
|
||||
private readonly IProfilService _profilService;
|
||||
|
||||
public ProfilController(IProfilService profilService)
|
||||
{
|
||||
_profilService = profilService;
|
||||
}
|
||||
|
||||
/// <summary>Récupère le profil (créé à la volée s'il est absent).</summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(ProfilDto), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ProfilDto>> GetProfil(CancellationToken cancellationToken)
|
||||
{
|
||||
var profil = await _profilService.GetProfilAsync(cancellationToken);
|
||||
return Ok(profil);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sauvegarde le profil. La validation du matricule (≤ 8 car.) est assurée par FluentValidation.
|
||||
/// Le géocodage de l'Adresse_Domicile est effectué au mieux : un échec BAN n'empêche pas la sauvegarde.
|
||||
/// </summary>
|
||||
[HttpPut]
|
||||
[ProducesResponseType(typeof(ProfilDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<ProfilDto>> SaveProfil(
|
||||
[FromBody] SaveProfilDto dto,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var profil = await _profilService.SaveProfilAsync(dto, cancellationToken);
|
||||
return Ok(profil);
|
||||
}
|
||||
|
||||
/// <summary>État de configuration du profil (onboarding).</summary>
|
||||
[HttpGet("etat")]
|
||||
[ProducesResponseType(typeof(EtatProfilDto), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<EtatProfilDto>> GetEtat(CancellationToken cancellationToken)
|
||||
{
|
||||
var etat = await _profilService.GetEtatAsync(cancellationToken);
|
||||
return Ok(etat);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SchotzIadKmTracker.Api.Models.DTOs;
|
||||
using SchotzIadKmTracker.Api.Services;
|
||||
using SchotzIadKmTracker.Api.Services.Exceptions;
|
||||
|
||||
namespace SchotzIadKmTracker.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoints « Barème et remboursements » : montant prévisionnel (80% indicatif),
|
||||
/// montants réglés (saisie/consultation) et régularisation annuelle (barème plein).
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api")]
|
||||
public class RemboursementController : ControllerBase
|
||||
{
|
||||
private readonly IMontantRegleService _montantRegleService;
|
||||
private readonly IRemboursementService _remboursementService;
|
||||
|
||||
public RemboursementController(
|
||||
IMontantRegleService montantRegleService,
|
||||
IRemboursementService remboursementService)
|
||||
{
|
||||
_montantRegleService = montantRegleService;
|
||||
_remboursementService = remboursementService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retourne le Montant_Previsionnel indicatif (80% du barème plein sur le total annuel partiel)
|
||||
/// pour un (année, mois, véhicule). Purement informatif — hors régularisation (Exigences 13.5/13.6).
|
||||
/// </summary>
|
||||
[HttpGet("montant-previsionnel/{annee:int}/{mois:int}")]
|
||||
[ProducesResponseType(typeof(MontantPrevisionnelDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<MontantPrevisionnelDto>> GetMontantPrevisionnel(
|
||||
int annee, int mois, [FromQuery] int vehiculeId, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dto = await _montantRegleService.GetMontantPrevisionnelAsync(
|
||||
annee, mois, vehiculeId, cancellationToken);
|
||||
return Ok(dto);
|
||||
}
|
||||
catch (VehiculeNotFoundException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retourne les Montant_Regle saisis pour une (année, véhicule), triés par mois croissant.
|
||||
/// </summary>
|
||||
[HttpGet("montants-regles/{annee:int}")]
|
||||
[ProducesResponseType(typeof(IReadOnlyList<MontantRegleDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<IReadOnlyList<MontantRegleDto>>> GetMontantsRegles(
|
||||
int annee, [FromQuery] int vehiculeId, CancellationToken cancellationToken)
|
||||
{
|
||||
var montants = await _montantRegleService.GetMontantsReglesAsync(annee, vehiculeId, cancellationToken);
|
||||
return Ok(montants);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enregistre ou met à jour (upsert) un Montant_Regle par (année, mois, véhicule).
|
||||
/// Rejette un montant négatif avec un code HTTP 400 (Exigence 14.4).
|
||||
/// </summary>
|
||||
[HttpPut("montants-regles")]
|
||||
[ProducesResponseType(typeof(MontantRegleDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<MontantRegleDto>> UpsertMontantRegle(
|
||||
[FromBody] MontantRegleDto dto, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _montantRegleService.UpsertMontantRegleAsync(dto, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (MontantRegleNegatifException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retourne la synthèse annuelle de régularisation (barème plein) pour une (année, véhicule) :
|
||||
/// tranche annuelle, 12 lignes mensuelles et totaux (km annuel, réglé, Montant_Reel, Regularisation).
|
||||
/// </summary>
|
||||
[HttpGet("regularisation/{annee:int}")]
|
||||
[ProducesResponseType(typeof(RegularisationAnnuelleDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<RegularisationAnnuelleDto>> GetRegularisation(
|
||||
int annee, [FromQuery] int vehiculeId, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dto = await _remboursementService.GetRegularisationAnnuelleAsync(
|
||||
annee, vehiculeId, cancellationToken);
|
||||
return Ok(dto);
|
||||
}
|
||||
catch (VehiculeNotFoundException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SchotzIadKmTracker.Api.Models.DTOs;
|
||||
using SchotzIadKmTracker.Api.Services;
|
||||
using SchotzIadKmTracker.Api.Services.Exceptions;
|
||||
|
||||
namespace SchotzIadKmTracker.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoints des résumés kilométriques : Résumé_Mensuel et Résumé_Annuel.
|
||||
/// Le filtre par véhicule est optionnel (Véhicule_Courant utilisé si omis).
|
||||
/// Les paramètres hors bornes (mois ∉ [1, 12], année ∉ [2000, 2100]) sont
|
||||
/// rejetés avec un code HTTP 400 (Exigences 3.6, 3.8, 4.1).
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/resumes")]
|
||||
public class ResumesController : ControllerBase
|
||||
{
|
||||
private readonly IResumeService _resumeService;
|
||||
|
||||
public ResumesController(IResumeService resumeService)
|
||||
{
|
||||
_resumeService = resumeService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retourne le Résumé_Mensuel d'un (année, mois) pour le véhicule indiqué
|
||||
/// (Véhicule_Courant si <paramref name="vehiculeId"/> est omis). Rejette (400)
|
||||
/// les paramètres hors bornes (Exigences 3.6, 3.8).
|
||||
/// </summary>
|
||||
[HttpGet("mensuel/{annee:int}/{mois:int}")]
|
||||
[ProducesResponseType(typeof(ResumeMensuelDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<ResumeMensuelDto>> GetResumeMensuel(
|
||||
int annee, int mois, [FromQuery] int? vehiculeId, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resume = await _resumeService.GetResumeMensuelAsync(annee, mois, vehiculeId, cancellationToken);
|
||||
return Ok(resume);
|
||||
}
|
||||
catch (ResumeParametreInvalideException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retourne le Résumé_Annuel d'une année (12 résumés mensuels) pour le véhicule
|
||||
/// indiqué (Véhicule_Courant si <paramref name="vehiculeId"/> est omis). Rejette (400)
|
||||
/// une année hors bornes (Exigences 3.6, 4.1).
|
||||
/// </summary>
|
||||
[HttpGet("annuel/{annee:int}")]
|
||||
[ProducesResponseType(typeof(ResumeAnnuelDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<ResumeAnnuelDto>> GetResumeAnnuel(
|
||||
int annee, [FromQuery] int? vehiculeId, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resume = await _resumeService.GetResumeAnnuelAsync(annee, vehiculeId, cancellationToken);
|
||||
return Ok(resume);
|
||||
}
|
||||
catch (ResumeParametreInvalideException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
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,
|
||||
trajet.Distance,
|
||||
trajet.Commentaire,
|
||||
trajet.Position,
|
||||
trajet.VehiculeId,
|
||||
trajet.PuissanceFiscale));
|
||||
}
|
||||
|
||||
/// <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,
|
||||
trajet.Distance ?? 0m,
|
||||
depart?.Latitude,
|
||||
depart?.Longitude,
|
||||
arrivee?.Latitude,
|
||||
arrivee?.Longitude,
|
||||
geometrie));
|
||||
}
|
||||
|
||||
return Ok(resultats);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SchotzIadKmTracker.Api.Models.DTOs;
|
||||
using SchotzIadKmTracker.Api.Services;
|
||||
using SchotzIadKmTracker.Api.Services.Exceptions;
|
||||
|
||||
namespace SchotzIadKmTracker.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoints de gestion des véhicules : historique, véhicule courant, création et réaffectation.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/vehicules")]
|
||||
public class VehiculesController : ControllerBase
|
||||
{
|
||||
private readonly IVehiculeService _vehiculeService;
|
||||
|
||||
public VehiculesController(IVehiculeService vehiculeService)
|
||||
{
|
||||
_vehiculeService = vehiculeService;
|
||||
}
|
||||
|
||||
/// <summary>Historique complet des véhicules (trié par date d'effet décroissante).</summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(IReadOnlyList<VehiculeDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<IReadOnlyList<VehiculeDto>>> GetHistorique(CancellationToken cancellationToken)
|
||||
{
|
||||
var vehicules = await _vehiculeService.GetHistoriqueAsync(cancellationToken);
|
||||
return Ok(vehicules);
|
||||
}
|
||||
|
||||
/// <summary>Véhicule courant (date d'effet la plus récente ≤ aujourd'hui).</summary>
|
||||
[HttpGet("courant")]
|
||||
[ProducesResponseType(typeof(VehiculeDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<VehiculeDto>> GetCourant(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var vehicule = await _vehiculeService.GetVehiculeCourantAsync(cancellationToken);
|
||||
return Ok(vehicule);
|
||||
}
|
||||
catch (VehiculeNotFoundException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enregistre un véhicule. Retourne <see cref="VehiculeCreeDto"/> indiquant si une réaffectation
|
||||
/// des trajets doit être proposée. Rejette une date d'effet future (400).
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(VehiculeCreeDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<VehiculeCreeDto>> Create(
|
||||
[FromBody] CreateVehiculeDto dto,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resultat = await _vehiculeService.CreateAsync(dto, cancellationToken);
|
||||
return CreatedAtAction(nameof(GetHistorique), null, resultat);
|
||||
}
|
||||
catch (DateEffetFutureException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Accepte ou refuse la réaffectation des trajets au véhicule indiqué.</summary>
|
||||
[HttpPost("reaffectation")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Reaffecter(
|
||||
[FromBody] ReaffectationDto dto,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _vehiculeService.ReaffecterAsync(dto, cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
catch (VehiculeNotFoundException ex)
|
||||
{
|
||||
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user