diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c7c92595..6df33e446 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog + +## [1.0.8-rc13] - unstable + +### Added + +* [PostIt] Integration d'un selecteur de lieu RDV base sur Mapsui (carte interactive dans le formulaire `Rdv`). +* [PostIt] Ajout d'un marqueur de position et d'une action de recentrage sur la carte RDV. +* [PostIt] Ajout d'un service de reverse geocoding pour suggerer une adresse a partir des coordonnees carte. +* [PostIt] Cache et debounce des resolutions d'adresse RDV pour limiter les appels reseau et lisser l'UX. +* [PostIt.Tests] Nouvelles non-regressions sur le panneau d'adresse suggeree RDV et le comportement de la carte. +* [Yavsc.Abstract] Activation de `#nullable enable annotations` sur les fichiers legacy avec annotations nullable. +* [Yavsc.Server] Activation de `#nullable enable annotations` sur les fichiers legacy avec annotations nullable. + +### Changed + +* [PostIt] Generalisation de la barre de statut d'action (severite explicite) sur pages principales, dialogues et formulaires billing. +* [PostIt] Harmonisation des messages de statut utilisateur en francais. +* [PostIt] Renforcement des gardes de navigation dans les flux de gestion des membres de cercle. +* [PostIt] Le flux RDV conserve l'adresse saisie manuellement et propose l'adresse resolue comme suggestion explicite. +* [PostIt] Le flux de geolocalisation RDV tolere les positions proches dans le cache de suggestion d'adresse. + +### Fixed + +* [PostIt.Desktop] Correction d'un crash au demarrage OIDC (`No authority specified`) via durcissement des valeurs par defaut de configuration d'authentification. +* [PostIt] Correction de la persistance des settings: l'etat runtime de statut n'est plus serialize dans le JSON utilisateur. +* [PostIt.Tests] Ajout d'un verrou de non-regression sur le premier chargement des settings. +* [PostIt] Correction du binding de la date RDV: `DatePicker.SelectedDate` est aligne sur un proxy `DateTimeOffset?` (`EventDateSelection`). + ## [1.0.8-rc12] - unstable ### Added diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props index 4dd4b2889..2f27340e9 100644 --- a/src/PostIt/Directory.Packages.props +++ b/src/PostIt/Directory.Packages.props @@ -16,6 +16,8 @@ + + diff --git a/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs b/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs index c7b5b926b..17fcc968c 100644 --- a/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs @@ -115,6 +115,89 @@ public class BillingCommandPageViewModelTests } } + [Fact] + public void ApplyLocationFromMap_sets_coordinates_and_updates_status() + { + var api = new RecordingApi(); + var client = new BillingApiClient(api, "https://business.example/api/v1/"); + var vm = + new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" } + .CreateCommandPageViewModel( + new ActivityInfo { Code = "dev", Name = "Développement" }, + new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" }, + client) as RdvViewModel; + + vm!.Address = string.Empty; + vm.ApplyLocationFromMap(48.85661234, 2.35224567); + + Assert.Equal(48.856612, vm.Latitude); + Assert.Equal(2.352246, vm.Longitude); + Assert.Contains("Position sélectionnée", vm.StatusMessage, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void EventDateSelection_round_trips_with_EventDate_for_DatePicker_binding() + { + var api = new RecordingApi(); + var client = new BillingApiClient(api, "https://business.example/api/v1/"); + var vm = + new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" } + .CreateCommandPageViewModel( + new ActivityInfo { Code = "dev", Name = "Développement" }, + new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" }, + client) as RdvViewModel; + + var selected = new DateTimeOffset(2026, 9, 7, 14, 30, 0, TimeSpan.FromHours(2)); + vm!.EventDateSelection = selected; + + Assert.Equal(selected.LocalDateTime, vm.EventDate); + Assert.Equal(vm.EventDate, vm.EventDateSelection!.Value.LocalDateTime); + } + + [Fact] + public void ApplyResolvedAddress_populates_empty_address_directly() + { + var api = new RecordingApi(); + var client = new BillingApiClient(api, "https://business.example/api/v1/"); + var vm = + new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" } + .CreateCommandPageViewModel( + new ActivityInfo { Code = "dev", Name = "Développement" }, + new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" }, + client) as RdvViewModel; + + vm!.Address = string.Empty; + vm.ApplyResolvedAddress("10 rue de Rivoli, 75001 Paris"); + + Assert.Equal("10 rue de Rivoli, 75001 Paris", vm.Address); + Assert.False(vm.HasSuggestedAddress); + } + + [Fact] + public void ApplyResolvedAddress_preserves_manual_address_and_exposes_suggestion() + { + var api = new RecordingApi(); + var client = new BillingApiClient(api, "https://business.example/api/v1/"); + var vm = + new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" } + .CreateCommandPageViewModel( + new ActivityInfo { Code = "dev", Name = "Développement" }, + new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" }, + client) as RdvViewModel; + + vm!.Address = "Saisie manuelle"; + vm.ApplyResolvedAddress("10 rue de Rivoli, 75001 Paris"); + + Assert.Equal("Saisie manuelle", vm.Address); + Assert.True(vm.HasSuggestedAddress); + Assert.Equal("10 rue de Rivoli, 75001 Paris", vm.SuggestedAddress); + + vm.ApplySuggestedAddressCommand.Execute(null); + + Assert.Equal("10 rue de Rivoli, 75001 Paris", vm.Address); + Assert.False(vm.HasSuggestedAddress); + } + [Fact] public async Task InitializeAsync_loads_prestations_for_brush_and_submit_posts_selected_prestation() { diff --git a/src/PostIt/PostIt.Tests/NominatimReverseGeocodingServiceTests.cs b/src/PostIt/PostIt.Tests/NominatimReverseGeocodingServiceTests.cs new file mode 100644 index 000000000..4eceb7a2f --- /dev/null +++ b/src/PostIt/PostIt.Tests/NominatimReverseGeocodingServiceTests.cs @@ -0,0 +1,72 @@ +using System.Net; +using System.Text; +using PostIt.Services; + +namespace PostIt.Tests; + +public class NominatimReverseGeocodingServiceTests +{ + [Fact] + public async Task TryResolveAddressAsync_formats_compact_street_address_from_nominatim_payload() + { + var handler = new StubHandler(""" + { + "display_name": "6, Place de l'Hôtel-de-Ville - Esplanade de la Libération, Paris, 75004, France", + "address": { + "house_number": "6", + "road": "Place de l'Hôtel-de-Ville - Esplanade de la Libération", + "postcode": "75004", + "city": "Paris" + } + } + """); + + var service = new NominatimReverseGeocodingService(new HttpClient(handler) + { + BaseAddress = new Uri("https://nominatim.openstreetmap.org/") + }); + + var result = await service.TryResolveAddressAsync(48.8566, 2.3522); + + Assert.Equal("6, Place de l'Hôtel-de-Ville - Esplanade de la Libération, 75004, Paris", result); + Assert.NotNull(handler.LastRequest); + Assert.Contains("reverse?format=jsonv2", handler.LastRequest!.RequestUri!.ToString(), StringComparison.Ordinal); + } + + [Fact] + public async Task TryResolveAddressAsync_returns_null_on_unsuccessful_response() + { + var handler = new StubHandler("{}", HttpStatusCode.TooManyRequests); + var service = new NominatimReverseGeocodingService(new HttpClient(handler) + { + BaseAddress = new Uri("https://nominatim.openstreetmap.org/") + }); + + var result = await service.TryResolveAddressAsync(48.8566, 2.3522); + + Assert.Null(result); + } + + private sealed class StubHandler : HttpMessageHandler + { + private readonly string _payload; + private readonly HttpStatusCode _statusCode; + + public HttpRequestMessage? LastRequest { get; private set; } + + public StubHandler(string payload, HttpStatusCode statusCode = HttpStatusCode.OK) + { + _payload = payload; + _statusCode = statusCode; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + LastRequest = request; + return Task.FromResult(new HttpResponseMessage(_statusCode) + { + Content = new StringContent(_payload, Encoding.UTF8, "application/json") + }); + } + } +} \ No newline at end of file diff --git a/src/PostIt/PostIt.Tests/RdvPageHeadlessTests.cs b/src/PostIt/PostIt.Tests/RdvPageHeadlessTests.cs new file mode 100644 index 000000000..fdf8689ab --- /dev/null +++ b/src/PostIt/PostIt.Tests/RdvPageHeadlessTests.cs @@ -0,0 +1,79 @@ +using Avalonia.Controls; +using Avalonia.Headless.XUnit; +using PostIt.ViewModels; +using PostIt.ViewModels.Commands; +using PostIt.Views.Commands; +using Yavsc.Abstract.Workflow; +using Yavsc.Api.Client; + +namespace PostIt.Tests; + +public class RdvPageHeadlessTests +{ + [AvaloniaFact] + public void Suggested_address_panel_is_hidden_by_default() + { + var page = CreatePage(out _); + + var panel = page.FindControl("SuggestedAddressPanel"); + var progress = page.FindControl("SuggestedAddressProgress"); + + Assert.NotNull(panel); + Assert.NotNull(progress); + Assert.False(panel!.IsVisible); + Assert.False(progress!.IsVisible); + } + + [AvaloniaFact] + public async Task Suggested_address_panel_and_spinner_follow_viewmodel_state() + { + var page = CreatePage(out var vm); + var panel = page.FindControl("SuggestedAddressPanel")!; + var progress = page.FindControl("SuggestedAddressProgress")!; + var applyButton = page.FindControl