Files
SchotzIAD/backend/TrajetKmTracker.Api/Repositories/TrajetRepository.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

194 lines
6.5 KiB
C#

using Microsoft.EntityFrameworkCore;
using TrajetKmTracker.Api.Data;
using TrajetKmTracker.Api.Models.Entities;
using TrajetKmTracker.Api.Services;
namespace TrajetKmTracker.Api.Repositories;
/// <inheritdoc />
public class TrajetRepository : ITrajetRepository
{
private readonly AppDbContext _context;
private readonly IVehiculeService _vehiculeService;
public TrajetRepository(AppDbContext context, IVehiculeService vehiculeService)
{
_context = context;
_vehiculeService = vehiculeService;
}
/// <inheritdoc />
public async Task<Trajet?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
{
return await _context.Trajets
.FirstOrDefaultAsync(t => t.Id == id, cancellationToken);
}
/// <inheritdoc />
public async Task<List<Trajet>> GetByDateAsync(DateOnly date, CancellationToken cancellationToken = default)
{
// SQLite ne sait pas trier (ORDER BY) sur une colonne decimal : on ramène les
// trajets du jour puis on trie côté client (volume par journée très réduit).
var trajets = await _context.Trajets
.Where(t => t.Date == date)
.ToListAsync(cancellationToken);
return trajets
.OrderBy(t => t.Position)
.ThenBy(t => t.Id)
.ToList();
}
/// <inheritdoc />
public async Task AppliquerVehiculeADateAsync(Trajet trajet, CancellationToken cancellationToken = default)
{
// Résolution du véhicule applicable à la date du trajet (Date_Effet la plus récente <= Date).
var vehicule = await _vehiculeService.GetVehiculeADateAsync(trajet.Date, cancellationToken);
if (vehicule is null)
{
// Aucun véhicule applicable à cette date : on laisse la logique métier (TrajetService) décider.
return;
}
// Dénormalisation : copie de la puissance fiscale du véhicule au moment de l'association (Exigence 10.14).
trajet.VehiculeId = vehicule.Id;
trajet.PuissanceFiscale = vehicule.PuissanceFiscale;
}
/// <inheritdoc />
public async Task<decimal> CalculerPositionFinDeJourneeAsync(DateOnly date, CancellationToken cancellationToken = default)
{
// SQLite ne sait pas appliquer MAX sur une colonne decimal : on ramène les positions
// du jour et on calcule le maximum côté client (volume par journée très réduit).
var positions = await _context.Trajets
.Where(t => t.Date == date)
.Select(t => t.Position)
.ToListAsync(cancellationToken);
if (positions.Count == 0)
{
return 1.0m;
}
return positions.Max() + 1.0m;
}
/// <inheritdoc />
public decimal CalculerPositionEntre(decimal positionPrecedente, decimal positionSuivante)
{
if (positionPrecedente >= positionSuivante)
{
throw new ArgumentException(
$"La position précédente ({positionPrecedente}) doit être strictement inférieure à la position suivante ({positionSuivante}).",
nameof(positionPrecedente));
}
return (positionPrecedente + positionSuivante) / 2.0m;
}
/// <inheritdoc />
public bool NecessiteRenumerotation(decimal positionPrecedente, decimal positionSuivante)
{
return positionSuivante - positionPrecedente < ITrajetRepository.SeuilRenumerotation;
}
/// <inheritdoc />
public Task AddAsync(Trajet trajet, CancellationToken cancellationToken = default)
{
_context.Trajets.Add(trajet);
return Task.CompletedTask;
}
/// <inheritdoc />
public void Update(Trajet trajet)
{
_context.Trajets.Update(trajet);
}
/// <inheritdoc />
public void Delete(Trajet trajet)
{
_context.Trajets.Remove(trajet);
}
/// <inheritdoc />
public Task<int> SauvegarderAsync(CancellationToken cancellationToken = default)
{
return _context.SaveChangesAsync(cancellationToken);
}
/// <inheritdoc />
public async Task ReordonnerAsync(
IReadOnlyList<(int Id, decimal Position)> positions,
CancellationToken cancellationToken = default)
{
if (positions.Count == 0)
{
return;
}
await using var transaction = await _context.Database.BeginTransactionAsync(cancellationToken);
try
{
var ids = positions.Select(p => p.Id).ToList();
var trajets = await _context.Trajets
.Where(t => ids.Contains(t.Id))
.ToListAsync(cancellationToken);
var positionParId = positions.ToDictionary(p => p.Id, p => p.Position);
foreach (var trajet in trajets)
{
if (positionParId.TryGetValue(trajet.Id, out var nouvellePosition))
{
// Seule la Position est modifiée ; aucun autre champ n'est altéré (Exigence 11.9).
trajet.Position = nouvellePosition;
trajet.UpdatedAt = DateTime.UtcNow;
}
}
await _context.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
}
catch
{
await transaction.RollbackAsync(cancellationToken);
throw;
}
}
/// <inheritdoc />
public async Task<List<Trajet>> RenumeroterJourneeAsync(DateOnly date, CancellationToken cancellationToken = default)
{
await using var transaction = await _context.Database.BeginTransactionAsync(cancellationToken);
try
{
// Préservation de l'ordre relatif courant (Position croissante, Id en départage) — Property 15.
var trajets = (await _context.Trajets
.Where(t => t.Date == date)
.ToListAsync(cancellationToken))
.OrderBy(t => t.Position)
.ThenBy(t => t.Id)
.ToList();
decimal position = 1.0m;
foreach (var trajet in trajets)
{
trajet.Position = position;
trajet.UpdatedAt = DateTime.UtcNow;
position += 1.0m;
}
await _context.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
return trajets;
}
catch
{
await transaction.RollbackAsync(cancellationToken);
throw;
}
}
}