diff --git a/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs b/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs index 5f467ec8..c2fab4c3 100644 --- a/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs @@ -17,10 +17,12 @@ public class ActivitiesPageViewModelTests await client.GetCatalogAsync("brush", TestContext.Current.CancellationToken); await client.GetUsersAsync("brush-pro", TestContext.Current.CancellationToken); await billingClient.CreateAsync("Rdv", new { Foo = "Bar" }, TestContext.Current.CancellationToken); + await billingClient.GetQuerySummariesAsync("Rdv", TestContext.Current.CancellationToken); Assert.Equal("https://business.example/api/v1/activity/catalog?parentCode=brush", api.Paths[0]); Assert.Equal("https://business.example/api/v1/activity/brush-pro/users", api.Paths[1]); Assert.Equal("https://business.example/api/v1/billing/Rdv", api.Paths[2]); + Assert.Equal("https://business.example/api/v1/billing/Rdv", api.Paths[3]); } [Fact] diff --git a/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs b/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs index 13b08f69..4aa69978 100644 --- a/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs @@ -4,6 +4,7 @@ using PostIt.ViewModels; using Yavsc; using Yavsc.Abstract.Workflow; using Yavsc.Api.Client; +using Yavsc.Models.Haircut; namespace PostIt.Tests; @@ -46,9 +47,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 = "brush", Name = "Brush" }, + new ActivityBrowseItemDto { Code = "book", Name = "Book" }, new ActivityUserDisplayItem { PerformerId = "perf-2", UserName = "Bob" }, - new CommandFormSummaryDto { Id = 13, ActionName = "Brush", Title = "Coupe" }, + new CommandFormSummaryDto { Id = 13, ActionName = "Book", Title = "Réservation" }, client); await vm.SubmitCommand.ExecuteAsync(null); @@ -57,16 +58,95 @@ public class BillingCommandPageViewModelTests Assert.Contains("n'est pas encore pris en charge", vm.StatusMessage, StringComparison.OrdinalIgnoreCase); } + [Fact] + public async Task InitializeAsync_loads_prestations_for_brush_and_submit_posts_selected_prestation() + { + var api = new RecordingApi + { + HairPrestations = new List + { + new() { Id = 10, Title = "Femme · Cheveux mi-longs", Details = "Coupe · Brushing" }, + new() { Id = 11, Title = "Homme · Cheveux courts", Details = "Coupe · Coiffage" }, + } + }; + var client = new BillingApiClient(api, "https://business.example/api/v1/"); + var vm = new BillingCommandPageViewModel( + new ActivityBrowseItemDto { Code = "brush", Name = "Brush" }, + new ActivityUserDisplayItem { PerformerId = "perf-2", UserName = "Bob" }, + new CommandFormSummaryDto { 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", + }; + + await vm.InitializeAsync(); + vm.SelectedPrestation = vm.AvailablePrestations[1]; + await vm.SubmitCommand.ExecuteAsync(null); + + Assert.Equal("https://business.example/api/v1/billing/Brush", api.LastPath); + using var json = JsonDocument.Parse(JsonSerializer.Serialize(api.LastBody)); + Assert.Equal(11, json.RootElement.GetProperty("PrestationId").GetInt32()); + Assert.Equal("Prévoir shampoing", json.RootElement.GetProperty("AdditionalInfo").GetString()); + } + + [Fact] + public async Task InitializeAsync_loads_prestations_for_mbrush_and_submit_posts_selected_prestations() + { + var api = new RecordingApi + { + HairPrestations = new List + { + new() { Id = 21, Title = "Femme · Cheveux longs", Details = "Coupe · Couleur" }, + new() { Id = 22, Title = "Enfant · Cheveux courts", Details = "Coupe · Sans technique" }, + } + }; + var client = new BillingApiClient(api, "https://business.example/api/v1/"); + var vm = new BillingCommandPageViewModel( + new ActivityBrowseItemDto { Code = "mbrush", Name = "MBrush" }, + new ActivityUserDisplayItem { PerformerId = "perf-3", UserName = "Cara" }, + new CommandFormSummaryDto { 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, + }; + + await vm.InitializeAsync(); + vm.MultiPrestations[0].IsSelected = true; + vm.MultiPrestations[1].IsSelected = true; + await vm.SubmitCommand.ExecuteAsync(null); + + Assert.Equal("https://business.example/api/v1/billing/MBrush", api.LastPath); + using var json = JsonDocument.Parse(JsonSerializer.Serialize(api.LastBody)); + var prestations = json.RootElement.GetProperty("Prestations"); + Assert.Equal(2, prestations.GetArrayLength()); + Assert.Equal(21, prestations[0].GetProperty("PrestationId").GetInt32()); + Assert.Equal(22, prestations[1].GetProperty("PrestationId").GetInt32()); + } + private sealed class RecordingApi : IYavscApiClient { public HttpClient Http { get; } = new(); public string? LastPath { get; private set; } public object? LastBody { get; private set; } + public List? HairPrestations { get; init; } public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) { LastPath = path; LastBody = body; + if (typeof(T) == typeof(List)) + { + return Task.FromResult((T)(object)(HairPrestations ?? new List())); + } return Task.FromResult(default(T)!); } diff --git a/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs b/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs new file mode 100644 index 00000000..f8a3348b --- /dev/null +++ b/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs @@ -0,0 +1,90 @@ +using System.Net.Http; +using PostIt.ViewModels; +using Yavsc; +using Yavsc.Abstract.Workflow; +using Yavsc.Api.Client; + +namespace PostIt.Tests; + +public class BillingQueriesPageViewModelTests +{ + [Fact] + public async Task RefreshAsync_filters_queries_by_selected_activity_and_performer() + { + 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 ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" }, + new CommandFormSummaryDto { Id = 1, ActionName = "Rdv", Title = "Rendez-vous" }, + client); + + await vm.InitializeAsync(); + + Assert.Equal("https://business.example/api/v1/billing/Rdv", api.Paths.Single()); + Assert.Equal(1, vm.Queries.Count); + Assert.Equal("Rendez-vous #1", vm.Queries[0].Description); + Assert.Contains("1 commande", vm.StatusMessage, StringComparison.OrdinalIgnoreCase); + } + + private sealed class StubBillingApi : IYavscApiClient + { + public HttpClient Http { get; } = new(); + public List Paths { get; } = new(); + + public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) + { + Paths.Add(path); + + if (typeof(T) == typeof(List)) + { + var data = new List + { + new() + { + Id = 11, + ActivityCode = "dev", + PerformerId = "perf-1", + ClientId = "cli-1", + Status = QueryStatus.Inserted, + Description = "Rendez-vous #1", + Reason = "Point de cadrage", + EventDate = new DateTime(2026, 9, 1, 10, 0, 0, DateTimeKind.Utc), + }, + new() + { + Id = 12, + ActivityCode = "other", + PerformerId = "perf-1", + ClientId = "cli-1", + Status = QueryStatus.Accepted, + Description = "Autre activité", + EventDate = new DateTime(2026, 9, 2, 10, 0, 0, DateTimeKind.Utc), + }, + new() + { + Id = 13, + ActivityCode = "dev", + PerformerId = "perf-2", + ClientId = "cli-1", + Status = QueryStatus.Accepted, + Description = "Autre performer", + EventDate = new DateTime(2026, 9, 3, 10, 0, 0, DateTimeKind.Utc), + } + }; + + return Task.FromResult((T)(object)data); + } + + return Task.FromResult(default(T)!); + } + + public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) + { + Paths.Add(path); + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs index bdc3cee6..e86d1c4c 100644 --- a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs +++ b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs @@ -50,6 +50,7 @@ public static class ServiceCollectionHelpers services.AddSingleton(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); // ViewModels services.AddSingleton(settings); services.AddSingleton(api); @@ -64,6 +65,7 @@ public static class ServiceCollectionHelpers services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddTransient(); // Dialogs (modal-light pages): the ViewLocator resolves // them when a caller pushes a PostAclDialogViewModel or diff --git a/src/PostIt/PostIt/ViewLocator.cs b/src/PostIt/PostIt/ViewLocator.cs index 85a7eb55..2fd51633 100644 --- a/src/PostIt/PostIt/ViewLocator.cs +++ b/src/PostIt/PostIt/ViewLocator.cs @@ -42,6 +42,7 @@ public class ViewLocator : IDataTemplate ActivitiesPageViewModel => services.GetRequiredService(), CommandFormsPageViewModel => services.GetRequiredService(), BillingCommandPageViewModel => services.GetRequiredService(), + BillingQueriesPageViewModel => 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 index ba66075f..68516ea9 100644 --- a/src/PostIt/PostIt/ViewModels/BillingCommandPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/BillingCommandPageViewModel.cs @@ -1,7 +1,10 @@ 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; @@ -9,6 +12,7 @@ using Yavsc; using Yavsc.Abstract.Workflow; using Yavsc.Api.Client; using Yavsc.Models.Billing; +using Yavsc.Models.Haircut; using Yavsc.Models.Relationship; namespace PostIt.ViewModels; @@ -45,13 +49,36 @@ public partial class BillingCommandPageViewModel : ViewModelBase [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; + public string Title => Form.Title; public string PerformerLabel => Performer.UserName; public string ActivityLabel => Activity.Name; - public bool IsSupported => string.Equals(Form.ActionName, BillingCodes.Rdv, StringComparison.Ordinal); + 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 string SupportMessage => IsSupported - ? "Complétez les informations du rendez-vous puis postez la commande." + ? 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 @@ -81,6 +108,39 @@ public partial class BillingCommandPageViewModel : ViewModelBase StatusMessage = SupportMessage; } + public async Task InitializeAsync() + { + if (!IsBrush && !IsMultiBrush) + { + 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; + } + } + [RelayCommand] private async Task SubmitAsync() { @@ -102,7 +162,7 @@ public partial class BillingCommandPageViewModel : ViewModelBase return; } - if (string.IsNullOrWhiteSpace(Reason)) + if (IsRdv && string.IsNullOrWhiteSpace(Reason)) { StatusMessage = "Le motif du rendez-vous est requis."; return; @@ -129,21 +189,66 @@ public partial class BillingCommandPageViewModel : ViewModelBase IsBusy = true; try { - await _billingClient.CreateAsync(Form.ActionName, new + var location = new Location { - ActivityCode = Activity.Code, - PerformerId = Performer.PerformerId, - Consent, - EventDate = eventDate, - Location = new Location + Address = Address.Trim(), + Latitude = latitude, + Longitude = longitude, + }; + + if (IsRdv) + { + await _billingClient.CreateAsync(Form.ActionName, new { - Address = Address.Trim(), - Latitude = latitude, - Longitude = longitude, - }, - Reason = Reason.Trim(), - Status = QueryStatus.Inserted, - }).ConfigureAwait(true); + ActivityCode = Activity.Code, + PerformerId = Performer.PerformerId, + Consent, + EventDate = eventDate, + Location = location, + Reason = Reason.Trim(), + Status = QueryStatus.Inserted, + }).ConfigureAwait(true); + } + else if (IsBrush) + { + if (SelectedPrestation is null) + { + StatusMessage = "Sélectionnez une prestation coiffure."; + return; + } + + await _billingClient.CreateAsync(Form.ActionName, new + { + ActivityCode = Activity.Code, + PerformerId = Performer.PerformerId, + Consent, + EventDate = (DateTime?)eventDate, + Location = location, + PrestationId = SelectedPrestation.Id, + AdditionalInfo = string.IsNullOrWhiteSpace(AdditionalInfo) ? null : AdditionalInfo.Trim(), + Status = QueryStatus.Inserted, + }).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; + } + + await _billingClient.CreateAsync(Form.ActionName, new + { + ActivityCode = Activity.Code, + PerformerId = Performer.PerformerId, + Consent, + EventDate = eventDate, + Location = location, + Prestations = selectedPrestations.Select(x => new { PrestationId = x.Id }).ToList(), + Status = QueryStatus.Inserted, + }).ConfigureAwait(true); + } StatusMessage = $"Commande transmise sur {BillingRoute} pour {Performer.UserName}."; } diff --git a/src/PostIt/PostIt/ViewModels/BillingQueriesPageViewModel.cs b/src/PostIt/PostIt/ViewModels/BillingQueriesPageViewModel.cs new file mode 100644 index 00000000..7629ac42 --- /dev/null +++ b/src/PostIt/PostIt/ViewModels/BillingQueriesPageViewModel.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Yavsc.Api.Client; +using Yavsc.Abstract.Workflow; + +namespace PostIt.ViewModels; + +public partial class BillingQueriesPageViewModel : ViewModelBase +{ + private readonly BillingApiClient _billingClient; + + public ActivityBrowseItemDto Activity { get; } + public ActivityUserDisplayItem Performer { get; } + public CommandFormSummaryDto Form { get; } + + [ObservableProperty] + public partial ObservableCollection Queries { get; set; } = new(); + + [ObservableProperty] + public partial bool IsBusy { get; set; } + + [ObservableProperty] + public partial string StatusMessage { get; set; } = "Chargement des commandes..."; + + public string Title => $"Commandes {Form.Title}"; + public string ContextLabel => $"{Performer.UserName} · {Activity.Name}"; + + public override bool CanNavigateNext + { + get => false; + protected set { _ = value; } + } + + public override bool CanNavigatePrevious + { + get => true; + protected set { _ = value; } + } + + public BillingQueriesPageViewModel( + ActivityBrowseItemDto activity, + ActivityUserDisplayItem performer, + CommandFormSummaryDto 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)); + } + + public Task InitializeAsync() => RefreshAsync(); + + [RelayCommand] + public async Task RefreshAsync() + { + IsBusy = true; + try + { + var list = await _billingClient.GetQuerySummariesAsync(Form.ActionName).ConfigureAwait(true); + var filtered = (list ?? new()) + .Where(q => q.ActivityCode == Activity.Code && q.PerformerId == Performer.PerformerId) + .OrderByDescending(q => q.EventDate ?? DateTime.MinValue) + .ThenByDescending(q => q.Id) + .Select(BillingQueryDisplayItem.FromDto) + .ToList(); + + Queries = new ObservableCollection(filtered); + StatusMessage = filtered.Count == 0 + ? "Aucune commande trouvée pour ce formulaire." + : $"{filtered.Count} commande(s) chargée(s)."; + } + catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden) + { + Queries = new ObservableCollection(); + StatusMessage = "Accès refusé au billing (scope 'api'). Déconnectez puis reconnectez-vous."; + } + catch (Exception ex) + { + Queries = new ObservableCollection(); + StatusMessage = $"Erreur: {ex.Message}"; + } + finally + { + IsBusy = false; + } + } +} \ No newline at end of file diff --git a/src/PostIt/PostIt/ViewModels/BillingQueryDisplayItem.cs b/src/PostIt/PostIt/ViewModels/BillingQueryDisplayItem.cs new file mode 100644 index 00000000..bc437337 --- /dev/null +++ b/src/PostIt/PostIt/ViewModels/BillingQueryDisplayItem.cs @@ -0,0 +1,35 @@ +using System; +using Yavsc.Api.Client; + +namespace PostIt.ViewModels; + +public sealed class BillingQueryDisplayItem +{ + public long Id { get; init; } + public string Description { get; init; } = string.Empty; + public string Summary { get; init; } = string.Empty; + public string StatusLabel { get; init; } = string.Empty; + public string EventDateLabel { get; init; } = string.Empty; + public string BillingCode { get; init; } = string.Empty; + + public static BillingQueryDisplayItem FromDto(BillingQuerySummaryDto dto) + { + var summary = !string.IsNullOrWhiteSpace(dto.Reason) + ? dto.Reason + : !string.IsNullOrWhiteSpace(dto.AdditionalInfo) + ? dto.AdditionalInfo + : dto.Description; + + return new BillingQueryDisplayItem + { + Id = dto.Id, + Description = string.IsNullOrWhiteSpace(dto.Description) + ? $"Commande #{dto.Id}" + : dto.Description, + Summary = summary, + StatusLabel = dto.Status.ToString(), + EventDateLabel = dto.EventDate?.ToLocalTime().ToString("g") ?? "Date non précisée", + BillingCode = dto.BillingCode, + }; + } +} \ No newline at end of file diff --git a/src/PostIt/PostIt/ViewModels/CommandFormsPageViewModel.cs b/src/PostIt/PostIt/ViewModels/CommandFormsPageViewModel.cs index be60e936..426533a7 100644 --- a/src/PostIt/PostIt/ViewModels/CommandFormsPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/CommandFormsPageViewModel.cs @@ -21,7 +21,7 @@ public partial class CommandFormsPageViewModel : ViewModelBase [ObservableProperty] public partial ObservableCollection Forms { get; set; } - [ObservableProperty, NotifyCanExecuteChangedFor(nameof(OpenSelectedFormCommand))] + [ObservableProperty, NotifyCanExecuteChangedFor(nameof(OpenSelectedFormCommand)), NotifyCanExecuteChangedFor(nameof(OpenQueriesCommand))] public partial CommandFormSummaryDto? SelectedForm { get; set; } [ObservableProperty] @@ -62,6 +62,8 @@ public partial class CommandFormsPageViewModel : ViewModelBase private bool CanOpenSelectedForm() => SelectedForm is not null; + private bool CanOpenQueries() => SelectedForm is not null; + [RelayCommand(CanExecute = nameof(CanOpenSelectedForm))] private async Task OpenSelectedFormAsync() { @@ -77,6 +79,28 @@ public partial class CommandFormsPageViewModel : ViewModelBase throw new InvalidOperationException("Application PostIt indisponible."); } - await app.PushPageAsync(new BillingCommandPageViewModel(Activity, Performer, SelectedForm, _billingClient)); + var vm = new BillingCommandPageViewModel(Activity, Performer, SelectedForm, _billingClient); + await vm.InitializeAsync(); + await app.PushPageAsync(vm); + } + + [RelayCommand(CanExecute = nameof(CanOpenQueries))] + private async Task OpenQueriesAsync() + { + if (SelectedForm is null) + { + StatusMessage = "Sélectionnez un formulaire."; + return; + } + + var app = (App?)Application.Current; + if (app is null) + { + throw new InvalidOperationException("Application PostIt indisponible."); + } + + var vm = new BillingQueriesPageViewModel(Activity, Performer, SelectedForm, _billingClient); + await vm.InitializeAsync(); + await app.PushPageAsync(vm); } } \ No newline at end of file diff --git a/src/PostIt/PostIt/ViewModels/SelectableHairPrestationItem.cs b/src/PostIt/PostIt/ViewModels/SelectableHairPrestationItem.cs new file mode 100644 index 00000000..ee07bb97 --- /dev/null +++ b/src/PostIt/PostIt/ViewModels/SelectableHairPrestationItem.cs @@ -0,0 +1,22 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Yavsc.Models.Haircut; + +namespace PostIt.ViewModels; + +public partial class SelectableHairPrestationItem : ObservableObject +{ + public long Id { get; init; } + public string Title { get; init; } = string.Empty; + public string Details { get; init; } = string.Empty; + + [ObservableProperty] + public partial bool IsSelected { get; set; } + + public static SelectableHairPrestationItem FromDto(HairPrestationDto dto) + => new() + { + Id = dto.Id, + Title = dto.Title, + Details = dto.Details, + }; +} \ No newline at end of file diff --git a/src/PostIt/PostIt/Views/BillingCommandPage.axaml b/src/PostIt/PostIt/Views/BillingCommandPage.axaml index 73820ba9..251376e1 100644 --- a/src/PostIt/PostIt/Views/BillingCommandPage.axaml +++ b/src/PostIt/PostIt/Views/BillingCommandPage.axaml @@ -5,7 +5,7 @@ x:DataType="vm:BillingCommandPageViewModel" Header="Commande billing"> - + - - + + @@ -38,12 +46,67 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - +