Add provider ongoing requests flow and sort persistence
This commit is contained in:
parent
86091990c5
commit
2397f4d3a8
10 changed files with 784 additions and 2 deletions
|
|
@ -0,0 +1,208 @@
|
||||||
|
using System.Net.Http;
|
||||||
|
using PostIt.ViewModels;
|
||||||
|
using Yavsc;
|
||||||
|
using Yavsc.Api.Client;
|
||||||
|
|
||||||
|
namespace PostIt.Tests;
|
||||||
|
|
||||||
|
public class ProviderOngoingRequestsPageViewModelTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task RefreshAsync_calls_provider_endpoint_and_filters_out_unknown_billing_codes()
|
||||||
|
{
|
||||||
|
var api = new StubProviderApi();
|
||||||
|
var client = new BillingApiClient(api, "https://business.example/api/v1/");
|
||||||
|
var vm = new ProviderOngoingRequestsPageViewModel(client);
|
||||||
|
|
||||||
|
await vm.InitializeAsync();
|
||||||
|
|
||||||
|
Assert.Contains("https://business.example/api/v1/bill/provider/ongoing", api.Paths);
|
||||||
|
Assert.Equal(3, vm.Queries.Count);
|
||||||
|
Assert.Equal(12, vm.Queries[0].Id);
|
||||||
|
Assert.Equal(11, vm.Queries[1].Id);
|
||||||
|
Assert.Equal(10, vm.Queries[2].Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FilterText_filters_by_activity_code_and_status()
|
||||||
|
{
|
||||||
|
var api = new StubProviderApi();
|
||||||
|
var client = new BillingApiClient(api, "https://business.example/api/v1/");
|
||||||
|
var vm = new ProviderOngoingRequestsPageViewModel(client);
|
||||||
|
|
||||||
|
await vm.InitializeAsync();
|
||||||
|
|
||||||
|
vm.FilterText = "mbrush";
|
||||||
|
Assert.Single(vm.Queries);
|
||||||
|
Assert.Equal("MBrush", vm.Queries[0].BillingCode);
|
||||||
|
|
||||||
|
vm.FilterText = "accepted";
|
||||||
|
Assert.Single(vm.Queries);
|
||||||
|
Assert.Equal(QueryStatus.Accepted, vm.Queries[0].Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task OpenSelectedEditorCommand_can_execute_only_when_selection_exists()
|
||||||
|
{
|
||||||
|
var api = new StubProviderApi();
|
||||||
|
var client = new BillingApiClient(api, "https://business.example/api/v1/");
|
||||||
|
var vm = new ProviderOngoingRequestsPageViewModel(client);
|
||||||
|
|
||||||
|
await vm.InitializeAsync();
|
||||||
|
|
||||||
|
Assert.False(vm.OpenSelectedEditorCommand.CanExecute(null));
|
||||||
|
|
||||||
|
vm.SelectedQuery = vm.Queries[0];
|
||||||
|
|
||||||
|
Assert.True(vm.OpenSelectedEditorCommand.CanExecute(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SelectedSortOption_date_keeps_most_recent_first()
|
||||||
|
{
|
||||||
|
var api = new StubProviderApi();
|
||||||
|
var client = new BillingApiClient(api, "https://business.example/api/v1/");
|
||||||
|
var vm = new ProviderOngoingRequestsPageViewModel(client);
|
||||||
|
|
||||||
|
await vm.InitializeAsync();
|
||||||
|
vm.SelectedSortOption = ProviderOngoingRequestsPageViewModel.SortByDate;
|
||||||
|
|
||||||
|
Assert.Equal(new long[] { 12, 11, 10 }, vm.Queries.Select(q => q.Id).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SelectedSortOption_date_ascending_keeps_oldest_first()
|
||||||
|
{
|
||||||
|
var api = new StubProviderApi();
|
||||||
|
var client = new BillingApiClient(api, "https://business.example/api/v1/");
|
||||||
|
var vm = new ProviderOngoingRequestsPageViewModel(client);
|
||||||
|
|
||||||
|
await vm.InitializeAsync();
|
||||||
|
vm.SelectedSortOption = ProviderOngoingRequestsPageViewModel.SortByDateAsc;
|
||||||
|
|
||||||
|
Assert.Equal(new long[] { 10, 11, 12 }, vm.Queries.Select(q => q.Id).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SelectedSortOption_status_prioritizes_inprogress_then_accepted_then_inserted()
|
||||||
|
{
|
||||||
|
var api = new StubProviderApi();
|
||||||
|
var client = new BillingApiClient(api, "https://business.example/api/v1/");
|
||||||
|
var vm = new ProviderOngoingRequestsPageViewModel(client);
|
||||||
|
|
||||||
|
await vm.InitializeAsync();
|
||||||
|
vm.SelectedSortOption = ProviderOngoingRequestsPageViewModel.SortByStatus;
|
||||||
|
|
||||||
|
Assert.Equal(new long[] { 11, 12, 10 }, vm.Queries.Select(q => q.Id).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_reads_saved_sort_option_from_settings()
|
||||||
|
{
|
||||||
|
var api = new StubProviderApi();
|
||||||
|
var client = new BillingApiClient(api, "https://business.example/api/v1/");
|
||||||
|
var settings = new Settings
|
||||||
|
{
|
||||||
|
ProviderOngoingRequestsSortOption = ProviderOngoingRequestsPageViewModel.SortByStatus,
|
||||||
|
};
|
||||||
|
|
||||||
|
var vm = new ProviderOngoingRequestsPageViewModel(client, settings);
|
||||||
|
|
||||||
|
Assert.Equal(ProviderOngoingRequestsPageViewModel.SortByStatus, vm.SelectedSortOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_falls_back_to_default_when_saved_sort_is_invalid()
|
||||||
|
{
|
||||||
|
var api = new StubProviderApi();
|
||||||
|
var client = new BillingApiClient(api, "https://business.example/api/v1/");
|
||||||
|
var settings = new Settings
|
||||||
|
{
|
||||||
|
ProviderOngoingRequestsSortOption = "invalide",
|
||||||
|
};
|
||||||
|
|
||||||
|
var vm = new ProviderOngoingRequestsPageViewModel(client, settings);
|
||||||
|
|
||||||
|
Assert.Equal(ProviderOngoingRequestsPageViewModel.SortByDate, vm.SelectedSortOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class StubProviderApi : IYavscApiClient
|
||||||
|
{
|
||||||
|
public HttpClient Http { get; } = new();
|
||||||
|
public List<string> Paths { get; } = new();
|
||||||
|
|
||||||
|
public Task<T> CallAsync<T>(HttpMethod method, string path, object? body = null, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
Paths.Add(path);
|
||||||
|
|
||||||
|
if (typeof(T) == typeof(List<BillingQuerySummaryDto>))
|
||||||
|
{
|
||||||
|
var data = new List<BillingQuerySummaryDto>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 10,
|
||||||
|
BillingCode = "Rdv",
|
||||||
|
ActivityCode = "dev",
|
||||||
|
PerformerId = "perf-1",
|
||||||
|
ClientId = "cli-1",
|
||||||
|
Status = QueryStatus.Inserted,
|
||||||
|
Description = "Rendez-vous",
|
||||||
|
EventDate = new DateTime(2026, 9, 10, 10, 0, 0, DateTimeKind.Utc),
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 11,
|
||||||
|
BillingCode = "MBrush",
|
||||||
|
ActivityCode = "hair",
|
||||||
|
PerformerId = "perf-1",
|
||||||
|
ClientId = "cli-2",
|
||||||
|
Status = QueryStatus.InProgress,
|
||||||
|
Description = "Coupe multiple",
|
||||||
|
EventDate = new DateTime(2026, 9, 11, 10, 0, 0, DateTimeKind.Utc),
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 12,
|
||||||
|
BillingCode = "Brush",
|
||||||
|
ActivityCode = "hair",
|
||||||
|
PerformerId = "perf-1",
|
||||||
|
ClientId = "cli-3",
|
||||||
|
Status = QueryStatus.Accepted,
|
||||||
|
Description = "Coupe simple",
|
||||||
|
EventDate = new DateTime(2026, 9, 12, 10, 0, 0, DateTimeKind.Utc),
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 13,
|
||||||
|
BillingCode = "",
|
||||||
|
ActivityCode = "unknown",
|
||||||
|
PerformerId = "perf-1",
|
||||||
|
ClientId = "cli-4",
|
||||||
|
Status = QueryStatus.Accepted,
|
||||||
|
Description = "Doit être filtrée",
|
||||||
|
EventDate = new DateTime(2026, 9, 13, 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 Task<T> CallAsync<T>(HttpMethod method, string path, Func<HttpContent> contentFactory, CancellationToken ct = default)
|
||||||
|
=> CallAsync<T>(method, path, (object?)null, ct);
|
||||||
|
|
||||||
|
public Task CallAsync(HttpMethod method, string path, Func<HttpContent> contentFactory, CancellationToken ct = default)
|
||||||
|
=> CallAsync(method, path, (object?)null, ct);
|
||||||
|
|
||||||
|
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -59,6 +59,7 @@ public static class ServiceCollectionHelpers
|
||||||
services.AddTransient<BrushPage>();
|
services.AddTransient<BrushPage>();
|
||||||
services.AddTransient<BillingQueriesPage>();
|
services.AddTransient<BillingQueriesPage>();
|
||||||
services.AddTransient<BillingQueryDetailsPage>();
|
services.AddTransient<BillingQueryDetailsPage>();
|
||||||
|
services.AddTransient<ProviderOngoingRequestsPage>();
|
||||||
// ViewModels
|
// ViewModels
|
||||||
services.AddSingleton(settings);
|
services.AddSingleton(settings);
|
||||||
services.AddSingleton<YavscApiClient>(api);
|
services.AddSingleton<YavscApiClient>(api);
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ public class ViewLocator : IDataTemplate
|
||||||
PostAclDialogViewModel => services.GetRequiredService<PostAclDialog>(),
|
PostAclDialogViewModel => services.GetRequiredService<PostAclDialog>(),
|
||||||
BillingQueriesPageViewModel => services.GetRequiredService<BillingQueriesPage>(),
|
BillingQueriesPageViewModel => services.GetRequiredService<BillingQueriesPage>(),
|
||||||
BillingQueryDetailsPageViewModel => services.GetRequiredService<BillingQueryDetailsPage>(),
|
BillingQueryDetailsPageViewModel => services.GetRequiredService<BillingQueryDetailsPage>(),
|
||||||
|
ProviderOngoingRequestsPageViewModel => services.GetRequiredService<ProviderOngoingRequestsPage>(),
|
||||||
null => new TextBlock { Text = "No view for <null>" },
|
null => new TextBlock { Text = "No view for <null>" },
|
||||||
_ => new TextBlock { Text = $"No view for {data.GetType().Name}" }
|
_ => new TextBlock { Text = $"No view for {data.GetType().Name}" }
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,356 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using PostIt.Helpers;
|
||||||
|
using Yavsc;
|
||||||
|
using Yavsc.Abstract.Workflow;
|
||||||
|
using Yavsc.Api.Client;
|
||||||
|
|
||||||
|
namespace PostIt.ViewModels;
|
||||||
|
|
||||||
|
public partial class ProviderOngoingRequestsPageViewModel : ViewModelBase, IActionStatusViewModel
|
||||||
|
{
|
||||||
|
public const string SortByDate = "Date (plus récent d'abord)";
|
||||||
|
public const string SortByDateAsc = "Date (plus ancien d'abord)";
|
||||||
|
public const string SortByStatus = "Statut (en cours d'abord)";
|
||||||
|
|
||||||
|
private readonly BillingApiClient _billingClient;
|
||||||
|
private readonly Settings? _settings;
|
||||||
|
private List<BillingQuerySummaryDto> _allQueries = new();
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial ObservableCollection<BillingQuerySummaryDto> Queries { get; set; } = new();
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial string FilterText { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public IReadOnlyList<string> SortOptions { get; } = new[]
|
||||||
|
{
|
||||||
|
SortByDate,
|
||||||
|
SortByDateAsc,
|
||||||
|
SortByStatus,
|
||||||
|
};
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial string SelectedSortOption { get; set; } = SortByDate;
|
||||||
|
|
||||||
|
[ObservableProperty, NotifyCanExecuteChangedFor(nameof(OpenSelectedQueryCommand))]
|
||||||
|
public partial BillingQuerySummaryDto? SelectedQuery { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial bool IsBusy { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial string StatusMessage { get; set; } = "Chargement des demandes fournisseur...";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial StatusNotice ActionStatus { get; set; } = StatusNotice.Info("Chargement des demandes fournisseur...");
|
||||||
|
|
||||||
|
public string Title => "Mes demandes en cours";
|
||||||
|
|
||||||
|
public override bool CanNavigateNext
|
||||||
|
{
|
||||||
|
get => false;
|
||||||
|
protected set { _ = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanNavigatePrevious
|
||||||
|
{
|
||||||
|
get => true;
|
||||||
|
protected set { _ = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProviderOngoingRequestsPageViewModel(BillingApiClient billingClient, Settings? settings = null)
|
||||||
|
{
|
||||||
|
_billingClient = billingClient ?? throw new ArgumentNullException(nameof(billingClient));
|
||||||
|
_settings = settings;
|
||||||
|
|
||||||
|
if (_settings is not null)
|
||||||
|
{
|
||||||
|
var preferredSort = NormalizeSortOption(_settings.ProviderOngoingRequestsSortOption);
|
||||||
|
if (!string.Equals(preferredSort, SelectedSortOption, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
SelectedSortOption = preferredSort;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task InitializeAsync() => RefreshAsync();
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
public async Task RefreshAsync()
|
||||||
|
{
|
||||||
|
IsBusy = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var items = await _billingClient.GetProviderOngoingQueriesAsync().ConfigureAwait(true) ?? new();
|
||||||
|
_allQueries = items
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x.BillingCode))
|
||||||
|
.OrderByDescending(x => x.EventDate ?? DateTime.MinValue)
|
||||||
|
.ThenByDescending(x => x.Id)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
ApplyFilter();
|
||||||
|
this.SetInfoStatus(_allQueries.Count == 0
|
||||||
|
? "Aucune demande en cours pour votre profil fournisseur."
|
||||||
|
: $"{_allQueries.Count} demande(s) en cours chargée(s).");
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
||||||
|
{
|
||||||
|
_allQueries = new List<BillingQuerySummaryDto>();
|
||||||
|
Queries = new ObservableCollection<BillingQuerySummaryDto>();
|
||||||
|
this.SetWarningStatus("Accès refusé au billing (scope 'api'). Déconnectez puis reconnectez-vous.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_allQueries = new List<BillingQuerySummaryDto>();
|
||||||
|
Queries = new ObservableCollection<BillingQuerySummaryDto>();
|
||||||
|
this.SetErrorStatus($"Erreur: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsBusy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanOpenSelectedQuery() => SelectedQuery is not null;
|
||||||
|
|
||||||
|
private bool CanOpenSelectedEditor() => SelectedQuery is not null;
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanOpenSelectedQuery))]
|
||||||
|
public async Task OpenSelectedQueryAsync()
|
||||||
|
{
|
||||||
|
if (SelectedQuery is null)
|
||||||
|
{
|
||||||
|
this.SetWarningStatus("Sélectionnez une demande.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var app = (App?)Application.Current;
|
||||||
|
if (app is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Application PostIt indisponible.");
|
||||||
|
}
|
||||||
|
|
||||||
|
IsBusy = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var details = await _billingClient
|
||||||
|
.GetQueryAsync(SelectedQuery.BillingCode, SelectedQuery.Id)
|
||||||
|
.ConfigureAwait(true);
|
||||||
|
var (activity, performer, form) = BuildNavigationContext(SelectedQuery);
|
||||||
|
|
||||||
|
var vm = new BillingQueryDetailsPageViewModel(
|
||||||
|
activity,
|
||||||
|
performer,
|
||||||
|
form,
|
||||||
|
_billingClient,
|
||||||
|
details,
|
||||||
|
isReadOnly: false);
|
||||||
|
|
||||||
|
await app.PushPageAsync(vm).ConfigureAwait(true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
this.SetErrorStatus($"Erreur lors de l'ouverture: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsBusy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanOpenSelectedEditor))]
|
||||||
|
public async Task OpenSelectedEditorAsync()
|
||||||
|
{
|
||||||
|
if (SelectedQuery is null)
|
||||||
|
{
|
||||||
|
this.SetWarningStatus("Sélectionnez une demande.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var app = (App?)Application.Current;
|
||||||
|
if (app is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Application PostIt indisponible.");
|
||||||
|
}
|
||||||
|
|
||||||
|
IsBusy = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var details = await _billingClient
|
||||||
|
.GetQueryAsync(SelectedQuery.BillingCode, SelectedQuery.Id)
|
||||||
|
.ConfigureAwait(true);
|
||||||
|
|
||||||
|
var (activity, performer, form) = BuildNavigationContext(SelectedQuery);
|
||||||
|
var vm = form.CreateCommandPageViewModel(activity, performer, _billingClient);
|
||||||
|
if (vm is null)
|
||||||
|
{
|
||||||
|
this.SetWarningStatus($"Le formulaire '{form.ActionName}' n'est pas pris en charge en édition.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await vm.InitializeAsync(details).ConfigureAwait(true);
|
||||||
|
await app.PushPageAsync(vm).ConfigureAwait(true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
this.SetErrorStatus($"Erreur lors de l'ouverture en édition: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsBusy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnFilterTextChanged(string value)
|
||||||
|
{
|
||||||
|
ApplyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSelectedSortOptionChanged(string value)
|
||||||
|
{
|
||||||
|
var normalized = NormalizeSortOption(value);
|
||||||
|
if (!string.Equals(normalized, value, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
SelectedSortOption = normalized;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PersistSortPreference(value);
|
||||||
|
ApplyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyFilter()
|
||||||
|
{
|
||||||
|
var query = FilterText?.Trim();
|
||||||
|
var filtered = string.IsNullOrWhiteSpace(query)
|
||||||
|
? _allQueries
|
||||||
|
: _allQueries.Where(x =>
|
||||||
|
ContainsInsensitive(x.Description, query)
|
||||||
|
|| ContainsInsensitive(x.ActivityCode, query)
|
||||||
|
|| ContainsInsensitive(x.BillingCode, query)
|
||||||
|
|| ContainsInsensitive(x.ClientId, query)
|
||||||
|
|| ContainsInsensitive(x.Status.ToString(), query))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var sorted = ApplySort(filtered);
|
||||||
|
Queries = new ObservableCollection<BillingQuerySummaryDto>(sorted);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<BillingQuerySummaryDto> ApplySort(IEnumerable<BillingQuerySummaryDto> source)
|
||||||
|
{
|
||||||
|
if (string.Equals(SelectedSortOption, SortByStatus, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return source
|
||||||
|
.OrderBy(x => GetStatusRank(x.Status))
|
||||||
|
.ThenByDescending(x => x.EventDate ?? DateTime.MinValue)
|
||||||
|
.ThenByDescending(x => x.Id)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(SelectedSortOption, SortByDateAsc, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return source
|
||||||
|
.OrderBy(x => x.EventDate ?? DateTime.MinValue)
|
||||||
|
.ThenBy(x => x.Id)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return source
|
||||||
|
.OrderByDescending(x => x.EventDate ?? DateTime.MinValue)
|
||||||
|
.ThenByDescending(x => x.Id)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PersistSortPreference(string selectedSort)
|
||||||
|
{
|
||||||
|
if (_settings is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(_settings.ProviderOngoingRequestsSortOption, selectedSort, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_settings.ProviderOngoingRequestsSortOption = selectedSort;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_settings.Save();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
this.SetWarningStatus("Le tri a été appliqué, mais sa sauvegarde a échoué.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeSortOption(string? sortOption)
|
||||||
|
{
|
||||||
|
if (string.Equals(sortOption, SortByDate, StringComparison.Ordinal)
|
||||||
|
|| string.Equals(sortOption, SortByDateAsc, StringComparison.Ordinal)
|
||||||
|
|| string.Equals(sortOption, SortByStatus, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return sortOption!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SortByDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetStatusRank(QueryStatus status)
|
||||||
|
=> status switch
|
||||||
|
{
|
||||||
|
QueryStatus.InProgress => 0,
|
||||||
|
QueryStatus.Accepted => 1,
|
||||||
|
QueryStatus.Inserted => 2,
|
||||||
|
QueryStatus.Success => 3,
|
||||||
|
QueryStatus.Rejected => 4,
|
||||||
|
QueryStatus.Failed => 5,
|
||||||
|
_ => 99,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static bool ContainsInsensitive(string? source, string query)
|
||||||
|
=> !string.IsNullOrWhiteSpace(source)
|
||||||
|
&& source.Contains(query, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private static (ActivityInfo activity, ActivityUserDisplayItem performer, CommandFormSummary form)
|
||||||
|
BuildNavigationContext(BillingQuerySummaryDto query)
|
||||||
|
{
|
||||||
|
var activity = new ActivityInfo
|
||||||
|
{
|
||||||
|
Code = query.ActivityCode,
|
||||||
|
Name = string.IsNullOrWhiteSpace(query.ActivityCode)
|
||||||
|
? "Activité"
|
||||||
|
: query.ActivityCode,
|
||||||
|
};
|
||||||
|
|
||||||
|
var performer = new ActivityUserDisplayItem
|
||||||
|
{
|
||||||
|
PerformerId = query.PerformerId,
|
||||||
|
UserName = "Mon profil fournisseur",
|
||||||
|
AvatarFallbackLabel = "M",
|
||||||
|
IsPerformerActive = true,
|
||||||
|
PerformerStatusBadgeLabel = "Actif",
|
||||||
|
PerformerStatusBadgeBackground = "#E6F7EC",
|
||||||
|
PerformerStatusBadgeBorder = "#2E7D32",
|
||||||
|
PerformerStatusBadgeForeground = "#1B5E20",
|
||||||
|
};
|
||||||
|
|
||||||
|
var form = new CommandFormSummary
|
||||||
|
{
|
||||||
|
ActionName = query.BillingCode,
|
||||||
|
Title = query.BillingCode,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (activity, performer, form);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.Input;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using PostIt.Helpers;
|
using PostIt.Helpers;
|
||||||
using PostIt.Services;
|
using PostIt.Services;
|
||||||
|
using Yavsc.Api.Client;
|
||||||
namespace PostIt.ViewModels;
|
namespace PostIt.ViewModels;
|
||||||
|
|
||||||
public class HomePageViewModel : ViewModelBase
|
public class HomePageViewModel : ViewModelBase
|
||||||
|
|
@ -50,7 +51,24 @@ public class HomePageViewModel : ViewModelBase
|
||||||
await app.PushPageAsync(vm);
|
await app.PushPageAsync(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task OpenProviderRequestsAsync() => OpenActivitiesAsync();
|
private async Task OpenProviderRequestsAsync()
|
||||||
|
{
|
||||||
|
var app = (App?)Application.Current;
|
||||||
|
if (app is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Application PostIt indisponible.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var billingClient = app.ServiceProvider?.GetRequiredService<BillingApiClient>();
|
||||||
|
if (billingClient is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Client billing indisponible.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var vm = new ProviderOngoingRequestsPageViewModel(billingClient, Settings);
|
||||||
|
await vm.InitializeAsync();
|
||||||
|
await app.PushPageAsync(vm);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Avalonia designer constructor. Builds a self-contained VM
|
/// Avalonia designer constructor. Builds a self-contained VM
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ public partial class Settings : ViewModelBase
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial string SearchText { get; set; } = string.Empty;
|
public partial string SearchText { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial string ProviderOngoingRequestsSortOption { get; set; } = string.Empty;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public partial StatusNotice ActionStatus { get; set; } = StatusNotice.Info("Pret.");
|
public partial StatusNotice ActionStatus { get; set; } = StatusNotice.Info("Pret.");
|
||||||
|
|
@ -62,6 +65,7 @@ public partial class Settings : ViewModelBase
|
||||||
partial void OnBlogsApiUrlChanged(string value) => MarkDirty();
|
partial void OnBlogsApiUrlChanged(string value) => MarkDirty();
|
||||||
partial void OnApiUrlChanged(string value) => MarkDirty();
|
partial void OnApiUrlChanged(string value) => MarkDirty();
|
||||||
partial void OnSearchTextChanged(string value) => MarkDirty();
|
partial void OnSearchTextChanged(string value) => MarkDirty();
|
||||||
|
partial void OnProviderOngoingRequestsSortOptionChanged(string value) => MarkDirty();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Authentication can be reassigned wholesale by
|
/// Authentication can be reassigned wholesale by
|
||||||
|
|
@ -346,6 +350,7 @@ public partial class Settings : ViewModelBase
|
||||||
? settings.ApiUrl
|
? settings.ApiUrl
|
||||||
: this.ApiUrl;
|
: this.ApiUrl;
|
||||||
this.SearchText = settings.SearchText ?? string.Empty;
|
this.SearchText = settings.SearchText ?? string.Empty;
|
||||||
|
this.ProviderOngoingRequestsSortOption = settings.ProviderOngoingRequestsSortOption ?? string.Empty;
|
||||||
if (!(settings.Authentication is null))
|
if (!(settings.Authentication is null))
|
||||||
{
|
{
|
||||||
this.Authentication = new AuthenticationSettings();
|
this.Authentication = new AuthenticationSettings();
|
||||||
|
|
@ -424,6 +429,7 @@ public partial class Settings : ViewModelBase
|
||||||
this.BlogsApiUrl = "https://blogs.pschneider.fr/api/v1/";
|
this.BlogsApiUrl = "https://blogs.pschneider.fr/api/v1/";
|
||||||
this.ApiUrl = "https://api.pschneider.fr/api/v1/";
|
this.ApiUrl = "https://api.pschneider.fr/api/v1/";
|
||||||
this.SearchText = string.Empty;
|
this.SearchText = string.Empty;
|
||||||
|
this.ProviderOngoingRequestsSortOption = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
<ContentPage xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:PostIt.ViewModels"
|
||||||
|
xmlns:dto="using:Yavsc.Api.Client"
|
||||||
|
xmlns:postitControls="using:PostIt.Controls"
|
||||||
|
x:Class="PostIt.Views.ProviderOngoingRequestsPage"
|
||||||
|
x:DataType="vm:ProviderOngoingRequestsPageViewModel"
|
||||||
|
Header="Mes demandes en cours">
|
||||||
|
<Grid RowDefinitions="Auto,Auto,Auto,*,Auto" Margin="12">
|
||||||
|
<StackPanel Grid.Row="0" Spacing="3">
|
||||||
|
<TextBlock Text="{Binding Title}" FontSize="20" FontWeight="Bold" />
|
||||||
|
<TextBlock Text="Point de vue fournisseur" Opacity="0.75" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1" ColumnDefinitions="Auto,8,Auto,8,*,8,Auto" Margin="0,10,0,8">
|
||||||
|
<Button Grid.Column="0" Content="Rafraîchir" Command="{Binding RefreshCommand}" />
|
||||||
|
<ComboBox Grid.Column="2"
|
||||||
|
Width="220"
|
||||||
|
ItemsSource="{Binding SortOptions}"
|
||||||
|
SelectedItem="{Binding SelectedSortOption, Mode=TwoWay}" />
|
||||||
|
<TextBox Grid.Column="4"
|
||||||
|
PlaceholderText="Filtrer: activité, code, client, statut..."
|
||||||
|
Text="{Binding FilterText, Mode=TwoWay}" />
|
||||||
|
<TextBlock Grid.Column="6"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{Binding Queries.Count, StringFormat='Résultats : {0}'}"
|
||||||
|
Opacity="0.7" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2"
|
||||||
|
Text="Astuce: sélectionnez une ligne puis ouvrez le détail ou l'édition directe."
|
||||||
|
Opacity="0.65"
|
||||||
|
FontSize="11"
|
||||||
|
Margin="0,0,0,8" />
|
||||||
|
|
||||||
|
<Grid Grid.Row="3" RowDefinitions="*,Auto">
|
||||||
|
<ListBox Grid.Row="0"
|
||||||
|
ItemsSource="{Binding Queries}"
|
||||||
|
SelectedItem="{Binding SelectedQuery, Mode=TwoWay}">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="dto:BillingQuerySummaryDto">
|
||||||
|
<Border BorderThickness="1"
|
||||||
|
BorderBrush="#22000000"
|
||||||
|
CornerRadius="8"
|
||||||
|
Padding="10"
|
||||||
|
Margin="0,0,0,8">
|
||||||
|
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="*,Auto">
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
Text="{Binding Description}"
|
||||||
|
FontWeight="Bold"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
Grid.Column="1"
|
||||||
|
Text="{Binding Status}"
|
||||||
|
FontSize="11"
|
||||||
|
Opacity="0.75"
|
||||||
|
HorizontalAlignment="Right" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1"
|
||||||
|
Grid.Column="0"
|
||||||
|
Margin="0,6,0,0"
|
||||||
|
Text="{Binding ActivityCode, StringFormat='Activité : {0}'}"
|
||||||
|
FontSize="11"
|
||||||
|
Opacity="0.75" />
|
||||||
|
<TextBlock Grid.Row="1"
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="0,6,0,0"
|
||||||
|
Text="{Binding BillingCode, StringFormat='Code : {0}'}"
|
||||||
|
FontSize="11"
|
||||||
|
Opacity="0.65"
|
||||||
|
HorizontalAlignment="Right" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="1"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Margin="0,8,0,0"
|
||||||
|
Spacing="8">
|
||||||
|
<Button Content="Ouvrir le détail"
|
||||||
|
Command="{Binding OpenSelectedQueryCommand}" />
|
||||||
|
<Button Content="Ouvrir en édition"
|
||||||
|
Command="{Binding OpenSelectedEditorCommand}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid Grid.Row="4" ColumnDefinitions="*,Auto" Margin="0,12,0,0">
|
||||||
|
<postitControls:StatusBar Grid.Column="0"
|
||||||
|
DataContext="{Binding ActionStatus}" />
|
||||||
|
<ProgressBar Grid.Column="1" Width="120" IsIndeterminate="True" IsVisible="{Binding IsBusy}" />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</ContentPage>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
|
||||||
|
namespace PostIt.Views;
|
||||||
|
|
||||||
|
public partial class ProviderOngoingRequestsPage : ContentPage
|
||||||
|
{
|
||||||
|
public ProviderOngoingRequestsPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -77,6 +77,14 @@ public sealed class BillingApiClient
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<List<BillingQuerySummaryDto>> GetProviderOngoingQueriesAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return _api.CallAsync<List<BillingQuerySummaryDto>>(
|
||||||
|
HttpMethod.Get,
|
||||||
|
Absolute("bill/provider/ongoing"),
|
||||||
|
ct: ct);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<BillingQueryDetailsDto> GetQueryAsync(string billingCode, long queryId, CancellationToken ct = default)
|
public async Task<BillingQueryDetailsDto> GetQueryAsync(string billingCode, long queryId, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(billingCode))
|
if (string.IsNullOrWhiteSpace(billingCode))
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Localization;
|
using Microsoft.Extensions.Localization;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
using Yavsc.Billing;
|
||||||
using Yavsc.Helpers;
|
using Yavsc.Helpers;
|
||||||
using Yavsc.ViewModels;
|
using Yavsc.ViewModels;
|
||||||
using Yavsc.Models.Billing;
|
using Yavsc.Models.Billing;
|
||||||
|
|
@ -100,6 +101,57 @@ namespace Yavsc.ApiControllers
|
||||||
return ViewComponent("Bill",new object[] { billingCode, bill, OutputFormat.Pdf, true } );
|
return ViewComponent("Bill",new object[] { billingCode, bill, OutputFormat.Pdf, true } );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lists ongoing service commands for the authenticated performer.
|
||||||
|
/// This endpoint is tailored for the PostIt provider homepage flow
|
||||||
|
/// ("Mes demandes en cours").
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("provider/ongoing")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public IActionResult GetProviderOngoingCommands()
|
||||||
|
{
|
||||||
|
var uid = User.GetUserId();
|
||||||
|
if (string.IsNullOrWhiteSpace(uid))
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (billingService.BillingMap.Count == 0)
|
||||||
|
{
|
||||||
|
WorkflowHelpers.ConfigureBillingService();
|
||||||
|
}
|
||||||
|
|
||||||
|
var commands = dbContext.Set<NominativeServiceCommand>()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(q => q.PerformerId == uid)
|
||||||
|
.Where(q => q.Status == QueryStatus.Inserted
|
||||||
|
|| q.Status == QueryStatus.Accepted
|
||||||
|
|| q.Status == QueryStatus.InProgress)
|
||||||
|
.OrderByDescending(q => q.DateModified)
|
||||||
|
.ThenByDescending(q => q.Id)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var payload = commands
|
||||||
|
.Select(q => new
|
||||||
|
{
|
||||||
|
Id = q.Id,
|
||||||
|
BillingCode = ResolveBillingCode(q),
|
||||||
|
ActivityCode = q.ActivityCode,
|
||||||
|
PerformerId = q.PerformerId,
|
||||||
|
ClientId = q.ClientId,
|
||||||
|
Status = q.Status,
|
||||||
|
Description = q.Description,
|
||||||
|
EventDate = ResolveEventDate(q),
|
||||||
|
Reason = q is Models.Workflow.RdvQuery rdv ? rdv.Reason : string.Empty,
|
||||||
|
AdditionalInfo = q is Models.Haircut.HairCutQuery hc ? hc.AdditionalInfo : string.Empty,
|
||||||
|
Provisional = q.Provisional,
|
||||||
|
})
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x.BillingCode))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Ok(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[HttpPost("prosign/{billingCode}/{id}")]
|
[HttpPost("prosign/{billingCode}/{id}")]
|
||||||
public async Task<IActionResult> ProSign(string billingCode, long id)
|
public async Task<IActionResult> ProSign(string billingCode, long id)
|
||||||
|
|
@ -133,6 +185,23 @@ namespace Yavsc.ApiControllers
|
||||||
return Ok (new { ProviderValidationDate = estimate.ProviderValidationDate, GCMSent = gcmSent });
|
return Ok (new { ProviderValidationDate = estimate.ProviderValidationDate, GCMSent = gcmSent });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string ResolveBillingCode(NominativeServiceCommand command)
|
||||||
|
{
|
||||||
|
var typeName = command.GetType().Name;
|
||||||
|
return billingService.BillingMap.TryGetValue(typeName, out var code)
|
||||||
|
? code
|
||||||
|
: string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime? ResolveEventDate(NominativeServiceCommand command)
|
||||||
|
=> command switch
|
||||||
|
{
|
||||||
|
Models.Workflow.RdvQuery rdv => rdv.EventDate,
|
||||||
|
Models.Haircut.HairCutQuery brush => brush.EventDate,
|
||||||
|
Models.Haircut.HairMultiCutQuery mbrush => mbrush.EventDate,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
[HttpGet("prosign/{billingCode}/{id}")]
|
[HttpGet("prosign/{billingCode}/{id}")]
|
||||||
public async Task<IActionResult> GetProSign(string billingCode, long id)
|
public async Task<IActionResult> GetProSign(string billingCode, long id)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue