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.
This commit is contained in:
2026-09-14 02:08:27 +02:00
commit e3f946f7d4
225 changed files with 38168 additions and 0 deletions
@@ -0,0 +1,143 @@
using System.Globalization;
using Microsoft.EntityFrameworkCore;
using TrajetKmTracker.Api.Data;
using TrajetKmTracker.Api.Models.DTOs;
using TrajetKmTracker.Api.Models.Entities;
using TrajetKmTracker.Api.Repositories;
using TrajetKmTracker.Api.Services.Exceptions;
namespace TrajetKmTracker.Api.Services;
/// <inheritdoc />
public class VehiculeService : IVehiculeService
{
private const string DateFormat = "dd/MM/yyyy";
private readonly IVehiculeRepository _repository;
private readonly AppDbContext _context;
public VehiculeService(IVehiculeRepository repository, AppDbContext context)
{
_repository = repository;
_context = context;
}
/// <inheritdoc />
public async Task<IReadOnlyList<VehiculeDto>> GetHistoriqueAsync(CancellationToken cancellationToken = default)
{
var vehicules = await _repository.GetHistoriqueAsync(cancellationToken);
return vehicules.Select(ToDto).ToList();
}
/// <inheritdoc />
public async Task<VehiculeDto> GetVehiculeCourantAsync(CancellationToken cancellationToken = default)
{
var aujourdHui = DateOnly.FromDateTime(DateTime.Today);
var vehicule = await _repository.GetVehiculeCourantAsync(aujourdHui, cancellationToken)
?? throw new VehiculeNotFoundException(
"Aucun véhicule courant : aucun véhicule n'a de date d'effet inférieure ou égale à aujourd'hui.");
return ToDto(vehicule);
}
/// <inheritdoc />
public async Task<VehiculeDto?> GetVehiculeADateAsync(DateOnly date, CancellationToken cancellationToken = default)
{
var vehicule = await _repository.GetVehiculeCourantAsync(date, cancellationToken);
return vehicule is null ? null : ToDto(vehicule);
}
/// <inheritdoc />
public async Task<VehiculeCreeDto> CreateAsync(CreateVehiculeDto dto, CancellationToken cancellationToken = default)
{
var dateEffet = ParseDateEffet(dto.DateEffet);
// Rejet d'une date d'effet dans le futur (Exigence 10.11).
if (dateEffet > DateOnly.FromDateTime(DateTime.Today))
{
throw new DateEffetFutureException("La date d'effet ne peut pas être postérieure à aujourd'hui.");
}
var vehicule = new Vehicule
{
Denomination = dto.Denomination,
Plaque = dto.Plaque,
PuissanceFiscale = dto.PuissanceFiscale,
DateEffet = dateEffet,
};
// Conservation de l'historique : on ajoute sans supprimer/modifier les véhicules existants (Property 22).
await using var transaction = await _context.Database.BeginTransactionAsync(cancellationToken);
try
{
await _repository.AddAsync(vehicule, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
}
catch
{
await transaction.RollbackAsync(cancellationToken);
throw;
}
// Détection des trajets à réaffecter : date >= DateEffet et puissance fiscale différente.
var trajetsConcernes = await _repository.GetTrajetsAReaffecterAsync(
dateEffet, vehicule.PuissanceFiscale, cancellationToken);
return new VehiculeCreeDto(
ToDto(vehicule),
ReaffectationProposee: trajetsConcernes.Count > 0,
NombreTrajetsConcernes: trajetsConcernes.Count);
}
/// <inheritdoc />
public async Task ReaffecterAsync(ReaffectationDto dto, CancellationToken cancellationToken = default)
{
var vehicule = await _repository.GetByIdAsync(dto.VehiculeId, cancellationToken)
?? throw new VehiculeNotFoundException($"Le véhicule d'identifiant {dto.VehiculeId} est introuvable.");
// Réaffectation refusée : aucun changement (Exigence 10.17).
if (!dto.Accepter)
{
return;
}
// Réaffectation acceptée : mise à jour bornée à la date d'effet (Exigence 10.16 / Property 20).
var trajets = await _repository.GetTrajetsAReaffecterAsync(
vehicule.DateEffet, vehicule.PuissanceFiscale, cancellationToken);
if (trajets.Count == 0)
{
return;
}
await using var transaction = await _context.Database.BeginTransactionAsync(cancellationToken);
try
{
var maintenant = DateTime.UtcNow;
foreach (var trajet in trajets)
{
trajet.VehiculeId = vehicule.Id;
trajet.PuissanceFiscale = vehicule.PuissanceFiscale;
trajet.UpdatedAt = maintenant;
}
await _context.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
}
catch
{
await transaction.RollbackAsync(cancellationToken);
throw;
}
}
private static DateOnly ParseDateEffet(string value)
{
// La validation FluentValidation garantit normalement le format ; on reste défensif ici.
return DateOnly.ParseExact(value, DateFormat, CultureInfo.InvariantCulture);
}
private static VehiculeDto ToDto(Vehicule v) =>
new(v.Id, v.Denomination, v.Plaque, v.PuissanceFiscale, v.DateEffet);
}