Files
SchotzIAD/backend/SchotzIadKmTracker.Api/Services/ResumeService.cs
T
MadJax 09c24d5f59 UX consultation, trajets aller/retour et rafraîchissement des totaux
- 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.
2026-09-15 12:33:10 +02:00

142 lines
4.8 KiB
C#

using Microsoft.EntityFrameworkCore;
using SchotzIadKmTracker.Api.Data;
using SchotzIadKmTracker.Api.Models.DTOs;
using SchotzIadKmTracker.Api.Models.Entities;
using SchotzIadKmTracker.Api.Services.Exceptions;
namespace SchotzIadKmTracker.Api.Services;
/// <inheritdoc />
public class ResumeService : IResumeService
{
private const int MoisMin = 1;
private const int MoisMax = 12;
private const int AnneeMin = 2000;
private const int AnneeMax = 2100;
private readonly AppDbContext _context;
public ResumeService(AppDbContext context)
{
_context = context;
}
/// <inheritdoc />
public async Task<ResumeMensuelDto> GetResumeMensuelAsync(
int annee, int mois, int? vehiculeId = null, CancellationToken cancellationToken = default)
{
ValiderAnnee(annee);
ValiderMois(mois);
var trajets = await ChargerTrajetsDuMoisAsync(annee, mois, vehiculeId, cancellationToken);
return ConstruireResumeMensuel(annee, mois, trajets);
}
/// <inheritdoc />
public async Task<ResumeAnnuelDto> GetResumeAnnuelAsync(
int annee, int? vehiculeId = null, CancellationToken cancellationToken = default)
{
ValiderAnnee(annee);
// Charge une seule fois tous les trajets de l'année, puis répartit par mois.
var query = _context.Trajets
.AsNoTracking()
.Where(t => t.Date.Year == annee);
if (vehiculeId.HasValue)
{
query = query.Where(t => t.VehiculeId == vehiculeId.Value);
}
var trajetsAnnee = await query.ToListAsync(cancellationToken);
var resumesMensuels = new List<ResumeMensuelDto>(MoisMax);
for (var mois = MoisMin; mois <= MoisMax; mois++)
{
var trajetsDuMois = trajetsAnnee
.Where(t => t.Date.Month == mois)
.ToList();
resumesMensuels.Add(ConstruireResumeMensuel(annee, mois, trajetsDuMois));
}
// Total annuel = somme des 12 totaux mensuels (Exigences 4.3, 4.7, 6.6).
var totalAnnuel = Arrondir(resumesMensuels.Sum(r => r.TotalKm));
return new ResumeAnnuelDto(annee, resumesMensuels, totalAnnuel);
}
/// <summary>
/// Charge les Trajets d'un mois donné, filtrés éventuellement par véhicule, triés par
/// date croissante puis par Position croissante (Exigence 3.2).
/// </summary>
private async Task<List<Trajet>> ChargerTrajetsDuMoisAsync(
int annee, int mois, int? vehiculeId, CancellationToken cancellationToken)
{
var query = _context.Trajets
.AsNoTracking()
.Where(t => t.Date.Year == annee && t.Date.Month == mois);
if (vehiculeId.HasValue)
{
query = query.Where(t => t.VehiculeId == vehiculeId.Value);
}
// Tri par Position (decimal) non traduisible par SQLite : ordonnancement côté client.
var trajets = await query.ToListAsync(cancellationToken);
return trajets
.OrderBy(t => t.Date)
.ThenBy(t => t.Position)
.ToList();
}
/// <summary>
/// Construit un <see cref="ResumeMensuelDto"/> à partir d'une liste de Trajets déjà triée.
/// Le total est la somme des distances (null → 0) arrondie à 2 décimales (Property 1).
/// </summary>
private static ResumeMensuelDto ConstruireResumeMensuel(int annee, int mois, IReadOnlyList<Trajet> trajets)
{
var trajetsDto = trajets.Select(ToDto).ToList();
// Total = somme des distances EFFECTIVES (doublées si aller/retour), null → 0 (Property 1).
var totalKm = Arrondir(trajets.Sum(t => t.DistanceEffective ?? 0m));
return new ResumeMensuelDto(annee, mois, trajetsDto, totalKm);
}
private void ValiderMois(int mois)
{
if (mois < MoisMin || mois > MoisMax)
{
throw new ResumeParametreInvalideException(
$"Le mois doit être compris entre {MoisMin} et {MoisMax} inclus ; valeur reçue : {mois}.");
}
}
private void ValiderAnnee(int annee)
{
if (annee < AnneeMin || annee > AnneeMax)
{
throw new ResumeParametreInvalideException(
$"L'année doit être comprise entre {AnneeMin} et {AnneeMax} inclus ; valeur reçue : {annee}.");
}
}
private static decimal Arrondir(decimal valeur) =>
Math.Round(valeur, 2, MidpointRounding.AwayFromZero);
private static TrajetDto ToDto(Trajet t) =>
new(
t.Id,
t.Date,
t.AdresseDepart,
t.AdresseArrivee,
// Distance EFFECTIVE (doublée si aller/retour), alignée sur le TrajetDto commun.
t.DistanceEffective,
t.Commentaire,
t.Position,
t.VehiculeId,
t.PuissanceFiscale,
t.AllerRetour);
}