Files
SchotzIAD/backend/SchotzIadKmTracker.Api/Controllers/ExportsPdfController.cs
T
MadJax 61298d6384 Refonte export PDF, colonne commentaire et UX résumés
- 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
2026-09-28 08:58:23 +02:00

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 &lt;= 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);
}