From 58ddd91471e4f54d13985d3f0b6794d585f6cd1f Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 6 Sep 2026 20:24:19 +0100 Subject: [PATCH 1/2] postit: add billing query details page and refresh rc14 changelog --- CHANGELOG.md | 7 + .../Helpers/ServiceCollectionHelpers.cs | 1 + src/PostIt/PostIt/ViewLocator.cs | 6 +- .../ViewModels/BillingQueriesPageViewModel.cs | 19 +- .../BillingQueryDetailsPageViewModel.cs | 208 ++++++++++++++++++ .../PostIt/Views/BillingQueriesPage.axaml | 83 +++++-- .../Views/BillingQueryDetailsPage.axaml | 131 +++++++++++ .../Views/BillingQueryDetailsPage.axaml.cs | 17 ++ 8 files changed, 441 insertions(+), 31 deletions(-) create mode 100644 src/PostIt/PostIt/ViewModels/BillingQueryDetailsPageViewModel.cs create mode 100644 src/PostIt/PostIt/Views/BillingQueryDetailsPage.axaml create mode 100644 src/PostIt/PostIt/Views/BillingQueryDetailsPage.axaml.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0befe9eb3..60d126bfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,20 @@ ### 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). + ### 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`). diff --git a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs index 00a8bde04..c0831b290 100644 --- a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs +++ b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs @@ -57,6 +57,7 @@ public static class ServiceCollectionHelpers services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // ViewModels services.AddSingleton(settings); services.AddSingleton(api); diff --git a/src/PostIt/PostIt/ViewLocator.cs b/src/PostIt/PostIt/ViewLocator.cs index 0055a9e60..f866e1bac 100644 --- a/src/PostIt/PostIt/ViewLocator.cs +++ b/src/PostIt/PostIt/ViewLocator.cs @@ -19,7 +19,7 @@ namespace PostIt; public class ViewLocator : IDataTemplate { - public Control Build(object? data) + public Control Build(object? data) { try { @@ -49,10 +49,12 @@ public class ViewLocator : IDataTemplate AddCircleMemberDialogViewModel => services.GetRequiredService(), CirclesPageViewModel => services.GetRequiredService(), PostAclDialogViewModel => services.GetRequiredService(), + BillingQueriesPageViewModel => services.GetRequiredService(), + BillingQueryDetailsPageViewModel => services.GetRequiredService(), null => new TextBlock { Text = "No view for " }, _ => new TextBlock { Text = $"No view for {data.GetType().Name}" } }; } - public bool Match(object? data) => data is ViewModelBase; + public bool Match(object? data) => data is ViewModelBase; } diff --git a/src/PostIt/PostIt/ViewModels/BillingQueriesPageViewModel.cs b/src/PostIt/PostIt/ViewModels/BillingQueriesPageViewModel.cs index d3c097f43..bccf97167 100644 --- a/src/PostIt/PostIt/ViewModels/BillingQueriesPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/BillingQueriesPageViewModel.cs @@ -43,7 +43,7 @@ public partial class BillingQueriesPageViewModel : ViewModelBase, IActionStatusV ? $"Demandes en cours ({Form.Title})" : $"Commandes {Form.Title}"; public string ContextLabel => $"{Performer.UserName} · {Activity.Name}"; - public bool CanOpenDetails => !IsReadOnly; + public bool CanOpenDetails => true; public override bool CanNavigateNext { @@ -75,7 +75,7 @@ public partial class BillingQueriesPageViewModel : ViewModelBase, IActionStatusV public Task InitializeAsync() => RefreshAsync(); - private bool CanOpenSelectedQuery() => !IsReadOnly && SelectedQuery is not null; + private bool CanOpenSelectedQuery() => SelectedQuery is not null; [RelayCommand] public async Task RefreshAsync() @@ -114,12 +114,6 @@ public partial class BillingQueriesPageViewModel : ViewModelBase, IActionStatusV [RelayCommand(CanExecute = nameof(CanOpenSelectedQuery))] public async Task OpenSelectedQueryAsync() { - if (IsReadOnly) - { - this.SetWarningStatus("Mode lecture seule: l'ouverture en modification est désactivée."); - return; - } - if (SelectedQuery is null) { this.SetWarningStatus("Sélectionnez une commande."); @@ -136,8 +130,13 @@ public partial class BillingQueriesPageViewModel : ViewModelBase, IActionStatusV try { var details = await _billingClient.GetQueryAsync(Form.ActionName, SelectedQuery.Id).ConfigureAwait(true); - var vm = Form.CreateCommandPageViewModel(Activity, Performer, _billingClient); - await vm!.InitializeAsync(details).ConfigureAwait(true); + var vm = new BillingQueryDetailsPageViewModel( + Activity, + Performer, + Form, + _billingClient, + details, + IsReadOnly); await app.PushPageAsync(vm).ConfigureAwait(true); } catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden) diff --git a/src/PostIt/PostIt/ViewModels/BillingQueryDetailsPageViewModel.cs b/src/PostIt/PostIt/ViewModels/BillingQueryDetailsPageViewModel.cs new file mode 100644 index 000000000..cd13daaf1 --- /dev/null +++ b/src/PostIt/PostIt/ViewModels/BillingQueryDetailsPageViewModel.cs @@ -0,0 +1,208 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Avalonia; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using PostIt.Helpers; +using Yavsc; +using Yavsc.Abstract.Workflow; +using Yavsc.Api.Client; + +namespace PostIt.ViewModels; + +public partial class BillingQueryDetailsPageViewModel : ViewModelBase, IActionStatusViewModel +{ + private readonly BillingApiClient _billingClient; + private readonly BillingQueryDetailsDto _details; + + public ActivityInfo Activity { get; } + public ActivityUserDisplayItem Performer { get; } + public CommandFormSummary Form { get; } + public bool IsReadOnly { get; } + + public long Id => _details.Id; + public string Title => $"Detail commande #{_details.Id}"; + public string ContextLabel => $"{Performer.UserName} · {Activity.Name} · {Form.Title}"; + public string StatusLabel => _details.Status.ToString(); + public string StatusGlyph => GetStatusGlyph(_details.Status); + public string StatusBadgeBackground => GetStatusBadgeBackground(_details.Status); + public string StatusBadgeBorder => GetStatusBadgeBorder(_details.Status); + public string StatusBadgeForeground => GetStatusBadgeForeground(_details.Status); + public string TitleForeground => StatusBadgeForeground; + public string BillingCode => _details.BillingCode; + public string Description => EmptyAsPlaceholder(_details.Description, "(sans description)"); + public string Reason => EmptyAsPlaceholder(_details.Reason, "(aucun motif)"); + public string AdditionalInfo => EmptyAsPlaceholder(_details.AdditionalInfo, "(aucune info complementaire)"); + public string ClientId => EmptyAsPlaceholder(_details.ClientId, "(non renseigne)"); + public string EventDateLabel => _details.EventDate?.ToLocalTime().ToString("f") ?? "Date non precisee"; + public string ConsentLabel => _details.Consent ? "Oui" : "Non"; + public string ProvisionalLabel => _details.Provisional.HasValue ? _details.Provisional.Value.ToString("0.00") : "(non renseigne)"; + public string LocationLabel => BuildLocationLabel(_details.Location); + public string PrestationsLabel => BuildPrestationsLabel(_details); + public bool CanEdit => !IsReadOnly; + + [ObservableProperty] + public partial bool IsBusy { get; set; } + + [ObservableProperty] + public partial string StatusMessage { get; set; } = "Pret."; + + [ObservableProperty] + public partial StatusNotice ActionStatus { get; set; } = StatusNotice.Info("Pret."); + + public override bool CanNavigateNext + { + get => false; + protected set { _ = value; } + } + + public override bool CanNavigatePrevious + { + get => true; + protected set { _ = value; } + } + + public BillingQueryDetailsPageViewModel( + ActivityInfo activity, + ActivityUserDisplayItem performer, + CommandFormSummary form, + BillingApiClient billingClient, + BillingQueryDetailsDto details, + bool isReadOnly) + { + Activity = activity ?? throw new ArgumentNullException(nameof(activity)); + Performer = performer ?? throw new ArgumentNullException(nameof(performer)); + Form = form ?? throw new ArgumentNullException(nameof(form)); + _billingClient = billingClient ?? throw new ArgumentNullException(nameof(billingClient)); + _details = details ?? throw new ArgumentNullException(nameof(details)); + IsReadOnly = isReadOnly; + + this.SetInfoStatus("Details de commande charges."); + } + + [RelayCommand] + private async Task OpenEditorAsync() + { + if (IsReadOnly) + { + this.SetWarningStatus("Mode lecture seule: edition desactivee."); + return; + } + + var app = (App?)Application.Current; + if (app is null) + { + throw new InvalidOperationException("Application PostIt indisponible."); + } + + IsBusy = true; + try + { + var vm = Form.CreateCommandPageViewModel(Activity, Performer, _billingClient); + if (vm is null) + { + this.SetWarningStatus("Ce formulaire n'est pas encore pris en charge en edition."); + return; + } + + await vm.InitializeAsync(_details).ConfigureAwait(true); + await app.PushPageAsync(vm).ConfigureAwait(true); + } + catch (Exception ex) + { + this.SetErrorStatus($"Erreur lors de l'ouverture en edition: {ex.Message}"); + } + finally + { + IsBusy = false; + } + } + + [RelayCommand] + private async Task BackAsync() + { + var app = (App?)Application.Current; + if (app is null) + { + throw new InvalidOperationException("Application PostIt indisponible."); + } + + await app.GoBackAsync().ConfigureAwait(true); + } + + private static string EmptyAsPlaceholder(string? value, string placeholder) + => string.IsNullOrWhiteSpace(value) ? placeholder : value; + + private static string BuildLocationLabel(BillingLocationDto? location) + { + if (location is null) + { + return "(non renseignee)"; + } + + var text = EmptyAsPlaceholder(location.Address, "adresse vide"); + if (location.Latitude.HasValue && location.Longitude.HasValue) + { + text += $" ({location.Latitude.Value:0.####}, {location.Longitude.Value:0.####})"; + } + + return text; + } + + private static string BuildPrestationsLabel(BillingQueryDetailsDto details) + { + if (details.PrestationIds.Count > 0) + { + return string.Join(", ", details.PrestationIds.Select(static id => id.ToString())); + } + + return details.PrestationId.HasValue + ? details.PrestationId.Value.ToString() + : "(aucune)"; + } + + private static string GetStatusBadgeBackground(QueryStatus status) + => status switch + { + QueryStatus.Accepted => "#E6F7EC", + QueryStatus.InProgress => "#FFF4D6", + QueryStatus.Rejected => "#FDECEA", + QueryStatus.Failed => "#ECEFF1", + QueryStatus.Success => "#E8F8EF", + _ => "#EAF3FF", + }; + + private static string GetStatusBadgeBorder(QueryStatus status) + => status switch + { + QueryStatus.Accepted => "#2E7D32", + QueryStatus.InProgress => "#B26A00", + QueryStatus.Rejected => "#C62828", + QueryStatus.Failed => "#607D8B", + QueryStatus.Success => "#1E8E3E", + _ => "#2A5EA8", + }; + + private static string GetStatusBadgeForeground(QueryStatus status) + => status switch + { + QueryStatus.Accepted => "#1B5E20", + QueryStatus.InProgress => "#7A4A00", + QueryStatus.Rejected => "#8E0000", + QueryStatus.Failed => "#37474F", + QueryStatus.Success => "#145A2A", + _ => "#1A4178", + }; + + private static string GetStatusGlyph(QueryStatus status) + => status switch + { + QueryStatus.Accepted => "OK", + QueryStatus.InProgress => "~", + QueryStatus.Rejected => "!", + QueryStatus.Failed => "X", + QueryStatus.Success => "V", + _ => "i", + }; +} \ No newline at end of file diff --git a/src/PostIt/PostIt/Views/BillingQueriesPage.axaml b/src/PostIt/PostIt/Views/BillingQueriesPage.axaml index 2de514760..52b255129 100644 --- a/src/PostIt/PostIt/Views/BillingQueriesPage.axaml +++ b/src/PostIt/PostIt/Views/BillingQueriesPage.axaml @@ -5,38 +5,83 @@ x:Class="PostIt.Views.BillingQueriesPage" x:DataType="vm:BillingQueriesPageViewModel" Header="Commandes billing"> - - - + + + - -