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,133 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using TrajetKmTracker.Api.Models.DTOs;
|
||||
using TrajetKmTracker.Api.Services.Exceptions;
|
||||
|
||||
namespace TrajetKmTracker.Api.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Client typé de l'API BAN. Appelle <c>GET /search/?q={query}&limit=5</c>,
|
||||
/// parse la réponse GeoJSON (FeatureCollection) et mappe les features en
|
||||
/// <see cref="BANAdresseDto"/>. Lève <see cref="BANIndisponibleException"/>
|
||||
/// en cas d'échec ou de timeout.
|
||||
/// </summary>
|
||||
public class BANClient : IBANClient
|
||||
{
|
||||
private const int MaxResultats = 5;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public BANClient(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<BANAdresseDto>> RechercherAsync(
|
||||
string query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return Array.Empty<BANAdresseDto>();
|
||||
}
|
||||
|
||||
var requestUri = $"/search/?q={Uri.EscapeDataString(query)}&limit={MaxResultats}";
|
||||
|
||||
BANFeatureCollection? collection;
|
||||
try
|
||||
{
|
||||
using var response = await _httpClient.GetAsync(requestUri, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
collection = await response.Content.ReadFromJsonAsync<BANFeatureCollection>(
|
||||
JsonOptions, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Le timeout configuré sur le HttpClient déclenche une annulation
|
||||
// non demandée par l'appelant.
|
||||
throw new BANIndisponibleException(
|
||||
"L'appel à l'API BAN a dépassé le délai imparti (timeout).");
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
throw new BANIndisponibleException(
|
||||
"L'API BAN est indisponible ou a renvoyé une réponse en erreur.", ex);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
throw new BANIndisponibleException(
|
||||
"La réponse de l'API BAN n'a pas pu être interprétée.", ex);
|
||||
}
|
||||
|
||||
if (collection?.Features is null || collection.Features.Count == 0)
|
||||
{
|
||||
return Array.Empty<BANAdresseDto>();
|
||||
}
|
||||
|
||||
var resultats = new List<BANAdresseDto>(collection.Features.Count);
|
||||
foreach (var feature in collection.Features)
|
||||
{
|
||||
var coordinates = feature.Geometry?.Coordinates;
|
||||
if (coordinates is null || coordinates.Count < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// GeoJSON : coordinates = [longitude, latitude].
|
||||
var longitude = coordinates[0];
|
||||
var latitude = coordinates[1];
|
||||
|
||||
resultats.Add(new BANAdresseDto(
|
||||
Label: feature.Properties?.Label ?? string.Empty,
|
||||
Latitude: latitude,
|
||||
Longitude: longitude,
|
||||
Postcode: feature.Properties?.Postcode ?? string.Empty,
|
||||
City: feature.Properties?.City ?? string.Empty));
|
||||
}
|
||||
|
||||
return resultats;
|
||||
}
|
||||
|
||||
// --- Modèles internes de désérialisation de la réponse GeoJSON BAN ---
|
||||
|
||||
private sealed class BANFeatureCollection
|
||||
{
|
||||
[JsonPropertyName("features")]
|
||||
public List<BANFeature>? Features { get; set; }
|
||||
}
|
||||
|
||||
private sealed class BANFeature
|
||||
{
|
||||
[JsonPropertyName("properties")]
|
||||
public BANProperties? Properties { get; set; }
|
||||
|
||||
[JsonPropertyName("geometry")]
|
||||
public BANGeometry? Geometry { get; set; }
|
||||
}
|
||||
|
||||
private sealed class BANProperties
|
||||
{
|
||||
[JsonPropertyName("label")]
|
||||
public string? Label { get; set; }
|
||||
|
||||
[JsonPropertyName("postcode")]
|
||||
public string? Postcode { get; set; }
|
||||
|
||||
[JsonPropertyName("city")]
|
||||
public string? City { get; set; }
|
||||
}
|
||||
|
||||
private sealed class BANGeometry
|
||||
{
|
||||
// [longitude, latitude]
|
||||
[JsonPropertyName("coordinates")]
|
||||
public List<double>? Coordinates { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user