From 2a36c19610fbf3a330c55851bd5e4cc37a083714 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Wed, 2 Sep 2026 14:52:17 +0100 Subject: [PATCH 1/3] renaming --- .../ActivitiesPageViewModelTests.cs | 16 ++++----- .../BillingCommandPageViewModelTests.cs | 28 +++++++-------- .../BillingQueriesPageViewModelTests.cs | 10 +++--- .../ViewModels/ActivitiesPageViewModel.cs | 36 +++++++++---------- .../ViewModels/ActivityUserDisplayItem.cs | 2 +- .../ViewModels/BillingCommandPageViewModel.cs | 10 +++--- .../ViewModels/BillingQueriesPageViewModel.cs | 10 +++--- .../ViewModels/CommandFormsPageViewModel.cs | 14 ++++---- ...tivityBrowseItemDto.cs => ActivityInfo.cs} | 6 ++-- ...ormSummaryDto.cs => CommandFormSummary.cs} | 2 +- ...tyPerformerDto.cs => PerformerActivity.cs} | 2 +- src/Yavsc.Api.Client/ActivityApiClient.cs | 10 +++--- .../ActivityApiControllerTests.cs | 18 +++++----- .../Business/ActivityApiController.cs | 20 +++++------ 14 files changed, 92 insertions(+), 92 deletions(-) rename src/Yavsc.Abstract/Workflow/{ActivityBrowseItemDto.cs => ActivityInfo.cs} (72%) rename src/Yavsc.Abstract/Workflow/{CommandFormSummaryDto.cs => CommandFormSummary.cs} (86%) rename src/Yavsc.Abstract/Workflow/{ActivityPerformerDto.cs => PerformerActivity.cs} (94%) diff --git a/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs b/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs index c2fab4c3..3f602fc4 100644 --- a/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs @@ -72,9 +72,9 @@ public class ActivitiesPageViewModelTests { Paths.Add(path); - if (typeof(T) == typeof(List)) + if (typeof(T) == typeof(List)) { - var activities = new List + var activities = new List { new() { @@ -82,11 +82,11 @@ public class ActivitiesPageViewModelTests Name = "Brush", Description = "Coiffure à domicile", PerformerCount = 1, - Forms = new List + Forms = new List { new() { Id = 1, ActionName = "Rdv", Title = "Rendez-vous" } }, - Children = new List + Children = new List { new() { @@ -95,7 +95,7 @@ public class ActivitiesPageViewModelTests Description = "Spécialisation premium", ParentCode = "brush", PerformerCount = 1, - Forms = new List + Forms = new List { new() { Id = 2, ActionName = "Rdv", Title = "Rendez-vous premium" } } @@ -106,14 +106,14 @@ public class ActivitiesPageViewModelTests return Task.FromResult((T)(object)activities); } - if (typeof(T) == typeof(List)) + if (typeof(T) == typeof(List)) { var performers = path.EndsWith("brush-pro/users", StringComparison.Ordinal) - ? new List + ? new List { new() { PerformerId = "pro-2", HasPerformerProfile = true, Active = false, UserName = "Bob", ActivityCode = "brush-pro", ActivityName = "Brush Pro", ExtraActivityCount = 2 } } - : new List + : new List { new() { PerformerId = "pro-1", HasPerformerProfile = true, Active = true, UserName = "Alice", ActivityCode = "brush", ActivityName = "Brush", ExtraActivityCount = 0 } }; diff --git a/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs b/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs index d80d3195..5b6f51bf 100644 --- a/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs @@ -17,9 +17,9 @@ public class BillingCommandPageViewModelTests var api = new RecordingApi(); var client = new BillingApiClient(api, "https://business.example/api/v1/"); var vm = new BillingCommandPageViewModel( - new ActivityBrowseItemDto { Code = "dev", Name = "Développement" }, + new ActivityInfo { Code = "dev", Name = "Développement" }, new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" }, - new CommandFormSummaryDto { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" }, + new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" }, client) { EventDateText = "2026-09-02 14:30", @@ -48,9 +48,9 @@ public class BillingCommandPageViewModelTests var api = new RecordingApi(); var client = new BillingApiClient(api, "https://business.example/api/v1/"); var vm = new BillingCommandPageViewModel( - new ActivityBrowseItemDto { Code = "book", Name = "Book" }, + new ActivityInfo { Code = "book", Name = "Book" }, new ActivityUserDisplayItem { PerformerId = "perf-2", UserName = "Bob" }, - new CommandFormSummaryDto { Id = 13, ActionName = "Book", Title = "Réservation" }, + new CommandFormSummary { Id = 13, ActionName = "Book", Title = "Réservation" }, client); await vm.SubmitCommand.ExecuteAsync(null); @@ -65,9 +65,9 @@ public class BillingCommandPageViewModelTests var api = new RecordingApi(); var client = new BillingApiClient(api, "https://business.example/api/v1/"); var vm = new BillingCommandPageViewModel( - new ActivityBrowseItemDto { Code = "dev", Name = "Développement" }, + new ActivityInfo { Code = "dev", Name = "Développement" }, new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" }, - new CommandFormSummaryDto { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" }, + new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" }, client) { EventDateText = "2026-09-02 14:30", @@ -98,9 +98,9 @@ public class BillingCommandPageViewModelTests var api = new RecordingApi(); var client = new BillingApiClient(api, "https://business.example/api/v1/"); var vm = new BillingCommandPageViewModel( - new ActivityBrowseItemDto { Code = "dev", Name = "Développement" }, + new ActivityInfo { Code = "dev", Name = "Développement" }, new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" }, - new CommandFormSummaryDto { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" }, + new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" }, client); await vm.UseCurrentLocationCommand.ExecuteAsync(null); @@ -127,9 +127,9 @@ public class BillingCommandPageViewModelTests }; var client = new BillingApiClient(api, "https://business.example/api/v1/"); var vm = new BillingCommandPageViewModel( - new ActivityBrowseItemDto { Code = "brush", Name = "Brush" }, + new ActivityInfo { Code = "brush", Name = "Brush" }, new ActivityUserDisplayItem { PerformerId = "perf-2", UserName = "Bob" }, - new CommandFormSummaryDto { Id = 13, ActionName = "Brush", Title = "Coupe" }, + new CommandFormSummary { Id = 13, ActionName = "Brush", Title = "Coupe" }, client) { EventDateText = "2026-09-02 14:30", @@ -163,9 +163,9 @@ public class BillingCommandPageViewModelTests }; var client = new BillingApiClient(api, "https://business.example/api/v1/"); var vm = new BillingCommandPageViewModel( - new ActivityBrowseItemDto { Code = "mbrush", Name = "MBrush" }, + new ActivityInfo { Code = "mbrush", Name = "MBrush" }, new ActivityUserDisplayItem { PerformerId = "perf-3", UserName = "Cara" }, - new CommandFormSummaryDto { Id = 14, ActionName = "MBrush", Title = "Coupe groupée" }, + new CommandFormSummary { Id = 14, ActionName = "MBrush", Title = "Coupe groupée" }, client) { EventDateText = "2026-09-03 10:00", @@ -201,9 +201,9 @@ public class BillingCommandPageViewModelTests }; var client = new BillingApiClient(api, "https://business.example/api/v1/"); var vm = new BillingCommandPageViewModel( - new ActivityBrowseItemDto { Code = "brush", Name = "Brush" }, + new ActivityInfo { Code = "brush", Name = "Brush" }, new ActivityUserDisplayItem { PerformerId = "perf-2", UserName = "Bob" }, - new CommandFormSummaryDto { Id = 13, ActionName = "Brush", Title = "Coupe" }, + new CommandFormSummary { Id = 13, ActionName = "Brush", Title = "Coupe" }, client); await vm.InitializeAsync(new BillingQueryDetailsDto diff --git a/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs b/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs index a5d37532..a7f2b084 100644 --- a/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs @@ -14,9 +14,9 @@ public class BillingQueriesPageViewModelTests var api = new StubBillingApi(); var client = new BillingApiClient(api, "https://business.example/api/v1/"); var vm = new BillingQueriesPageViewModel( - new ActivityBrowseItemDto { Code = "dev", Name = "Développement" }, + new ActivityInfo { Code = "dev", Name = "Développement" }, new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" }, - new CommandFormSummaryDto { Id = 1, ActionName = "Rdv", Title = "Rendez-vous" }, + new CommandFormSummary { Id = 1, ActionName = "Rdv", Title = "Rendez-vous" }, client); await vm.InitializeAsync(); @@ -33,9 +33,9 @@ public class BillingQueriesPageViewModelTests var api = new StubBillingApi(); var client = new BillingApiClient(api, "https://business.example/api/v1/"); var vm = new BillingQueriesPageViewModel( - new ActivityBrowseItemDto { Code = "dev", Name = "Développement" }, + new ActivityInfo { Code = "dev", Name = "Développement" }, new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" }, - new CommandFormSummaryDto { Id = 1, ActionName = "Rdv", Title = "Rendez-vous" }, + new CommandFormSummary { Id = 1, ActionName = "Rdv", Title = "Rendez-vous" }, client, isReadOnly: true, ongoingOnly: true); @@ -131,4 +131,4 @@ public class BillingQueriesPageViewModelTests public ValueTask DisposeAsync() => ValueTask.CompletedTask; } -} \ No newline at end of file +} diff --git a/src/PostIt/PostIt/ViewModels/ActivitiesPageViewModel.cs b/src/PostIt/PostIt/ViewModels/ActivitiesPageViewModel.cs index 5268d07a..2c776d16 100644 --- a/src/PostIt/PostIt/ViewModels/ActivitiesPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/ActivitiesPageViewModel.cs @@ -20,16 +20,16 @@ public partial class ActivitiesPageViewModel : ViewModelBase private bool _syncingSelection; [ObservableProperty] - public partial ObservableCollection Activities { get; set; } = new(); + public partial ObservableCollection Activities { get; set; } = new(); [ObservableProperty] - public partial ActivityBrowseItemDto? SelectedActivity { get; set; } + public partial ActivityInfo? SelectedActivity { get; set; } [ObservableProperty] - public partial ObservableCollection Specializations { get; set; } = new(); + public partial ObservableCollection Specializations { get; set; } = new(); [ObservableProperty] - public partial ActivityBrowseItemDto? SelectedSpecialization { get; set; } + public partial ActivityInfo? SelectedSpecialization { get; set; } [ObservableProperty] public partial ObservableCollection Performers { get; set; } = new(); @@ -43,7 +43,7 @@ public partial class ActivitiesPageViewModel : ViewModelBase [ObservableProperty] public partial string StatusMessage { get; set; } = "Choisissez une activité."; - public ActivityBrowseItemDto? CurrentActivity => SelectedSpecialization ?? SelectedActivity; + public ActivityInfo? CurrentActivity => SelectedSpecialization ?? SelectedActivity; public string SelectedActivityLabel => SelectedActivity?.Name ?? "(aucune activité)"; public string CurrentActivityLabel => CurrentActivity?.Name ?? "(aucune)"; public int CurrentFormCount => CurrentActivity?.Forms?.Count ?? 0; @@ -66,19 +66,19 @@ public partial class ActivitiesPageViewModel : ViewModelBase _billingClient = billingClient ?? throw new ArgumentNullException(nameof(billingClient)); } - partial void OnSelectedActivityChanged(ActivityBrowseItemDto? value) + partial void OnSelectedActivityChanged(ActivityInfo? value) { if (_syncingSelection) return; _ = ShowActivitySafeAsync(value); } - partial void OnSelectedSpecializationChanged(ActivityBrowseItemDto? value) + partial void OnSelectedSpecializationChanged(ActivityInfo? value) { if (_syncingSelection) return; _ = ShowSpecializationSafeAsync(value); } - private async Task ShowActivitySafeAsync(ActivityBrowseItemDto? value) + private async Task ShowActivitySafeAsync(ActivityInfo? value) { try { @@ -94,7 +94,7 @@ public partial class ActivitiesPageViewModel : ViewModelBase } } - private async Task ShowSpecializationSafeAsync(ActivityBrowseItemDto? value) + private async Task ShowSpecializationSafeAsync(ActivityInfo? value) { try { @@ -117,7 +117,7 @@ public partial class ActivitiesPageViewModel : ViewModelBase try { var list = await _client.GetCatalogAsync(); - Activities = new ObservableCollection(list ?? new()); + Activities = new ObservableCollection(list ?? new()); var first = Activities.FirstOrDefault(); await ShowActivityAsync(first); @@ -128,15 +128,15 @@ public partial class ActivitiesPageViewModel : ViewModelBase } catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden) { - Activities = new ObservableCollection(); - Specializations = new ObservableCollection(); + Activities = new ObservableCollection(); + Specializations = new ObservableCollection(); Performers = new ObservableCollection(); StatusMessage = "Accès refusé pour les activités (scope 'api'). Déconnectez puis reconnectez-vous."; } catch (Exception ex) { - Activities = new ObservableCollection(); - Specializations = new ObservableCollection(); + Activities = new ObservableCollection(); + Specializations = new ObservableCollection(); Performers = new ObservableCollection(); StatusMessage = $"Erreur: {ex.Message}"; } @@ -146,7 +146,7 @@ public partial class ActivitiesPageViewModel : ViewModelBase } } - public async Task ShowActivityAsync(ActivityBrowseItemDto? activity) + public async Task ShowActivityAsync(ActivityInfo? activity) { _syncingSelection = true; try @@ -163,7 +163,7 @@ public partial class ActivitiesPageViewModel : ViewModelBase OnPropertyChanged(nameof(SelectedActivityLabel)); OnPropertyChanged(nameof(CurrentActivityLabel)); OnPropertyChanged(nameof(CurrentFormCount)); - Specializations = new ObservableCollection(activity?.Children ?? new()); + Specializations = new ObservableCollection(activity?.Children ?? new()); if (activity is null) { @@ -175,7 +175,7 @@ public partial class ActivitiesPageViewModel : ViewModelBase await LoadPerformersAsync(activity); } - public async Task ShowSpecializationAsync(ActivityBrowseItemDto? specialization) + public async Task ShowSpecializationAsync(ActivityInfo? specialization) { _syncingSelection = true; try @@ -203,7 +203,7 @@ public partial class ActivitiesPageViewModel : ViewModelBase await LoadPerformersAsync(specialization); } - private async Task LoadPerformersAsync(ActivityBrowseItemDto activity) + private async Task LoadPerformersAsync(ActivityInfo activity) { IsBusy = true; try diff --git a/src/PostIt/PostIt/ViewModels/ActivityUserDisplayItem.cs b/src/PostIt/PostIt/ViewModels/ActivityUserDisplayItem.cs index ee06b883..3e4426b4 100644 --- a/src/PostIt/PostIt/ViewModels/ActivityUserDisplayItem.cs +++ b/src/PostIt/PostIt/ViewModels/ActivityUserDisplayItem.cs @@ -17,7 +17,7 @@ public sealed class ActivityUserDisplayItem public int ExtraActivityCount { get; init; } public string ExtraActivityLabel { get; init; } = "Pas d'autre activité"; - public static ActivityUserDisplayItem FromDto(ActivityPerformerDto dto) + public static ActivityUserDisplayItem FromDto(PerformerActivity dto) { return new ActivityUserDisplayItem { diff --git a/src/PostIt/PostIt/ViewModels/BillingCommandPageViewModel.cs b/src/PostIt/PostIt/ViewModels/BillingCommandPageViewModel.cs index 2ffeec26..cfd23086 100644 --- a/src/PostIt/PostIt/ViewModels/BillingCommandPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/BillingCommandPageViewModel.cs @@ -21,9 +21,9 @@ public partial class BillingCommandPageViewModel : ViewModelBase { private readonly BillingApiClient _billingClient; - public ActivityBrowseItemDto Activity { get; } + public ActivityInfo Activity { get; } public ActivityUserDisplayItem Performer { get; } - public CommandFormSummaryDto Form { get; } + public CommandFormSummary Form { get; } [ObservableProperty] public partial bool IsBusy { get; set; } @@ -104,9 +104,9 @@ public partial class BillingCommandPageViewModel : ViewModelBase } public BillingCommandPageViewModel( - ActivityBrowseItemDto activity, + ActivityInfo activity, ActivityUserDisplayItem performer, - CommandFormSummaryDto form, + CommandFormSummary form, BillingApiClient billingClient) { Activity = activity ?? throw new ArgumentNullException(nameof(activity)); @@ -487,4 +487,4 @@ public partial class BillingCommandPageViewModel : ViewModelBase longitude = parsedLongitude; return true; } -} \ No newline at end of file +} diff --git a/src/PostIt/PostIt/ViewModels/BillingQueriesPageViewModel.cs b/src/PostIt/PostIt/ViewModels/BillingQueriesPageViewModel.cs index d680ba74..2504799f 100644 --- a/src/PostIt/PostIt/ViewModels/BillingQueriesPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/BillingQueriesPageViewModel.cs @@ -18,9 +18,9 @@ public partial class BillingQueriesPageViewModel : ViewModelBase { private readonly BillingApiClient _billingClient; - public ActivityBrowseItemDto Activity { get; } + public ActivityInfo Activity { get; } public ActivityUserDisplayItem Performer { get; } - public CommandFormSummaryDto Form { get; } + public CommandFormSummary Form { get; } public bool IsReadOnly { get; } public bool OngoingOnly { get; } @@ -55,9 +55,9 @@ public partial class BillingQueriesPageViewModel : ViewModelBase } public BillingQueriesPageViewModel( - ActivityBrowseItemDto activity, + ActivityInfo activity, ActivityUserDisplayItem performer, - CommandFormSummaryDto form, + CommandFormSummary form, BillingApiClient billingClient, bool isReadOnly = false, bool ongoingOnly = false) @@ -170,4 +170,4 @@ public partial class BillingQueriesPageViewModel : ViewModelBase private static bool IsOngoingStatus(QueryStatus status) => status is QueryStatus.Inserted or QueryStatus.Accepted or QueryStatus.InProgress; -} \ No newline at end of file +} diff --git a/src/PostIt/PostIt/ViewModels/CommandFormsPageViewModel.cs b/src/PostIt/PostIt/ViewModels/CommandFormsPageViewModel.cs index e1453c11..e1ced1ef 100644 --- a/src/PostIt/PostIt/ViewModels/CommandFormsPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/CommandFormsPageViewModel.cs @@ -15,14 +15,14 @@ public partial class CommandFormsPageViewModel : ViewModelBase { private readonly BillingApiClient _billingClient; - public ActivityBrowseItemDto Activity { get; } + public ActivityInfo Activity { get; } public ActivityUserDisplayItem Performer { get; } [ObservableProperty] - public partial ObservableCollection Forms { get; set; } + public partial ObservableCollection Forms { get; set; } [ObservableProperty, NotifyCanExecuteChangedFor(nameof(OpenSelectedFormCommand)), NotifyCanExecuteChangedFor(nameof(OpenQueriesCommand)), NotifyCanExecuteChangedFor(nameof(OpenOngoingQueriesCommand))] - public partial CommandFormSummaryDto? SelectedForm { get; set; } + public partial CommandFormSummary? SelectedForm { get; set; } [ObservableProperty] public partial string StatusMessage { get; set; } @@ -43,7 +43,7 @@ public partial class CommandFormsPageViewModel : ViewModelBase } public CommandFormsPageViewModel( - ActivityBrowseItemDto activity, + ActivityInfo activity, ActivityUserDisplayItem performer, BillingApiClient billingClient) { @@ -51,7 +51,7 @@ public partial class CommandFormsPageViewModel : ViewModelBase Performer = performer ?? throw new ArgumentNullException(nameof(performer)); _billingClient = billingClient ?? throw new ArgumentNullException(nameof(billingClient)); - Forms = new ObservableCollection((activity.Forms ?? new()) + Forms = new ObservableCollection((activity.Forms ?? new()) .OrderBy(f => f.Title) .ThenBy(f => f.ActionName)); SelectedForm = Forms.FirstOrDefault(); @@ -81,7 +81,7 @@ public partial class CommandFormsPageViewModel : ViewModelBase throw new InvalidOperationException("Application PostIt indisponible."); } - var vm = new BillingCommandPageViewModel(Activity, Performer, SelectedForm, _billingClient); + var vm = SelectedForm.CreateCommandPageViewModel(Activity, Performer, _billingClient); await vm.InitializeAsync(); await app.PushPageAsync(vm); } @@ -131,4 +131,4 @@ public partial class CommandFormsPageViewModel : ViewModelBase await vm.InitializeAsync(); await app.PushPageAsync(vm); } -} \ No newline at end of file +} diff --git a/src/Yavsc.Abstract/Workflow/ActivityBrowseItemDto.cs b/src/Yavsc.Abstract/Workflow/ActivityInfo.cs similarity index 72% rename from src/Yavsc.Abstract/Workflow/ActivityBrowseItemDto.cs rename to src/Yavsc.Abstract/Workflow/ActivityInfo.cs index cb48b321..c0f543f4 100644 --- a/src/Yavsc.Abstract/Workflow/ActivityBrowseItemDto.cs +++ b/src/Yavsc.Abstract/Workflow/ActivityInfo.cs @@ -3,7 +3,7 @@ namespace Yavsc.Abstract.Workflow; /// /// Activity node returned by the browsing API. /// -public sealed class ActivityBrowseItemDto +public sealed class ActivityInfo { public string Code { get; set; } = string.Empty; public string Name { get; set; } = string.Empty; @@ -12,6 +12,6 @@ public sealed class ActivityBrowseItemDto public string Photo { get; set; } = string.Empty; public int Rate { get; set; } public int PerformerCount { get; set; } - public List Forms { get; set; } = new(); - public List Children { get; set; } = new(); + public List Forms { get; set; } = new(); + public List Children { get; set; } = new(); } diff --git a/src/Yavsc.Abstract/Workflow/CommandFormSummaryDto.cs b/src/Yavsc.Abstract/Workflow/CommandFormSummary.cs similarity index 86% rename from src/Yavsc.Abstract/Workflow/CommandFormSummaryDto.cs rename to src/Yavsc.Abstract/Workflow/CommandFormSummary.cs index c5322ca0..865690e0 100644 --- a/src/Yavsc.Abstract/Workflow/CommandFormSummaryDto.cs +++ b/src/Yavsc.Abstract/Workflow/CommandFormSummary.cs @@ -3,7 +3,7 @@ namespace Yavsc.Abstract.Workflow; /// /// Lightweight command-form description exposed to API clients. /// -public sealed class CommandFormSummaryDto +public sealed class CommandFormSummary { public long Id { get; set; } public string ActionName { get; set; } = string.Empty; diff --git a/src/Yavsc.Abstract/Workflow/ActivityPerformerDto.cs b/src/Yavsc.Abstract/Workflow/PerformerActivity.cs similarity index 94% rename from src/Yavsc.Abstract/Workflow/ActivityPerformerDto.cs rename to src/Yavsc.Abstract/Workflow/PerformerActivity.cs index 4c3df6b8..a5ee3b25 100644 --- a/src/Yavsc.Abstract/Workflow/ActivityPerformerDto.cs +++ b/src/Yavsc.Abstract/Workflow/PerformerActivity.cs @@ -3,7 +3,7 @@ namespace Yavsc.Abstract.Workflow; /// /// Lightweight performer description returned for one activity. /// -public sealed class ActivityPerformerDto +public sealed class PerformerActivity { public string PerformerId { get; set; } = string.Empty; public bool HasPerformerProfile { get; set; } diff --git a/src/Yavsc.Api.Client/ActivityApiClient.cs b/src/Yavsc.Api.Client/ActivityApiClient.cs index 56c7e4fb..9214e7db 100644 --- a/src/Yavsc.Api.Client/ActivityApiClient.cs +++ b/src/Yavsc.Api.Client/ActivityApiClient.cs @@ -30,7 +30,7 @@ public sealed class ActivityApiClient _baseAddress = new Uri(businessBaseAddress, UriKind.Absolute); } - public Task> GetCatalogAsync( + public Task> GetCatalogAsync( string? parentCode = null, CancellationToken ct = default) { @@ -38,23 +38,23 @@ public sealed class ActivityApiClient ? $"{PathPrefix}/catalog" : $"{PathPrefix}/catalog?parentCode={Uri.EscapeDataString(parentCode)}"; - return _api.CallAsync>(HttpMethod.Get, Absolute(path), ct: ct); + return _api.CallAsync>(HttpMethod.Get, Absolute(path), ct: ct); } - public Task> GetUsersAsync( + public Task> GetUsersAsync( string activityCode, CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(activityCode)) throw new ArgumentException("Activity code is required.", nameof(activityCode)); - return _api.CallAsync>( + return _api.CallAsync>( HttpMethod.Get, Absolute($"{PathPrefix}/{Uri.EscapeDataString(activityCode)}/users"), ct: ct); } - public Task> GetPerformersAsync( + public Task> GetPerformersAsync( string activityCode, CancellationToken ct = default) => GetUsersAsync(activityCode, ct); diff --git a/src/Yavsc.Api.Test/ActivityApiControllerTests.cs b/src/Yavsc.Api.Test/ActivityApiControllerTests.cs index a594eb5c..0525e349 100644 --- a/src/Yavsc.Api.Test/ActivityApiControllerTests.cs +++ b/src/Yavsc.Api.Test/ActivityApiControllerTests.cs @@ -42,7 +42,7 @@ public sealed class ActivityApiControllerTests : IClassFixture>(TestContext.Current.CancellationToken); + var payload = await response.Content.ReadFromJsonAsync>(TestContext.Current.CancellationToken); Assert.NotNull(payload); Assert.Single(payload!); Assert.Equal("alice", payload[0].PerformerId); @@ -68,7 +68,7 @@ public sealed class ActivityApiControllerTests : IClassFixture>(TestContext.Current.CancellationToken); + var payload = await response.Content.ReadFromJsonAsync>(TestContext.Current.CancellationToken); Assert.NotNull(payload); Assert.Single(payload!); Assert.Equal("alice", payload[0].PerformerId); @@ -84,7 +84,7 @@ public sealed class ActivityApiControllerTests : IClassFixture>(TestContext.Current.CancellationToken); + var catalog = await catalogResponse.Content.ReadFromJsonAsync>(TestContext.Current.CancellationToken); Assert.NotNull(catalog); var dev = catalog!.Single(a => a.Code == "dev"); @@ -93,7 +93,7 @@ public sealed class ActivityApiControllerTests : IClassFixture>(TestContext.Current.CancellationToken); + var performers = await performersResponse.Content.ReadFromJsonAsync>(TestContext.Current.CancellationToken); Assert.NotNull(performers); Assert.Equal(dev.PerformerCount, performers!.Count); Assert.Contains(performers, p => p.PerformerId == "alice"); @@ -108,7 +108,7 @@ public sealed class ActivityApiControllerTests : IClassFixture>(TestContext.Current.CancellationToken); + var catalog = await catalogResponse.Content.ReadFromJsonAsync>(TestContext.Current.CancellationToken); Assert.NotNull(catalog); Assert.DoesNotContain(catalog!, a => a.Code == "ghost"); Assert.Contains(catalog!, a => a.Code == "dev"); @@ -123,13 +123,13 @@ public sealed class ActivityApiControllerTests : IClassFixture>(TestContext.Current.CancellationToken); + var catalog = await catalogResponse.Content.ReadFromJsonAsync>(TestContext.Current.CancellationToken); Assert.NotNull(catalog); Assert.Contains(catalog!, a => a.Code == "declared-only"); var response = await http.GetAsync("/api/v1/activity/declared-only/users", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var payload = await response.Content.ReadFromJsonAsync>(TestContext.Current.CancellationToken); + var payload = await response.Content.ReadFromJsonAsync>(TestContext.Current.CancellationToken); Assert.NotNull(payload); Assert.Single(payload!); Assert.Equal("bob", payload[0].PerformerId); @@ -149,8 +149,8 @@ public sealed class ActivityApiControllerTests : IClassFixture>(TestContext.Current.CancellationToken); - var performersPayload = await performersResponse.Content.ReadFromJsonAsync>(TestContext.Current.CancellationToken); + var usersPayload = await usersResponse.Content.ReadFromJsonAsync>(TestContext.Current.CancellationToken); + var performersPayload = await performersResponse.Content.ReadFromJsonAsync>(TestContext.Current.CancellationToken); Assert.NotNull(usersPayload); Assert.NotNull(performersPayload); diff --git a/src/Yavsc.Api/Controllers/Business/ActivityApiController.cs b/src/Yavsc.Api/Controllers/Business/ActivityApiController.cs index f0fdaa1a..a8d28806 100644 --- a/src/Yavsc.Api/Controllers/Business/ActivityApiController.cs +++ b/src/Yavsc.Api/Controllers/Business/ActivityApiController.cs @@ -29,7 +29,7 @@ namespace Yavsc.Controllers } [HttpGet("catalog")] - public async Task>> GetCatalog( + public async Task>> GetCatalog( CancellationToken cancellationToken, [FromQuery] string parentCode = null) { @@ -74,7 +74,7 @@ namespace Yavsc.Controllers } [HttpGet("{id}/users")] - public async Task>> GetUsers( + public async Task>> GetUsers( [FromRoute] string id, CancellationToken cancellationToken) { @@ -97,7 +97,7 @@ namespace Yavsc.Controllers } [HttpGet("{id}/performers")] - public Task>> GetPerformers( + public Task>> GetPerformers( [FromRoute] string id, CancellationToken cancellationToken) { @@ -105,7 +105,7 @@ namespace Yavsc.Controllers return GetUsers(id, cancellationToken); } - private Task> QueryDeclaredUsersAsync( + private Task> QueryDeclaredUsersAsync( string activityCode, string activityName, CancellationToken cancellationToken) @@ -118,7 +118,7 @@ namespace Yavsc.Controllers from performer in performerProfiles.DefaultIfEmpty() where ua.DoesCode == activityCode orderby user != null ? user.UserName : ua.UserId - select new ActivityPerformerDto + select new PerformerActivity { PerformerId = ua.UserId, HasPerformerProfile = performer != null, @@ -260,11 +260,11 @@ namespace Yavsc.Controllers return _context.Activities.Count(e => e.Code == id) > 0; } - private static ActivityBrowseItemDto ToBrowseItem( + private static ActivityInfo ToBrowseItem( Activity activity, IReadOnlyDictionary performerCounts) { - return new ActivityBrowseItemDto + return new ActivityInfo { Code = activity.Code, Name = activity.Name, @@ -274,7 +274,7 @@ namespace Yavsc.Controllers Rate = activity.Rate, PerformerCount = performerCounts.TryGetValue(activity.Code, out var count) ? count : 0, Forms = (activity.Forms ?? Enumerable.Empty()) - .Select(f => new CommandFormSummaryDto + .Select(f => new CommandFormSummary { Id = f.Id, ActionName = f.ActionName, @@ -285,7 +285,7 @@ namespace Yavsc.Controllers .Where(c => !c.Hidden) .Where(c => performerCounts.TryGetValue(c.Code, out var childCount) && childCount > 0) .OrderByDescending(c => c.Rate) - .Select(c => new ActivityBrowseItemDto + .Select(c => new ActivityInfo { Code = c.Code, Name = c.Name, @@ -295,7 +295,7 @@ namespace Yavsc.Controllers Rate = c.Rate, PerformerCount = performerCounts.TryGetValue(c.Code, out var childCount) ? childCount : 0, Forms = (c.Forms ?? Enumerable.Empty()) - .Select(f => new CommandFormSummaryDto + .Select(f => new CommandFormSummary { Id = f.Id, ActionName = f.ActionName, From fc820362b9ca0d50664afff53fb0d62c2f96c5cd Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Fri, 4 Sep 2026 13:19:31 +0100 Subject: [PATCH 2/3] 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 @@ - - - - - - - - - - - - - - - - - - - - - - -