feat/files-control #52
10 changed files with 784 additions and 2 deletions
Add provider ongoing requests flow and sort persistence
commit
2397f4d3a8
|
|
@ -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<BillingQueriesPage>();
|
||||
services.AddTransient<BillingQueryDetailsPage>();
|
||||
services.AddTransient<ProviderOngoingRequestsPage>();
|
||||
// ViewModels
|
||||
services.AddSingleton(settings);
|
||||
services.AddSingleton<YavscApiClient>(api);
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ public class ViewLocator : IDataTemplate
|
|||
PostAclDialogViewModel => services.GetRequiredService<PostAclDialog>(),
|
||||
BillingQueriesPageViewModel => services.GetRequiredService<BillingQueriesPage>(),
|
||||
BillingQueryDetailsPageViewModel => services.GetRequiredService<BillingQueryDetailsPage>(),
|
||||
ProviderOngoingRequestsPageViewModel => services.GetRequiredService<ProviderOngoingRequestsPage>(),
|
||||
null => new TextBlock { Text = "No view for <null>" },
|
||||
_ => 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 PostIt.Helpers;
|
||||
using PostIt.Services;
|
||||
using Yavsc.Api.Client;
|
||||
namespace PostIt.ViewModels;
|
||||
|
||||
public class HomePageViewModel : ViewModelBase
|
||||
|
|
@ -50,7 +51,24 @@ public class HomePageViewModel : ViewModelBase
|
|||
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>
|
||||
/// Avalonia designer constructor. Builds a self-contained VM
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ public partial class Settings : ViewModelBase
|
|||
[ObservableProperty]
|
||||
public partial string SearchText { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string ProviderOngoingRequestsSortOption { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
[JsonIgnore]
|
||||
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 OnApiUrlChanged(string value) => MarkDirty();
|
||||
partial void OnSearchTextChanged(string value) => MarkDirty();
|
||||
partial void OnProviderOngoingRequestsSortOptionChanged(string value) => MarkDirty();
|
||||
|
||||
/// <summary>
|
||||
/// Authentication can be reassigned wholesale by
|
||||
|
|
@ -346,6 +350,7 @@ public partial class Settings : ViewModelBase
|
|||
? settings.ApiUrl
|
||||
: this.ApiUrl;
|
||||
this.SearchText = settings.SearchText ?? string.Empty;
|
||||
this.ProviderOngoingRequestsSortOption = settings.ProviderOngoingRequestsSortOption ?? string.Empty;
|
||||
if (!(settings.Authentication is null))
|
||||
{
|
||||
this.Authentication = new AuthenticationSettings();
|
||||
|
|
@ -424,6 +429,7 @@ public partial class Settings : ViewModelBase
|
|||
this.BlogsApiUrl = "https://blogs.pschneider.fr/api/v1/";
|
||||
this.ApiUrl = "https://api.pschneider.fr/api/v1/";
|
||||
this.SearchText = string.Empty;
|
||||
this.ProviderOngoingRequestsSortOption = string.Empty;
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(billingCode))
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||
using Microsoft.Extensions.Localization;
|
||||
using Newtonsoft.Json;
|
||||
using System.Security.Claims;
|
||||
using Yavsc.Billing;
|
||||
using Yavsc.Helpers;
|
||||
using Yavsc.ViewModels;
|
||||
using Yavsc.Models.Billing;
|
||||
|
|
@ -100,6 +101,57 @@ namespace Yavsc.ApiControllers
|
|||
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}")]
|
||||
public async Task<IActionResult> ProSign(string billingCode, long id)
|
||||
|
|
@ -133,6 +185,23 @@ namespace Yavsc.ApiControllers
|
|||
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}")]
|
||||
public async Task<IActionResult> GetProSign(string billingCode, long id)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue