From 7dd3821175c58519a8180a15000641a963dfdf8f Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 6 Sep 2026 16:32:59 +0100 Subject: [PATCH 01/11] postit-tests: lock first-load settings regression --- src/PostIt/PostIt.Tests/SettingsLoadTests.cs | 47 ++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/PostIt/PostIt.Tests/SettingsLoadTests.cs b/src/PostIt/PostIt.Tests/SettingsLoadTests.cs index 3f8dc02b9..069b6ac96 100644 --- a/src/PostIt/PostIt.Tests/SettingsLoadTests.cs +++ b/src/PostIt/PostIt.Tests/SettingsLoadTests.cs @@ -173,4 +173,51 @@ public class SettingsLoadTests Assert.NotNull(roundTrip); Assert.Equal("bonjour", roundTrip.SearchText); } + + /// + /// First-start regression guard: older settings payloads can + /// still contain ActionStatus from a previous write. This + /// runtime-only UI state must not be persisted anymore and must + /// not break deserialization when present. + /// + [Fact] + public void ActionStatus_is_not_persisted_and_legacy_payload_with_it_still_deserializes() + { + var settings = new PostIt.ViewModels.Settings + { + Authentication = new AuthenticationSettings + { + Authority = "https://example.test/", + ClientId = "postit-tests", + Scopes = new[] { "openid" } + } + }; + + var serialized = JsonSerializer.Serialize(settings); + Assert.DoesNotContain("\"ActionStatus\"", serialized, StringComparison.Ordinal); + + const string legacyPayload = """ + { + "Authentication": { + "Authority": "https://example.test/", + "ClientId": "postit-tests", + "Scopes": ["openid"], + "RedirectUri": "postit://callback" + }, + "DarkMode": false, + "BlogsApiUrl": "https://blogs.example.test/api/v1/", + "ApiUrl": "https://api.example.test/api/v1/", + "SearchText": "hello", + "ActionStatus": { + "Message": "runtime only", + "Severity": "Error" + } + } + """; + + var roundTrip = JsonSerializer.Deserialize(legacyPayload); + + Assert.NotNull(roundTrip); + Assert.Equal("hello", roundTrip.SearchText); + } } From 91e78a12703ab91efcffc31dfb3e5ce934eb9bad Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 6 Sep 2026 16:58:39 +0100 Subject: [PATCH 02/11] postit: add mapsui location picker to rdv form --- src/PostIt/Directory.Packages.props | 2 + .../BillingCommandPageViewModelTests.cs | 20 ++++++ src/PostIt/PostIt/PostIt.csproj | 2 + .../ViewModels/Commands/RdvViewModel.cs | 14 ++++ .../PostIt/Views/Commands/RdvPage.axaml | 12 +++- .../PostIt/Views/Commands/RdvPage.axaml.cs | 65 +++++++++++++++++++ 6 files changed, 114 insertions(+), 1 deletion(-) 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..da366d284 100644 --- a/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs @@ -115,6 +115,26 @@ 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 async Task InitializeAsync_loads_prestations_for_brush_and_submit_posts_selected_prestation() { diff --git a/src/PostIt/PostIt/PostIt.csproj b/src/PostIt/PostIt/PostIt.csproj index 177e346d3..09632bdf8 100644 --- a/src/PostIt/PostIt/PostIt.csproj +++ b/src/PostIt/PostIt/PostIt.csproj @@ -29,6 +29,8 @@ None All + + diff --git a/src/PostIt/PostIt/ViewModels/Commands/RdvViewModel.cs b/src/PostIt/PostIt/ViewModels/Commands/RdvViewModel.cs index afd0c54bd..44293f03e 100644 --- a/src/PostIt/PostIt/ViewModels/Commands/RdvViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/Commands/RdvViewModel.cs @@ -113,6 +113,20 @@ public partial class RdvViewModel : BillingCommandPageViewModel }; } + public void ApplyLocationFromMap(double latitude, double longitude) + { + Latitude = Math.Round(latitude, 6); + Longitude = Math.Round(longitude, 6); + + if (string.IsNullOrWhiteSpace(Address)) + { + this.SetInfoStatus("Position sélectionnée sur la carte. Complétez l'adresse puis envoyez la commande."); + return; + } + + this.SetInfoStatus("Position sélectionnée sur la carte."); + } + protected override async Task SubmitAsync() { diff --git a/src/PostIt/PostIt/Views/Commands/RdvPage.axaml b/src/PostIt/PostIt/Views/Commands/RdvPage.axaml index 4c88b0108..cee3c5c79 100644 --- a/src/PostIt/PostIt/Views/Commands/RdvPage.axaml +++ b/src/PostIt/PostIt/Views/Commands/RdvPage.axaml @@ -1,12 +1,13 @@ - + + + ("LocationMap"); + if (_locationMap is null) + return; + + var map = new Map(); + map.Layers.Add(OpenStreetMap.CreateTileLayer()); + _locationMap.Map = map; + _locationMap.MapTapped += OnMapTapped; + + DataContextChanged += OnDataContextChanged; + CenterFromViewModel(); + } + + private void OnDataContextChanged(object? sender, System.EventArgs e) + { + CenterFromViewModel(); + } + + private void OnMapTapped(object? sender, MapEventArgs e) + { + if (DataContext is not RdvViewModel vm) + return; + + var (longitude, latitude) = SphericalMercator.ToLonLat(e.WorldPosition.X, e.WorldPosition.Y); + vm.ApplyLocationFromMap(latitude, longitude); + CenterMap(latitude, longitude, zoomLevel: 13); + } + + private void CenterFromViewModel() + { + if (DataContext is not RdvViewModel vm) + { + CenterMap(48.8566, 2.3522, zoomLevel: 4); + return; + } + + if (vm.Latitude.HasValue && vm.Longitude.HasValue) + { + CenterMap(vm.Latitude.Value, vm.Longitude.Value, zoomLevel: 13); + return; + } + + CenterMap(48.8566, 2.3522, zoomLevel: 4); + } + + private void CenterMap(double latitude, double longitude, int zoomLevel) + { + if (_locationMap?.Map is null) + return; + + var (x, y) = SphericalMercator.FromLonLat(longitude, latitude); + _locationMap.Map.Navigator.CenterOn(x, y); + _locationMap.Map.Navigator.ZoomToLevel(zoomLevel); + } } From 9806bc0e0aefc9fbbd0a7a3841d52c9f64c0c169 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 6 Sep 2026 17:05:18 +0100 Subject: [PATCH 03/11] postit: add rdv map marker and recenter action --- .../PostIt/Views/Commands/RdvPage.axaml | 20 ++-- .../PostIt/Views/Commands/RdvPage.axaml.cs | 92 ++++++++++++++++++- 2 files changed, 101 insertions(+), 11 deletions(-) diff --git a/src/PostIt/PostIt/Views/Commands/RdvPage.axaml b/src/PostIt/PostIt/Views/Commands/RdvPage.axaml index cee3c5c79..a3cb477b1 100644 --- a/src/PostIt/PostIt/Views/Commands/RdvPage.axaml +++ b/src/PostIt/PostIt/Views/Commands/RdvPage.axaml @@ -40,12 +40,20 @@ -