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:
@@ -0,0 +1,121 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrajetKmTracker.Api.Data;
|
||||
using TrajetKmTracker.Api.Models.DTOs;
|
||||
using TrajetKmTracker.Api.Models.Entities;
|
||||
using TrajetKmTracker.Api.Services.Exceptions;
|
||||
|
||||
namespace TrajetKmTracker.Api.Services;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class MontantRegleService : IMontantRegleService
|
||||
{
|
||||
private const decimal TauxPrevisionnel = 0.80m;
|
||||
|
||||
private readonly AppDbContext _context;
|
||||
private readonly IBaremeService _baremeService;
|
||||
|
||||
public MontantRegleService(AppDbContext context, IBaremeService baremeService)
|
||||
{
|
||||
_context = context;
|
||||
_baremeService = baremeService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<MontantRegleDto> UpsertMontantRegleAsync(
|
||||
MontantRegleDto dto, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Défense en profondeur : le validateur FluentValidation impose déjà Montant >= 0,
|
||||
// mais le service refuse aussi les montants négatifs (Exigence 14.4).
|
||||
if (dto.Montant < 0m)
|
||||
{
|
||||
throw new MontantRegleNegatifException("Le montant réglé ne peut pas être négatif.");
|
||||
}
|
||||
|
||||
// Upsert par la clé métier (Annee, Mois, VehiculeId) — index unique
|
||||
// UX_MontantsRegles_Periode_Vehicule garantit l'unicité (Property 26, Exigence 14.3).
|
||||
var existant = await _context.MontantsRegles
|
||||
.FirstOrDefaultAsync(
|
||||
m => m.Annee == dto.Annee && m.Mois == dto.Mois && m.VehiculeId == dto.VehiculeId,
|
||||
cancellationToken);
|
||||
|
||||
if (existant is null)
|
||||
{
|
||||
var montantRegle = new MontantRegle
|
||||
{
|
||||
Annee = dto.Annee,
|
||||
Mois = dto.Mois,
|
||||
VehiculeId = dto.VehiculeId,
|
||||
Montant = dto.Montant,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
_context.MontantsRegles.Add(montantRegle);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return ToDto(montantRegle);
|
||||
}
|
||||
|
||||
existant.Montant = dto.Montant;
|
||||
existant.UpdatedAt = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return ToDto(existant);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<MontantRegleDto>> GetMontantsReglesAsync(
|
||||
int annee, int vehiculeId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var montants = await _context.MontantsRegles
|
||||
.AsNoTracking()
|
||||
.Where(m => m.Annee == annee && m.VehiculeId == vehiculeId)
|
||||
.OrderBy(m => m.Mois)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return montants.Select(ToDto).ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<MontantPrevisionnelDto> GetMontantPrevisionnelAsync(
|
||||
int annee, int mois, int vehiculeId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Puissance fiscale du véhicule concerné : détermine la tranche du barème.
|
||||
var vehicule = await _context.Vehicules
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(v => v.Id == vehiculeId, cancellationToken)
|
||||
?? throw new VehiculeNotFoundException($"Le véhicule d'identifiant {vehiculeId} est introuvable.");
|
||||
|
||||
// Total_Annuel_Partiel : somme des distances de l'année pour ce véhicule,
|
||||
// jusqu'au mois sélectionné inclus (les mois postérieurs sont exclus).
|
||||
// SQLite ne sait pas appliquer SUM sur une colonne decimal : on ramène les distances
|
||||
// concernées et on somme côté client (null traité comme 0).
|
||||
var distancesPartielles = await _context.Trajets
|
||||
.AsNoTracking()
|
||||
.Where(t => t.VehiculeId == vehiculeId
|
||||
&& t.Date.Year == annee
|
||||
&& t.Date.Month <= mois)
|
||||
.Select(t => t.Distance)
|
||||
.ToListAsync(cancellationToken);
|
||||
var totalAnnuelPartiel = distancesPartielles.Sum(d => d ?? 0m);
|
||||
|
||||
// Barème plein appliqué au total partiel (sans minoration).
|
||||
var bareme = await _baremeService.CalculerMontantAsync(
|
||||
annee, vehicule.PuissanceFiscale, totalAnnuelPartiel, cancellationToken);
|
||||
|
||||
var montantBaremePlein = Math.Round(bareme.Montant, 2, MidpointRounding.AwayFromZero);
|
||||
|
||||
// Montant_Previsionnel = 80% du barème plein, arrondi à 2 décimales (indicatif — Exigences 13.5/13.6).
|
||||
var montantPrevisionnel = Math.Round(bareme.Montant * TauxPrevisionnel, 2, MidpointRounding.AwayFromZero);
|
||||
|
||||
return new MontantPrevisionnelDto(
|
||||
Annee: annee,
|
||||
Mois: mois,
|
||||
VehiculeId: vehiculeId,
|
||||
TotalAnnuelPartielKm: Math.Round(totalAnnuelPartiel, 2, MidpointRounding.AwayFromZero),
|
||||
MontantBaremePlein: montantBaremePlein,
|
||||
MontantPrevisionnel: montantPrevisionnel);
|
||||
}
|
||||
|
||||
private static MontantRegleDto ToDto(MontantRegle m) =>
|
||||
new(m.Annee, m.Mois, m.VehiculeId, m.Montant);
|
||||
}
|
||||
Reference in New Issue
Block a user