Files
SchotzIAD/backend/TrajetKmTracker.Api/Controllers/ExportsPdfController.cs
T
MadJax e3f946f7d4 SchotzIAD KM Tracker : suivi des trajets kilométriques (Angular + .NET 8)
Application complète de saisie et de suivi des trajets kilométriques avec
calcul de distance routière (OpenRouteService), barème kilométrique, résumés
et régularisation annuelle.

Backend (ASP.NET Core .NET 8, EF Core, SQLite) :
- Trajets (CRUD, réordonnancement, insertion), calcul de distance ORS avec
  fallback Haversine, géométrie d'itinéraire stockée par trajet, recalcul en masse.
- Profil, véhicules (avec dénomination), carnet d'adresses nommées, cache d'adresses.
- Barème kilométrique : tranches multiples par puissance fiscale, borne max
  exclusive avec 'illimité' (KmMax nullable), coefficient à 3 décimales,
  recopie du dernier barème connu.
- Résumés mensuel/annuel, montants réglés, régularisation, export PDF.

Frontend (Angular 22, zoneless + signals) :
- Consultation par jour, navigation temporelle synchronisée, carte Leaflet,
  formulaire de trajet (adresses nommées, domicile, autocomplétion BAN).
- Détection de changement zoneless (indépendante de zone.js).

Hébergement : Docker Compose (nginx + backend), scripts run-dev.sh / run-docker.sh.
2026-09-14 02:08:27 +02:00

127 lines
5.5 KiB
C#

using System.Net.Mime;
using Microsoft.AspNetCore.Mvc;
using TrajetKmTracker.Api.Services;
using TrajetKmTracker.Api.Services.Exceptions;
namespace TrajetKmTracker.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 &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);
}