diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bef40e30..111c1491a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,58 @@ # Changelog +## [1.0.8-rc14] - unstable + +### Added + +* [PostIt] Ajout d'un `BillingQueryDetailsPageViewModel` et de sa page associee pour afficher le detail d'une commande billing depuis l'historique. +* [PostIt] Ajout d'un mode detail avec section metier (statut, date, description, motif, infos) et section technique repliable (code, client, provision, lieu, prestations). +* [PostIt] Ajout d'un badge de statut enrichi (couleur + pictogramme) sur le detail d'une commande pour visualiser l'etat en un coup d'oeil. +* [PostIt] Ajout d'un bloc d'actions rapide en tete du detail (`Retour`, `Ouvrir en edition`) pour eviter le scroll jusqu'au bas de page. +* [PostIt] Ajout d'un style monospace sur les metadonnees techniques (code billing, client, provision, lieu, prestations) pour faciliter la lecture des identifiants et valeurs brutes. + +### Changed + +* [PostIt] Le bouton d'ouverture depuis la liste billing ouvre maintenant une page de detail dediee avant l'eventuelle edition. +* [PostIt] Amelioration UX des pages billing: badges de statut colores, actions remontees en haut de page, et typographie monospace sur les metadonnees techniques. + +### Fixed + +* [Yavsc.Api] Correction d'un 500 sur le refresh du catalogue d'activites lorsque `Activity.Description` est `NULL` en base (nullabilite explicite + projection null-safe + gardes sur codes vides). +* [Yavsc.Api] Correction des erreurs 400/500 sur les routes billing (`Rdv`, `Brush`, `MBrush`) en imposant `ClientId` depuis l'utilisateur authentifie et en ignorant les champs server-owned lors de la validation modele. +* [Yavsc.Api] Correction du `PUT /api/v1/billing/Rdv/{id}`: mise a jour controlee de l'entite existante (et non remplacement brut du graphe JSON), ce qui supprime les `BadRequest` parasites. +* [Yavsc.Api] Correction PostgreSQL `timestamptz` sur RDV: normalisation UTC de `EventDate` sur `POST/PUT /api/v1/billing/Rdv` pour eviter l'erreur `Cannot write DateTime with Kind=Local`. +* [Yavsc.Api] Correction du flux FrontOffice accept/reject de query: sauvegarde avec contexte utilisateur et fallback d'injection pour `IBillingService` afin d'eviter les erreurs serveur en environnement de test. +* [Yavsc.Blogs] Correction des `BadRequest` sur `POST/PUT /api/v1/blogspot` avec payload JSON (PostIt): les proprietes de navigation/serveur (`Author`, `Tags`, `Comments`, audit) ne bloquent plus la validation. +* [Yavsc.Org] Correction du flux MVC de creation de commentaire: `SaveChangesAsync(userId)` est utilise pour renseigner les champs d'audit requis (`UserCreated`/`UserModified`). +* [Yavsc.Api.Test] Stabilisation des fixtures de seed billing: remplissage des metadonnees d'audit (`UserCreated`, `UserModified`, dates) pour eviter les echecs SQLite `NOT NULL`. + +## [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 @@ -13,12 +66,14 @@ * [PostIt] Les avatars ne sont plus relies en string sur `Image.Source`: ils sont telecharges et lies en `Bitmap`. * [Yavsc.Api.Client] `ActivityApiClient` accepte une base d'avatar dediee et construit les URLs avatar depuis l'autorite d'identification. +* [PostIt] Les clients Activites/Billing utilisent maintenant `ApiUrl` en lecture dynamique: un changement via Parametres prend effet sans redemarrer l'application (apres sauvegarde et rafraichissement de la page). * [PostIt] Le header de `MainPage` n'utilise plus `ScrollViewer`; remplacement par une barre de commandes basee sur `WrapPanel`. * [PostIt] Alignement de la navigation blogs: renommage `PushMainPageAsync` -> `PushBlogsPageAsync` et ajustement de `HomePageViewModel`. ### Fixed -néant +* [PostIt.Android] Correction d'un 404 sur la page Activites au premier lancement: la configuration embarquee pointait `ApiUrl` vers le host Blogs au lieu de l'API metier. +* [PostIt] Correction du bouton Sauver de la page Parametres: binding vers `SaveCommand` pour persister correctement `ApiUrl`/`BlogsApiUrl`. ## [1.0.8-rc11] - unstable 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/ActivitiesPageViewModelTests.cs b/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs index 2e8d3e544..04cc0f314 100644 --- a/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs @@ -39,6 +39,22 @@ public class ActivitiesPageViewModelTests Assert.Equal("https://business.example/api/v1/billing/Rdv", api.Paths[3]); } + [Fact] + public async Task ActivityApiClient_uses_updated_business_base_without_restart() + { + var api = new StubActivityApi(); + var baseUrl = "https://business-a.example/api/v1/"; + var client = new ActivityApiClient(api, () => baseUrl); + + await client.GetCatalogAsync(ct: TestContext.Current.CancellationToken); + + baseUrl = "https://business-b.example/api/v1/"; + await client.GetUsersAsync("brush", TestContext.Current.CancellationToken); + + Assert.Equal("https://business-a.example/api/v1/activity/catalog", api.Paths[0]); + Assert.Equal("https://business-b.example/api/v1/activity/brush/users", api.Paths[1]); + } + [Fact] public async Task RefreshAsync_loads_first_activity_then_specialization_performers() { 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