From fc820362b9ca0d50664afff53fb0d62c2f96c5cd Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Fri, 4 Sep 2026 13:19:31 +0100 Subject: [PATCH] refactoring the command form creation --- .vscode/tasks.json | 2 +- CONTRIBUTING.md | 13 + README.md | 4 + doc/README.md | 3 + doc/agent-intent-matrix.md | 20 + .../BillingCommandPageViewModelTests.cs | 112 ++-- src/PostIt/PostIt/Helpers/FormHelpers.cs | 45 ++ .../Helpers/ServiceCollectionHelpers.cs | 4 +- src/PostIt/PostIt/ViewLocator.cs | 5 +- .../ViewModels/BillingCommandPageViewModel.cs | 490 ------------------ .../ViewModels/BillingQueriesPageViewModel.cs | 2 +- .../ViewModels/CommandFormsPageViewModel.cs | 3 +- .../Commands/BillingCommandPageViewModel.cs | 121 +++++ .../ViewModels/Commands/BrushViewModel.cs | 182 +++++++ .../ViewModels/Commands/MBrushViewModel cs | 16 + .../ViewModels/Commands/RdvViewModel.cs | 210 ++++++++ src/PostIt/PostIt/ViewModels/ViewModelBase.cs | 2 +- src/PostIt/PostIt/Views/ActivitiesPage.axaml | 4 +- .../PostIt/Views/BillingCommandPage.axaml | 138 ----- .../PostIt/Views/CommandFormsPage.axaml | 4 +- .../PostIt/Views/Commands/BrushPage.axaml | 142 +++++ .../PostIt/Views/Commands/BrushPage.axaml.cs | 13 + .../PostIt/Views/Commands/RdvPage.axaml | 95 ++++ .../RdvPage.axaml.cs} | 8 +- 24 files changed, 940 insertions(+), 698 deletions(-) create mode 100644 doc/agent-intent-matrix.md create mode 100644 src/PostIt/PostIt/Helpers/FormHelpers.cs delete mode 100644 src/PostIt/PostIt/ViewModels/BillingCommandPageViewModel.cs create mode 100644 src/PostIt/PostIt/ViewModels/Commands/BillingCommandPageViewModel.cs create mode 100644 src/PostIt/PostIt/ViewModels/Commands/BrushViewModel.cs create mode 100644 src/PostIt/PostIt/ViewModels/Commands/MBrushViewModel cs create mode 100644 src/PostIt/PostIt/ViewModels/Commands/RdvViewModel.cs delete mode 100644 src/PostIt/PostIt/Views/BillingCommandPage.axaml create mode 100644 src/PostIt/PostIt/Views/Commands/BrushPage.axaml create mode 100644 src/PostIt/PostIt/Views/Commands/BrushPage.axaml.cs create mode 100644 src/PostIt/PostIt/Views/Commands/RdvPage.axaml rename src/PostIt/PostIt/Views/{BillingCommandPage.axaml.cs => Commands/RdvPage.axaml.cs} (63%) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index fd46437a..a30c9c06 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -7,7 +7,7 @@ "fileLocation": ["relative", "${workspaceFolder}"], "source": "dotnet", "pattern": { - "regexp": "^\\s+(.*)\\((\\d+),(\\d+)\\):\\s+(error|warning) (.+): (.*)$", + "regexp": "^\\s*(.*)\\((\\d+),(\\d+)\\):\\s+(error|warning) (.*)$", "file": 1, "line": 2, "column": 3, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fd408c5c..559da62a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,6 +49,19 @@ Les tests sont répartis en : item « Tests d'intégration smoke par BC ». - `src/PostIt.Tests/` — tests unitaires du client desktop PostIt. +## Onboarding assiste par agents IA + +Pour accelerer la prise en main du depot avec Copilot/Plan/Explore : + +- Parcours pas-a-pas : [doc/onboarding-agents.md](./doc/onboarding-agents.md) +- Playbook d'usage des agents : [doc/agent-playbook.md](./doc/agent-playbook.md) +- Matrice intentions -> agent -> preuves : [doc/agent-intent-matrix.md](./doc/agent-intent-matrix.md) + +Regle minimale en contribution assistee par agent : +- expliciter l'impact architecture, +- justifier le niveau de tests execute, +- documenter les risques residuels. + ## Le CHANGELOG.md Le `CHANGELOG.md` est un document de changement de version diff --git a/README.md b/README.md index b132f3f2..549348f4 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,10 @@ sous [`doc/`](./doc/). Voir l'[index de la documentation](./doc/README.md) pour le sommaire complet. La racine de l'architecture est [Architecture.md](./doc/Architecture.md). +Pour une prise en main guidee avec agents IA: +- parcours onboarding: [doc/onboarding-agents.md](./doc/onboarding-agents.md) +- playbook d'usage: [doc/agent-playbook.md](./doc/agent-playbook.md) + # Construction et déploiement diff --git a/doc/README.md b/doc/README.md index 912a2e56..fb9bea94 100644 --- a/doc/README.md +++ b/doc/README.md @@ -18,6 +18,9 @@ La racine de l'architecture est [Architecture.md](Architecture.md). | [architecture/postit.md](architecture/postit.md) | PostIt — topologie des projets, ViewLocator custo, navigation, DI, conventions de binding | | [architecture/decoupage-organisation.md](architecture/decoupage-organisation.md) | Découpage des projets .NET (Abstract, Server, Org, Api, Blogs, Web, Org.Tests) | | [testing.md](testing.md) | Stratégie de test : conventions des dossiers, EF Core in-memory, auth stubs, scaffold partagé | +| [onboarding-agents.md](onboarding-agents.md) | Parcours pas-à-pas pour prise en main agents IA + architecture + tests | +| [agent-playbook.md](agent-playbook.md) | Playbook d'usage de Copilot, Plan, Explore avec scénarios et anti-patterns | +| [agent-intent-matrix.md](agent-intent-matrix.md) | Matrice intentions développeur -> agent -> preuves attendues | ## Roadmap & design exploration diff --git a/doc/agent-intent-matrix.md b/doc/agent-intent-matrix.md new file mode 100644 index 00000000..975cd7b7 --- /dev/null +++ b/doc/agent-intent-matrix.md @@ -0,0 +1,20 @@ +# Matrice intentions -> agent -> preuves + +Cette matrice aide a choisir rapidement l'agent adapte et a exiger +une sortie verifiable. + +| Intention developpeur | Agent principal | Entrees minimales | Sortie minimale attendue | Verification | +|---|---|---|---|---| +| Comprendre un BC avant changement | Explore | BC cible, profondeur, contrainte de perimetre | Composants, points d'entree, tests relies, risques | Lire les fichiers cites + confirmer tests proposes | +| Decomposer une tache transverse | Plan | Objectif, contraintes, definition of done | Etapes ordonnees, dependances, criteres de verif | Verifier que chaque etape a une preuve observable | +| Implementer une modif locale | Copilot | Fichier cible, comportement attendu, conventions | Patch minimal, justification courte | Build/test du projet impacte | +| Ajouter un test smoke | Copilot (+Explore) | Route/endpoint, projet de test cible | Test + commande cible | Execution test cible | +| Corriger une regression | Plan + Copilot | Symptome, zone suspecte, test attendu | Fix + test NonRegression | Test rouge avant, vert apres | +| Diagnostiquer flux PostIt/OIDC | Explore + Plan | Flux, symptome, plateforme | Carte du flux + hypotheses testables | Verification manuelle + tests existants | + +## Regles d'arbitrage + +- Si l'intention est "comprendre": commencer par Explore. +- Si l'intention est "orchestrer": commencer par Plan. +- Si l'intention est "produire": utiliser Copilot apres cadrage. +- Si une sortie n'inclut pas de preuve, elle est incomplete. diff --git a/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs b/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs index 5b6f51bf..924182f1 100644 --- a/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs @@ -1,7 +1,8 @@ -using System.Net.Http; using System.Text.Json; +using PostIt.Helpers; using PostIt.Services; using PostIt.ViewModels; +using PostIt.ViewModels.Commands; using Yavsc; using Yavsc.Abstract.Workflow; using Yavsc.Api.Client; @@ -16,19 +17,19 @@ public class BillingCommandPageViewModelTests { var api = new RecordingApi(); var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = new BillingCommandPageViewModel( + 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" }, - new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" }, - client) - { - EventDateText = "2026-09-02 14:30", - Reason = "Point de cadrage", - Address = "1 rue du Test", - LatitudeText = "48.8566", - LongitudeText = "2.3522", - Consent = true, - }; + client) as RdvViewModel; + + vm.EventDate = DateTime.Parse("2026-09-02 14:30"); + vm.Reason = "Point de cadrage"; + vm.Address = "1 rue du Test"; + vm.Latitude = 48.8566; + vm.Longitude = 2.3522; + vm.Consent = true; await vm.SubmitCommand.ExecuteAsync(null); @@ -47,10 +48,12 @@ public class BillingCommandPageViewModelTests { var api = new RecordingApi(); var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = new BillingCommandPageViewModel( + + var vm = + new CommandFormSummary { Id = 13, ActionName = "Book", Title = "Réservation" } + .CreateCommandPageViewModel( new ActivityInfo { Code = "book", Name = "Book" }, new ActivityUserDisplayItem { PerformerId = "perf-2", UserName = "Bob" }, - new CommandFormSummary { Id = 13, ActionName = "Book", Title = "Réservation" }, client); await vm.SubmitCommand.ExecuteAsync(null); @@ -64,19 +67,19 @@ public class BillingCommandPageViewModelTests { var api = new RecordingApi(); var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = new BillingCommandPageViewModel( + 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" }, - new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" }, - client) - { - EventDateText = "2026-09-02 14:30", - Reason = "Point de cadrage", - Address = "1 rue du Test", - LatitudeText = string.Empty, - LongitudeText = string.Empty, - Consent = true, - }; + client) as RdvViewModel; + vm.EventDate = DateTime.Parse("2026-09-02 14:30"); + vm.Reason = "Point de cadrage"; + vm.Address = "1 rue du Test"; + vm.Latitude = 0; + vm.Longitude = 0; + vm.Consent = true; await vm.SubmitCommand.ExecuteAsync(null); @@ -97,16 +100,17 @@ public class BillingCommandPageViewModelTests var api = new RecordingApi(); var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = new BillingCommandPageViewModel( + 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" }, - new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" }, - client); + client) as RdvViewModel; await vm.UseCurrentLocationCommand.ExecuteAsync(null); - Assert.Equal("48.8566", vm.LatitudeText); - Assert.Equal("2.3522", vm.LongitudeText); + Assert.Equal(48.8566, vm.Latitude); + Assert.Equal(2.3522, vm.Longitude); } finally { @@ -126,19 +130,18 @@ public class BillingCommandPageViewModelTests } }; var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = new BillingCommandPageViewModel( + var vm = + new CommandFormSummary { Id = 13, ActionName = "Brush", Title = "Coupe" } + .CreateCommandPageViewModel( new ActivityInfo { Code = "brush", Name = "Brush" }, new ActivityUserDisplayItem { PerformerId = "perf-2", UserName = "Bob" }, - new CommandFormSummary { Id = 13, ActionName = "Brush", Title = "Coupe" }, - client) - { - EventDateText = "2026-09-02 14:30", - Address = "1 rue du Test", - LatitudeText = "48.8566", - LongitudeText = "2.3522", - Consent = true, - AdditionalInfo = "Prévoir shampoing", - }; + client) as BrushViewModel; + vm.EventDate = DateTime.Parse("2026-09-02 14:30"); + vm.Address = "1 rue du Test"; + vm.Latitude = 48.8566; + vm.Longitude = 2.3522; + vm.Consent = true; + vm.AdditionalInfo = "Prévoir shampoing"; await vm.InitializeAsync(); vm.SelectedPrestation = vm.AvailablePrestations[1]; @@ -162,18 +165,17 @@ public class BillingCommandPageViewModelTests } }; var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = new BillingCommandPageViewModel( + var vm = + new CommandFormSummary { Id = 14, ActionName = "MBrush", Title = "Coupe groupée" } + .CreateCommandPageViewModel( new ActivityInfo { Code = "mbrush", Name = "MBrush" }, new ActivityUserDisplayItem { PerformerId = "perf-3", UserName = "Cara" }, - new CommandFormSummary { Id = 14, ActionName = "MBrush", Title = "Coupe groupée" }, - client) - { - EventDateText = "2026-09-03 10:00", - Address = "2 rue du Test", - LatitudeText = "48.8567", - LongitudeText = "2.3523", - Consent = true, - }; + client) as PostIt.ViewModels.Commands.BrushViewModel; + vm.EventDate = DateTime.Parse("2026-09-03 10:00"); + vm.Address = "2 rue du Test"; + vm.Latitude = 48.8567; + vm.Longitude = 2.3523; + vm.Consent = true; await vm.InitializeAsync(); vm.MultiPrestations[0].IsSelected = true; @@ -200,12 +202,12 @@ public class BillingCommandPageViewModelTests } }; var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = new BillingCommandPageViewModel( + var vm = + new CommandFormSummary { Id = 13, ActionName = "Brush", Title = "Coupe" } + .CreateCommandPageViewModel( new ActivityInfo { Code = "brush", Name = "Brush" }, new ActivityUserDisplayItem { PerformerId = "perf-2", UserName = "Bob" }, - new CommandFormSummary { Id = 13, ActionName = "Brush", Title = "Coupe" }, - client); - + client) as BrushViewModel; await vm.InitializeAsync(new BillingQueryDetailsDto { Id = 77, diff --git a/src/PostIt/PostIt/Helpers/FormHelpers.cs b/src/PostIt/PostIt/Helpers/FormHelpers.cs new file mode 100644 index 00000000..44266c10 --- /dev/null +++ b/src/PostIt/PostIt/Helpers/FormHelpers.cs @@ -0,0 +1,45 @@ +using System; +using PostIt.ViewModels; +using Yavsc.Abstract.Workflow; +using Yavsc.Api.Client; + +namespace PostIt.Helpers; + +public static class FormHelpers +{ + public static BillingCommandPageViewModel + CreateCommandPageViewModel( + this CommandFormSummary form, + ActivityInfo activity, + ActivityUserDisplayItem performer, + BillingApiClient billingClient) + { + + string formVMName = form.ActionName + "ViewModel"; + + string formOnActivityVMName = activity.Code + formVMName; + + var vmType = Type.GetType(formOnActivityVMName); + if (vmType == null) + { + vmType = Type.GetType(formVMName); + } + if (vmType == null) + { + throw new InvalidOperationException($"Cannot find type '{formOnActivityVMName}' or '{formVMName}'"); + } + if (!typeof(BillingCommandPageViewModel).IsAssignableFrom(vmType)) + { + throw new InvalidOperationException($"The type '{formOnActivityVMName}' or '{formVMName}' is not a BillingCommandPageViewModel"); + } + + var vm = Activator.CreateInstance(vmType); + + if (vm == null) + { + throw new InvalidOperationException($"Cannot create instance of '{formOnActivityVMName}' or '{formVMName}'"); + } + + return vm as BillingCommandPageViewModel ?? throw new InvalidOperationException($"The type '{formOnActivityVMName}' or '{formVMName}' is not a BillingCommandPageViewModel"); + } +} diff --git a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs index e86d1c4c..ff532b2b 100644 --- a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs +++ b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using PostIt.Services; using PostIt.ViewModels; using PostIt.Views; +using PostIt.Views.Commands; using Yavsc.Api.Client; namespace PostIt.Helpers; @@ -49,7 +50,8 @@ public static class ServiceCollectionHelpers services.AddSingleton(); services.AddSingleton(); services.AddTransient(); - services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddTransient(); // ViewModels services.AddSingleton(settings); diff --git a/src/PostIt/PostIt/ViewLocator.cs b/src/PostIt/PostIt/ViewLocator.cs index 2fd51633..96001115 100644 --- a/src/PostIt/PostIt/ViewLocator.cs +++ b/src/PostIt/PostIt/ViewLocator.cs @@ -4,7 +4,9 @@ using Avalonia.Controls; using Avalonia.Controls.Templates; using Microsoft.Extensions.DependencyInjection; using PostIt.ViewModels; +using PostIt.ViewModels.Commands; using PostIt.Views; +using PostIt.Views.Commands; namespace PostIt; @@ -41,8 +43,7 @@ public class ViewLocator : IDataTemplate HomePageViewModel => services.GetRequiredService(), ActivitiesPageViewModel => services.GetRequiredService(), CommandFormsPageViewModel => services.GetRequiredService(), - BillingCommandPageViewModel => services.GetRequiredService(), - BillingQueriesPageViewModel => services.GetRequiredService(), + RdvViewModel => services.GetRequiredService(), SignaturePageViewModel => services.GetRequiredService(), AddCircleMemberDialogViewModel => services.GetRequiredService(), CirclesPageViewModel => services.GetRequiredService(), diff --git a/src/PostIt/PostIt/ViewModels/BillingCommandPageViewModel.cs b/src/PostIt/PostIt/ViewModels/BillingCommandPageViewModel.cs deleted file mode 100644 index cfd23086..00000000 --- a/src/PostIt/PostIt/ViewModels/BillingCommandPageViewModel.cs +++ /dev/null @@ -1,490 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; -using System.Net; -using System.Net.Http; -using System.Linq; -using System.Threading.Tasks; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PostIt.Services; -using Yavsc; -using Yavsc.Abstract.Workflow; -using Yavsc.Api.Client; -using Yavsc.Models.Billing; -using Yavsc.Models.Haircut; - -namespace PostIt.ViewModels; - -public partial class BillingCommandPageViewModel : ViewModelBase -{ - private readonly BillingApiClient _billingClient; - - public ActivityInfo Activity { get; } - public ActivityUserDisplayItem Performer { get; } - public CommandFormSummary Form { get; } - - [ObservableProperty] - public partial bool IsBusy { get; set; } - - [ObservableProperty] - public partial string StatusMessage { get; set; } - - [ObservableProperty] - public partial string EventDateText { get; set; } - - [ObservableProperty] - public partial string Reason { get; set; } = string.Empty; - - [ObservableProperty] - public partial string Address { get; set; } = string.Empty; - - [ObservableProperty] - public partial string LatitudeText { get; set; } = string.Empty; - - [ObservableProperty] - public partial string LongitudeText { get; set; } = string.Empty; - - [ObservableProperty] - public partial bool Consent { get; set; } = true; - - [ObservableProperty] - public partial ObservableCollection AvailablePrestations { get; set; } = new(); - - [ObservableProperty] - public partial HairPrestationDto? SelectedPrestation { get; set; } - - [ObservableProperty] - public partial ObservableCollection MultiPrestations { get; set; } = new(); - - [ObservableProperty] - public partial string AdditionalInfo { get; set; } = string.Empty; - - [ObservableProperty] - public partial long? ExistingQueryId { get; set; } - - [ObservableProperty] - public partial QueryStatus CommandStatus { get; set; } = QueryStatus.Inserted; - - public bool CanUseCurrentLocation => IsSupported && !IsBusy; - - public string Title => Form.Title; - public string PerformerLabel => Performer.UserName; - public string ActivityLabel => Activity.Name; - public bool IsSupported => IsRdv || IsBrush || IsMultiBrush; - public bool IsRdv => string.Equals(Form.ActionName, BillingCodes.Rdv, StringComparison.Ordinal); - public bool IsBrush => string.Equals(Form.ActionName, BillingCodes.Brush, StringComparison.Ordinal); - public bool IsMultiBrush => string.Equals(Form.ActionName, BillingCodes.MBrush, StringComparison.Ordinal); - public bool ShowsReason => IsRdv; - public bool ShowsAdditionalInfo => IsBrush; - public bool ShowsSinglePrestation => IsBrush; - public bool ShowsMultiplePrestations => IsMultiBrush; - public string BillingRoute => $"/billing/{Form.ActionName}"; - public bool IsEditingExisting => ExistingQueryId.HasValue; - public string SubmitLabel => IsEditingExisting ? "Mettre à jour la commande" : "Poster la commande"; - public string SupportMessage => IsSupported - ? IsRdv - ? "Complétez les informations du rendez-vous puis postez la commande." - : IsBrush - ? "Choisissez une prestation coiffure puis postez la commande." - : "Choisissez une ou plusieurs prestations coiffure puis postez la commande." - : $"Le formulaire {Form.ActionName} n'est pas encore pris en charge dans PostIt."; - - public override bool CanNavigateNext - { - get => false; - protected set { _ = value; } - } - - public override bool CanNavigatePrevious - { - get => true; - protected set { _ = value; } - } - - public BillingCommandPageViewModel( - ActivityInfo activity, - ActivityUserDisplayItem performer, - CommandFormSummary form, - BillingApiClient billingClient) - { - 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)); - - EventDateText = DateTime.Now.AddDays(1).ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture); - StatusMessage = SupportMessage; - } - - partial void OnExistingQueryIdChanged(long? value) - { - OnPropertyChanged(nameof(IsEditingExisting)); - OnPropertyChanged(nameof(SubmitLabel)); - } - - partial void OnIsBusyChanged(bool value) - { - OnPropertyChanged(nameof(CanUseCurrentLocation)); - UseCurrentLocationCommand.NotifyCanExecuteChanged(); - } - - public async Task InitializeAsync(BillingQueryDetailsDto? existingQuery = null) - { - if (!IsBrush && !IsMultiBrush) - { - if (existingQuery is not null) - { - ApplyExistingQuery(existingQuery); - } - - return; - } - - IsBusy = true; - try - { - var prestations = await _billingClient.GetHairPrestationsAsync(Form.ActionName).ConfigureAwait(true); - AvailablePrestations = new ObservableCollection(prestations ?? new List()); - SelectedPrestation = AvailablePrestations.FirstOrDefault(); - MultiPrestations = new ObservableCollection(AvailablePrestations.Select(SelectableHairPrestationItem.FromDto)); - - StatusMessage = AvailablePrestations.Count == 0 - ? "Aucune prestation coiffure disponible." - : SupportMessage; - } - catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden) - { - StatusMessage = "Accès refusé au catalogue de prestations (scope 'api'). Déconnectez puis reconnectez-vous."; - } - catch (Exception ex) - { - StatusMessage = $"Erreur lors du chargement des prestations: {ex.Message}"; - } - finally - { - IsBusy = false; - } - - if (existingQuery is not null) - { - ApplyExistingQuery(existingQuery); - } - } - - [RelayCommand] - private async Task SubmitAsync() - { - if (!IsSupported) - { - StatusMessage = SupportMessage; - return; - } - - if (!Consent) - { - StatusMessage = "Le consentement est requis pour poster la commande."; - return; - } - - if (!TryParseEventDate(out var eventDate)) - { - StatusMessage = "La date doit être saisie au format yyyy-MM-dd HH:mm."; - return; - } - - if (IsRdv && string.IsNullOrWhiteSpace(Reason)) - { - StatusMessage = "Le motif du rendez-vous est requis."; - return; - } - - if (string.IsNullOrWhiteSpace(Address)) - { - StatusMessage = "L'adresse du rendez-vous est requise."; - return; - } - - if (!TryParseCoordinates(out var latitude, out var longitude, out var coordinateError)) - { - StatusMessage = coordinateError; - return; - } - - IsBusy = true; - try - { - var address = Address.Trim(); - var locationPayload = BuildLocationPayload(address, latitude, longitude); - - var payload = new BillingQueryDetailsDto - { - Id = ExistingQueryId ?? 0, - BillingCode = Form.ActionName, - ActivityCode = Activity.Code, - PerformerId = Performer.PerformerId, - Consent = Consent, - EventDate = eventDate, - Status = CommandStatus, - Reason = Reason.Trim(), - AdditionalInfo = string.IsNullOrWhiteSpace(AdditionalInfo) ? string.Empty : AdditionalInfo.Trim(), - Location = new BillingLocationDto - { - Address = address, - Latitude = latitude, - Longitude = longitude, - } - }; - - if (IsRdv) - { - if (IsEditingExisting) - { - await _billingClient.UpdateAsync(Form.ActionName, ExistingQueryId!.Value, payload).ConfigureAwait(true); - } - else - { - await _billingClient.CreateAsync(Form.ActionName, new - { - ActivityCode = Activity.Code, - PerformerId = Performer.PerformerId, - Consent, - EventDate = eventDate, - Location = locationPayload, - Reason = payload.Reason, - Status = payload.Status, - }).ConfigureAwait(true); - } - } - else if (IsBrush) - { - if (SelectedPrestation is null) - { - StatusMessage = "Sélectionnez une prestation coiffure."; - return; - } - - payload.PrestationId = SelectedPrestation.Id; - - if (IsEditingExisting) - { - await _billingClient.UpdateAsync(Form.ActionName, ExistingQueryId!.Value, payload).ConfigureAwait(true); - } - else - { - await _billingClient.CreateAsync(Form.ActionName, new - { - ActivityCode = Activity.Code, - PerformerId = Performer.PerformerId, - Consent, - EventDate = (DateTime?)eventDate, - Location = locationPayload, - PrestationId = SelectedPrestation.Id, - AdditionalInfo = string.IsNullOrWhiteSpace(AdditionalInfo) ? null : AdditionalInfo.Trim(), - Status = payload.Status, - }).ConfigureAwait(true); - } - } - else if (IsMultiBrush) - { - var selectedPrestations = MultiPrestations.Where(x => x.IsSelected).ToList(); - if (selectedPrestations.Count == 0) - { - StatusMessage = "Sélectionnez au moins une prestation coiffure."; - return; - } - - payload.PrestationIds = selectedPrestations.Select(x => x.Id).ToList(); - - if (IsEditingExisting) - { - await _billingClient.UpdateAsync(Form.ActionName, ExistingQueryId!.Value, payload).ConfigureAwait(true); - } - else - { - await _billingClient.CreateAsync(Form.ActionName, new - { - ActivityCode = Activity.Code, - PerformerId = Performer.PerformerId, - Consent, - EventDate = eventDate, - Location = locationPayload, - Prestations = selectedPrestations.Select(x => new { PrestationId = x.Id }).ToList(), - Status = payload.Status, - }).ConfigureAwait(true); - } - } - - StatusMessage = IsEditingExisting - ? $"Commande #{ExistingQueryId} mise à jour sur {BillingRoute} pour {Performer.UserName}." - : $"Commande transmise sur {BillingRoute} pour {Performer.UserName}."; - } - catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden) - { - StatusMessage = "Accès refusé au billing (scope 'api'). Déconnectez puis reconnectez-vous."; - } - catch (Exception ex) - { - StatusMessage = $"Erreur lors de l'envoi: {ex.Message}"; - } - finally - { - IsBusy = false; - } - } - - [RelayCommand(CanExecute = nameof(CanUseCurrentLocation))] - private async Task UseCurrentLocationAsync() - { - if (!CanUseCurrentLocation) - { - return; - } - - IsBusy = true; - try - { - var result = await Platform.TryGetCurrentLocationAsync(default).ConfigureAwait(true); - if (!result.IsSuccess || !result.Latitude.HasValue || !result.Longitude.HasValue) - { - StatusMessage = result.Message; - return; - } - - LatitudeText = result.Latitude.Value.ToString(CultureInfo.InvariantCulture); - LongitudeText = result.Longitude.Value.ToString(CultureInfo.InvariantCulture); - StatusMessage = string.IsNullOrWhiteSpace(Address) - ? "Position récupérée. Complétez l'adresse puis envoyez la commande." - : result.Message; - } - catch (OperationCanceledException) - { - StatusMessage = "La récupération de la position a été annulée."; - } - catch (Exception ex) - { - StatusMessage = $"Impossible de récupérer la position: {ex.Message}"; - } - finally - { - IsBusy = false; - } - } - - private void ApplyExistingQuery(BillingQueryDetailsDto existingQuery) - { - ExistingQueryId = existingQuery.Id; - CommandStatus = existingQuery.Status; - Consent = existingQuery.Consent; - Reason = existingQuery.Reason ?? string.Empty; - AdditionalInfo = existingQuery.AdditionalInfo ?? string.Empty; - - if (existingQuery.EventDate is not null) - { - EventDateText = existingQuery.EventDate.Value - .ToLocalTime() - .ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture); - } - - if (existingQuery.Location is not null) - { - Address = existingQuery.Location.Address ?? string.Empty; - LatitudeText = existingQuery.Location.Latitude?.ToString(CultureInfo.InvariantCulture) ?? string.Empty; - LongitudeText = existingQuery.Location.Longitude?.ToString(CultureInfo.InvariantCulture) ?? string.Empty; - } - - if (IsBrush && existingQuery.PrestationId is not null) - { - SelectedPrestation = AvailablePrestations.FirstOrDefault(x => x.Id == existingQuery.PrestationId.Value); - } - - if (IsMultiBrush) - { - var selectedIds = existingQuery.PrestationIds is null - ? new HashSet() - : new HashSet(existingQuery.PrestationIds); - foreach (var item in MultiPrestations) - { - item.IsSelected = selectedIds.Contains(item.Id); - } - } - - StatusMessage = $"Commande #{existingQuery.Id} chargée."; - } - - private bool TryParseEventDate(out DateTime eventDate) - { - return DateTime.TryParse( - EventDateText, - CultureInfo.CurrentCulture, - DateTimeStyles.AssumeLocal, - out eventDate) - || DateTime.TryParse( - EventDateText, - CultureInfo.InvariantCulture, - DateTimeStyles.AssumeLocal, - out eventDate); - } - - private static bool TryParseCoordinate(string text, out double value) - { - return double.TryParse(text, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.CurrentCulture, out value) - || double.TryParse(text, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out value); - } - - private static object BuildLocationPayload(string address, double? latitude, double? longitude) - { - if (latitude.HasValue && longitude.HasValue) - { - return new - { - Address = address, - Latitude = latitude.Value, - Longitude = longitude.Value, - }; - } - - return new - { - Address = address, - }; - } - - private bool TryParseCoordinates(out double? latitude, out double? longitude, out string error) - { - latitude = null; - longitude = null; - error = string.Empty; - - var latitudeMissing = string.IsNullOrWhiteSpace(LatitudeText); - var longitudeMissing = string.IsNullOrWhiteSpace(LongitudeText); - - if (latitudeMissing && longitudeMissing) - { - return true; - } - - if (latitudeMissing != longitudeMissing) - { - error = "Latitude et longitude doivent être renseignées ensemble, ou laissées vides toutes les deux."; - return false; - } - - if (!TryParseCoordinate(LatitudeText, out var parsedLatitude)) - { - error = "Latitude invalide."; - return false; - } - - if (!TryParseCoordinate(LongitudeText, out var parsedLongitude)) - { - error = "Longitude invalide."; - return false; - } - - latitude = parsedLatitude; - longitude = parsedLongitude; - return true; - } -} diff --git a/src/PostIt/PostIt/ViewModels/BillingQueriesPageViewModel.cs b/src/PostIt/PostIt/ViewModels/BillingQueriesPageViewModel.cs index 2504799f..7198f2bc 100644 --- a/src/PostIt/PostIt/ViewModels/BillingQueriesPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/BillingQueriesPageViewModel.cs @@ -133,7 +133,7 @@ public partial class BillingQueriesPageViewModel : ViewModelBase try { var details = await _billingClient.GetQueryAsync(Form.ActionName, SelectedQuery.Id).ConfigureAwait(true); - var vm = new BillingCommandPageViewModel(Activity, Performer, Form, _billingClient); + var vm = Form.CreateCommandPageViewModel(Activity, Performer, _billingClient); await vm.InitializeAsync(details).ConfigureAwait(true); await app.PushPageAsync(vm).ConfigureAwait(true); } diff --git a/src/PostIt/PostIt/ViewModels/CommandFormsPageViewModel.cs b/src/PostIt/PostIt/ViewModels/CommandFormsPageViewModel.cs index e1ced1ef..7d233971 100644 --- a/src/PostIt/PostIt/ViewModels/CommandFormsPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/CommandFormsPageViewModel.cs @@ -81,7 +81,8 @@ public partial class CommandFormsPageViewModel : ViewModelBase throw new InvalidOperationException("Application PostIt indisponible."); } - var vm = SelectedForm.CreateCommandPageViewModel(Activity, Performer, _billingClient); + var vm = SelectedForm.CreateCommandPageViewModel( + Activity, Performer, _billingClient); await vm.InitializeAsync(); await app.PushPageAsync(vm); } diff --git a/src/PostIt/PostIt/ViewModels/Commands/BillingCommandPageViewModel.cs b/src/PostIt/PostIt/ViewModels/Commands/BillingCommandPageViewModel.cs new file mode 100644 index 00000000..07c33b27 --- /dev/null +++ b/src/PostIt/PostIt/ViewModels/Commands/BillingCommandPageViewModel.cs @@ -0,0 +1,121 @@ +using System; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Yavsc; +using Yavsc.Abstract.Workflow; +using Yavsc.Api.Client; +using Yavsc.Models.Billing; + +namespace PostIt.ViewModels; + +public abstract partial class BillingCommandPageViewModel : ViewModelBase +{ + protected readonly BillingApiClient _billingClient; + + public ActivityInfo Activity { get; } + public ActivityUserDisplayItem Performer { get; } + public CommandFormSummary Form { get; } + + [ObservableProperty] + public partial bool IsBusy { get; set; } + + [ObservableProperty] + public partial string StatusMessage { get; set; } + + [ObservableProperty] + public partial string Reason { get; set; } = string.Empty; + + + + [ObservableProperty] + public partial bool Consent { get; set; } = true; + + + [ObservableProperty] + public partial string AdditionalInfo { get; set; } = string.Empty; + + [ObservableProperty] + public partial long? ExistingQueryId { get; set; } + + [ObservableProperty] + public partial QueryStatus CommandStatus { get; set; } = QueryStatus.Inserted; + + public bool CanUseCurrentLocation => IsSupported && !IsBusy; + + public string Title => Form.Title; + public string PerformerLabel => Performer.UserName; + public string ActivityLabel => Activity.Name; + public bool IsSupported => IsRdv || IsBrush || IsMultiBrush; + public bool IsRdv => string.Equals(Form.ActionName, BillingCodes.Rdv, StringComparison.Ordinal); + public bool IsBrush => string.Equals(Form.ActionName, BillingCodes.Brush, StringComparison.Ordinal); + public bool IsMultiBrush => string.Equals(Form.ActionName, BillingCodes.MBrush, StringComparison.Ordinal); + public bool ShowsReason => IsRdv; + public bool ShowsAdditionalInfo => IsBrush; + public bool ShowsSinglePrestation => IsBrush; + public bool ShowsMultiplePrestations => IsMultiBrush; + public string BillingRoute => $"/billing/{Form.ActionName}"; + public bool IsEditingExisting => ExistingQueryId.HasValue; + public string SubmitLabel => IsEditingExisting ? "Mettre à jour la commande" : "Poster la commande"; + public string SupportMessage => IsSupported + ? IsRdv + ? "Complétez les informations du rendez-vous puis postez la commande." + : IsBrush + ? "Choisissez une prestation coiffure puis postez la commande." + : "Choisissez une ou plusieurs prestations coiffure puis postez la commande." + : $"Le formulaire {Form.ActionName} n'est pas encore pris en charge dans PostIt."; + + public override bool CanNavigateNext + { + get => false; + protected set { _ = value; } + } + + public override bool CanNavigatePrevious + { + get => true; + protected set { _ = value; } + } + + public BillingCommandPageViewModel( + ActivityInfo activity, + ActivityUserDisplayItem performer, + CommandFormSummary form, + BillingApiClient billingClient) + { + 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)); + + StatusMessage = SupportMessage; + } + + partial void OnExistingQueryIdChanged(long? value) + { + OnPropertyChanged(nameof(IsEditingExisting)); + OnPropertyChanged(nameof(SubmitLabel)); + } + + partial void OnIsBusyChanged(bool value) + { + OnPropertyChanged(nameof(CanUseCurrentLocation)); + } + + public async Task InitializeAsync(BillingQueryDetailsDto? existingQuery = null) + { + + if (existingQuery is not null) + { + ApplyExistingQuery(existingQuery); + return; + } + } + + protected abstract void ApplyExistingQuery(BillingQueryDetailsDto existingQuery); + + + + [RelayCommand] + protected abstract Task SubmitAsync(); +} diff --git a/src/PostIt/PostIt/ViewModels/Commands/BrushViewModel.cs b/src/PostIt/PostIt/ViewModels/Commands/BrushViewModel.cs new file mode 100644 index 00000000..2cd61b36 --- /dev/null +++ b/src/PostIt/PostIt/ViewModels/Commands/BrushViewModel.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using Yavsc.Abstract.Workflow; +using Yavsc.Api.Client; +using Yavsc.Models.Haircut; +namespace PostIt.ViewModels.Commands; +public partial class BrushViewModel : RdvViewModel +{ + + [ObservableProperty] + public partial ObservableCollection AvailablePrestations { get; set; } = new(); + + [ObservableProperty] + public partial HairPrestationDto? SelectedPrestation { get; set; } + + [ObservableProperty] + public partial ObservableCollection MultiPrestations { get; set; } = new(); + + + public BrushViewModel(ActivityInfo activity, ActivityUserDisplayItem performer, CommandFormSummary form, BillingApiClient billingClient) + : base(activity, performer, form, billingClient) + { + } + + protected override async void ApplyExistingQuery(BillingQueryDetailsDto existingQuery) + { + if (IsBrush && existingQuery.PrestationId is not null) + { + SelectedPrestation = AvailablePrestations.FirstOrDefault(x => x.Id == existingQuery.PrestationId.Value); + } + + if (IsMultiBrush) + { + var selectedIds = existingQuery.PrestationIds is null + ? new HashSet() + : new HashSet(existingQuery.PrestationIds); + foreach (var item in MultiPrestations) + { + item.IsSelected = selectedIds.Contains(item.Id); + } + } + + IsBusy = true; + try + { + var prestations = await _billingClient.GetHairPrestationsAsync + (Form.ActionName).ConfigureAwait(true); + AvailablePrestations = new ObservableCollection + (prestations ?? new List()); + SelectedPrestation = AvailablePrestations.FirstOrDefault(); + MultiPrestations = new ObservableCollection(AvailablePrestations.Select(SelectableHairPrestationItem.FromDto)); + + StatusMessage = AvailablePrestations.Count == 0 + ? "Aucune prestation coiffure disponible." + : SupportMessage; + } + catch (HttpRequestException ex) + when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden) + { + StatusMessage = "Accès refusé au catalogue de prestations (scope 'api'). Déconnectez puis reconnectez-vous."; + } + catch (Exception ex) + { + StatusMessage = $"Erreur lors du chargement des prestations: {ex.Message}"; + } + finally + { + IsBusy = false; + } + } + + protected override async Task SubmitAsync() + { + + + + IsBusy = true; + try + { + var address = Address.Trim(); + var locationPayload = BuildLocationPayload(address, Latitude, Longitude); + + var payload = new BillingQueryDetailsDto + { + Id = ExistingQueryId ?? 0, + BillingCode = Form.ActionName, + ActivityCode = Activity.Code, + PerformerId = Performer.PerformerId, + Consent = Consent, + EventDate = EventDate, + Status = CommandStatus, + Reason = Reason.Trim(), + AdditionalInfo = string.IsNullOrWhiteSpace(AdditionalInfo) ? string.Empty : AdditionalInfo.Trim(), + Location = new BillingLocationDto + { + Address = address, + Latitude = Latitude, + Longitude = Longitude, + } + }; + + if (IsBrush) + { + if (SelectedPrestation is null) + { + StatusMessage = "Sélectionnez une prestation coiffure."; + return; + } + + payload.PrestationId = SelectedPrestation.Id; + + if (IsEditingExisting) + { + await _billingClient.UpdateAsync(Form.ActionName, ExistingQueryId!.Value, payload).ConfigureAwait(true); + } + else + { + await _billingClient.CreateAsync(Form.ActionName, new + { + ActivityCode = Activity.Code, + PerformerId = Performer.PerformerId, + Consent, + EventDate = (DateTime?)EventDate, + Location = locationPayload, + PrestationId = SelectedPrestation.Id, + AdditionalInfo = string.IsNullOrWhiteSpace(AdditionalInfo) ? null : AdditionalInfo.Trim(), + Status = payload.Status, + }).ConfigureAwait(true); + } + } + else if (IsMultiBrush) + { + var selectedPrestations = MultiPrestations.Where(x => x.IsSelected).ToList(); + if (selectedPrestations.Count == 0) + { + StatusMessage = "Sélectionnez au moins une prestation coiffure."; + return; + } + + payload.PrestationIds = selectedPrestations.Select(x => x.Id).ToList(); + + if (IsEditingExisting) + { + await _billingClient.UpdateAsync(Form.ActionName, ExistingQueryId!.Value, payload).ConfigureAwait(true); + } + else + { + await _billingClient.CreateAsync(Form.ActionName, new + { + ActivityCode = Activity.Code, + PerformerId = Performer.PerformerId, + Consent, + EventDate = EventDate, + Location = locationPayload, + Prestations = selectedPrestations.Select(x => new { PrestationId = x.Id }).ToList(), + Status = payload.Status, + }).ConfigureAwait(true); + } + } + } + catch (HttpRequestException ex) + when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden) + { + StatusMessage = "Accès refusé au billing (scope 'api'). Déconnectez puis reconnectez-vous."; + } + catch (Exception ex) + { + StatusMessage = $"Erreur lors de l'envoi de la commande: {ex.Message}"; + } + finally + { + IsBusy = false; + } + + } +} diff --git a/src/PostIt/PostIt/ViewModels/Commands/MBrushViewModel cs b/src/PostIt/PostIt/ViewModels/Commands/MBrushViewModel cs new file mode 100644 index 00000000..c407682a --- /dev/null +++ b/src/PostIt/PostIt/ViewModels/Commands/MBrushViewModel cs @@ -0,0 +1,16 @@ +using PostIt.ViewModels; + +namespace PostIt.ViewModels.Comands; + +public class MBrushViewModel : BillingCommandPageViewModel +{ + public MBrushViewModel(ActivityInfo activity, ActivityUserDisplayItem performer, CommandFormSummary form, BillingApiClient billingClient) + : base(activity, performer, form, billingClient) + { + } + + protected override void ApplyExistingQuery(BillingQueryDetailsDto existingQuery) + { + throw new System.NotImplementedException(); + } +} diff --git a/src/PostIt/PostIt/ViewModels/Commands/RdvViewModel.cs b/src/PostIt/PostIt/ViewModels/Commands/RdvViewModel.cs new file mode 100644 index 00000000..5662d495 --- /dev/null +++ b/src/PostIt/PostIt/ViewModels/Commands/RdvViewModel.cs @@ -0,0 +1,210 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using PostIt.Services; +using Yavsc.Abstract.Workflow; +using Yavsc.Api.Client; + +namespace PostIt.ViewModels.Commands; + +public partial class RdvViewModel : BillingCommandPageViewModel +{ + [ObservableProperty] + public partial string Address { get; set; } = string.Empty; + + [ObservableProperty] + public partial double Latitude { get; set; } + + [ObservableProperty] + public partial double Longitude { get; set; } + + + [ObservableProperty] + public partial DateTime EventDate { get; set; } + + public RdvViewModel(ActivityInfo activity, ActivityUserDisplayItem performer, CommandFormSummary form, BillingApiClient billingClient) + : base(activity, performer, form, billingClient) + { + EventDate = DateTime.Now.AddDays(1); + } + + protected override void ApplyExistingQuery(BillingQueryDetailsDto existingQuery) + { + ExistingQueryId = existingQuery.Id; + CommandStatus = existingQuery.Status; + Consent = existingQuery.Consent; + Reason = existingQuery.Reason ?? string.Empty; + AdditionalInfo = existingQuery.AdditionalInfo ?? string.Empty; + + if (existingQuery.EventDate is not null) + { + EventDate = existingQuery.EventDate.Value + .ToLocalTime(); + } + + if (existingQuery.Location is not null) + { + Address = existingQuery.Location.Address ?? string.Empty; + Latitude = existingQuery.Location.Latitude ?? 0; + Longitude = existingQuery.Location.Longitude ?? 0; + } + + + + StatusMessage = $"Commande #{existingQuery.Id} chargée."; + } + + [RelayCommand(CanExecute = nameof(CanUseCurrentLocation))] + private async Task UseCurrentLocationAsync() + { + if (!CanUseCurrentLocation) + { + return; + } + + IsBusy = true; + try + { + var result = await Platform.TryGetCurrentLocationAsync(default).ConfigureAwait(true); + if (!result.IsSuccess || !result.Latitude.HasValue || !result.Longitude.HasValue) + { + StatusMessage = result.Message; + return; + } + + Latitude = result.Latitude.Value; + Longitude = result.Longitude.Value; + StatusMessage = string.IsNullOrWhiteSpace(Address) + ? "Position récupérée. Complétez l'adresse puis envoyez la commande." + : result.Message; + } + catch (OperationCanceledException) + { + StatusMessage = "La récupération de la position a été annulée."; + } + catch (Exception ex) + { + StatusMessage = $"Impossible de récupérer la position: {ex.Message}"; + } + finally + { + IsBusy = false; + } + } + + protected static object BuildLocationPayload(string address, double? latitude, double? longitude) + { + if (latitude.HasValue && longitude.HasValue) + { + return new + { + Address = address, + Latitude = latitude.Value, + Longitude = longitude.Value, + }; + } + + return new + { + Address = address, + }; + } + + + protected override async Task SubmitAsync() + { + if (!IsSupported) + { + StatusMessage = SupportMessage; + return; + } + + if (!Consent) + { + StatusMessage = "Le consentement est requis pour poster la commande."; + return; + } + + if (string.IsNullOrWhiteSpace(Address)) + { + StatusMessage = "L'adresse du rendez-vous est requise."; + return; + } + + + if (IsRdv && string.IsNullOrWhiteSpace(Reason)) + { + StatusMessage = "Le motif du rendez-vous est requis."; + return; + } + + + + IsBusy = true; + try + { + var address = Address.Trim(); + var locationPayload = BuildLocationPayload(address, Latitude, Longitude); + + var payload = new BillingQueryDetailsDto + { + Id = ExistingQueryId ?? 0, + BillingCode = Form.ActionName, + ActivityCode = Activity.Code, + PerformerId = Performer.PerformerId, + Consent = Consent, + EventDate = EventDate, + Status = CommandStatus, + Reason = Reason.Trim(), + AdditionalInfo = string.IsNullOrWhiteSpace(AdditionalInfo) ? string.Empty : AdditionalInfo.Trim(), + Location = new BillingLocationDto + { + Address = address, + Latitude = Latitude, + Longitude = Longitude, + } + }; + + if (IsRdv) + { + if (IsEditingExisting) + { + await _billingClient.UpdateAsync(Form.ActionName, ExistingQueryId!.Value, payload).ConfigureAwait(true); + } + else + { + await _billingClient.CreateAsync(Form.ActionName, new + { + ActivityCode = Activity.Code, + PerformerId = Performer.PerformerId, + Consent, + EventDate = EventDate, + Location = locationPayload, + Reason = payload.Reason, + Status = payload.Status, + }).ConfigureAwait(true); + } + } + else + + StatusMessage = IsEditingExisting + ? $"Commande #{ExistingQueryId} mise à jour sur {BillingRoute} pour {Performer.UserName}." + : $"Commande transmise sur {BillingRoute} pour {Performer.UserName}."; + } + catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden) + { + StatusMessage = "Accès refusé au billing (scope 'api'). Déconnectez puis reconnectez-vous."; + } + catch (Exception ex) + { + StatusMessage = $"Erreur lors de l'envoi: {ex.Message}"; + } + finally + { + IsBusy = false; + } + } +} diff --git a/src/PostIt/PostIt/ViewModels/ViewModelBase.cs b/src/PostIt/PostIt/ViewModels/ViewModelBase.cs index 4ca69eea..57ab091c 100644 --- a/src/PostIt/PostIt/ViewModels/ViewModelBase.cs +++ b/src/PostIt/PostIt/ViewModels/ViewModelBase.cs @@ -2,7 +2,7 @@ namespace PostIt.ViewModels; -public abstract partial class ViewModelBase : ObservableObject +public abstract class ViewModelBase : ObservableObject { /// /// Gets if the user can navigate to the next page diff --git a/src/PostIt/PostIt/Views/ActivitiesPage.axaml b/src/PostIt/PostIt/Views/ActivitiesPage.axaml index 31d7be5f..fa87051a 100644 --- a/src/PostIt/PostIt/Views/ActivitiesPage.axaml +++ b/src/PostIt/PostIt/Views/ActivitiesPage.axaml @@ -32,7 +32,7 @@ ItemsSource="{Binding Activities}" SelectedItem="{Binding SelectedActivity, Mode=TwoWay}"> - + @@ -53,7 +53,7 @@ ItemsSource="{Binding Specializations}" SelectedItem="{Binding SelectedSpecialization, Mode=TwoWay}"> - + diff --git a/src/PostIt/PostIt/Views/BillingCommandPage.axaml b/src/PostIt/PostIt/Views/BillingCommandPage.axaml deleted file mode 100644 index ee8cb61d..00000000 --- a/src/PostIt/PostIt/Views/BillingCommandPage.axaml +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - -