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,103 @@
using Microsoft.AspNetCore.Mvc;
using TrajetKmTracker.Api.Models.DTOs;
using TrajetKmTracker.Api.Services;
using TrajetKmTracker.Api.Services.Exceptions;
namespace TrajetKmTracker.Api.Controllers;
/// <summary>
/// Endpoints « Barème et remboursements » : montant prévisionnel (80% indicatif),
/// montants réglés (saisie/consultation) et régularisation annuelle (barème plein).
/// </summary>
[ApiController]
[Route("api")]
public class RemboursementController : ControllerBase
{
private readonly IMontantRegleService _montantRegleService;
private readonly IRemboursementService _remboursementService;
public RemboursementController(
IMontantRegleService montantRegleService,
IRemboursementService remboursementService)
{
_montantRegleService = montantRegleService;
_remboursementService = remboursementService;
}
/// <summary>
/// Retourne le Montant_Previsionnel indicatif (80% du barème plein sur le total annuel partiel)
/// pour un (année, mois, véhicule). Purement informatif — hors régularisation (Exigences 13.5/13.6).
/// </summary>
[HttpGet("montant-previsionnel/{annee:int}/{mois:int}")]
[ProducesResponseType(typeof(MontantPrevisionnelDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<MontantPrevisionnelDto>> GetMontantPrevisionnel(
int annee, int mois, [FromQuery] int vehiculeId, CancellationToken cancellationToken)
{
try
{
var dto = await _montantRegleService.GetMontantPrevisionnelAsync(
annee, mois, vehiculeId, cancellationToken);
return Ok(dto);
}
catch (VehiculeNotFoundException ex)
{
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
}
}
/// <summary>
/// Retourne les Montant_Regle saisis pour une (année, véhicule), triés par mois croissant.
/// </summary>
[HttpGet("montants-regles/{annee:int}")]
[ProducesResponseType(typeof(IReadOnlyList<MontantRegleDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<IReadOnlyList<MontantRegleDto>>> GetMontantsRegles(
int annee, [FromQuery] int vehiculeId, CancellationToken cancellationToken)
{
var montants = await _montantRegleService.GetMontantsReglesAsync(annee, vehiculeId, cancellationToken);
return Ok(montants);
}
/// <summary>
/// Enregistre ou met à jour (upsert) un Montant_Regle par (année, mois, véhicule).
/// Rejette un montant négatif avec un code HTTP 400 (Exigence 14.4).
/// </summary>
[HttpPut("montants-regles")]
[ProducesResponseType(typeof(MontantRegleDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<MontantRegleDto>> UpsertMontantRegle(
[FromBody] MontantRegleDto dto, CancellationToken cancellationToken)
{
try
{
var result = await _montantRegleService.UpsertMontantRegleAsync(dto, cancellationToken);
return Ok(result);
}
catch (MontantRegleNegatifException ex)
{
return Problem(detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
}
}
/// <summary>
/// Retourne la synthèse annuelle de régularisation (barème plein) pour une (année, véhicule) :
/// tranche annuelle, 12 lignes mensuelles et totaux (km annuel, réglé, Montant_Reel, Regularisation).
/// </summary>
[HttpGet("regularisation/{annee:int}")]
[ProducesResponseType(typeof(RegularisationAnnuelleDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<RegularisationAnnuelleDto>> GetRegularisation(
int annee, [FromQuery] int vehiculeId, CancellationToken cancellationToken)
{
try
{
var dto = await _remboursementService.GetRegularisationAnnuelleAsync(
annee, vehiculeId, cancellationToken);
return Ok(dto);
}
catch (VehiculeNotFoundException ex)
{
return Problem(detail: ex.Message, statusCode: StatusCodes.Status404NotFound);
}
}
}