- Export PDF: nouveau nommage AAAAMM/AAAA_Trajets_MATRICULE_PLAQUE.pdf, logo carré embarqué en en-tête, km formatés selon Precision_Distance, restructuration en-têtes (Domicile déplacé, période/année fusionnées au titre) - Correction bug: le nom de fichier suggéré au téléchargement ne remontait pas au frontend (CORS WithExposedHeaders manquant + composants résumé ne lisaient pas Content-Disposition) - Résumé mensuel: colonne Commentaire, sélecteur temporel unifié (auto-chargement), km selon précision - Résumé annuel: bouton Export PDF repositionné, km selon précision - Panneau totaux: renommage "Remboursement indicatif" + infobulles d'aide (icône) au lieu du texte permanent - Ajout de PdfServiceIntegrationTests (3 tests), 65/65 tests backend OK - Mise à jour requirements.md et design.md en conséquence
128 lines
5.6 KiB
C#
128 lines
5.6 KiB
C#
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>AAAAMM_Trajets_MATRICULE_PLAQUE.pdf</c> pour le mensuel, <c>AAAA_Trajets_MATRICULE_PLAQUE.pdf</c>
|
|
/// pour l'annuel — les segments Matricule/Plaque sont omis s'ils sont vides).
|
|
///
|
|
/// 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="AAAAMM_Trajets_MATRICULE_PLAQUE.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="AAAA_Trajets_MATRICULE_PLAQUE.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);
|
|
}
|