display my queries
This commit is contained in:
parent
62a6236865
commit
f4271b4052
11 changed files with 691 additions and 43 deletions
|
|
@ -4,10 +4,17 @@
|
|||
|
||||
### Added
|
||||
|
||||
* [PostIt] Une page d'historique des commandes billing permet maintenant d'ouvrir une commande existante.
|
||||
* [PostIt] Une vue "Demandes en cours" en lecture seule est disponible pour le performer, filtrée sur les statuts actifs (Inserted, Accepted, InProgress).
|
||||
|
||||
### Changed
|
||||
|
||||
* [PostIt] La page détail billing se préremplit depuis une commande existante (Rdv, Brush, MBrush) et passe en mode mise à jour.
|
||||
|
||||
### Fixed
|
||||
|
||||
* [PostIt] Le flux historique n'est plus limité à une simple liste: l'action d'ouverture charge la commande cible puis navigue vers la page détail.
|
||||
|
||||
## [1.0.8-rc9] - unstable
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -132,15 +132,71 @@ public class BillingCommandPageViewModelTests
|
|||
Assert.Equal(22, prestations[1].GetProperty("PrestationId").GetInt32());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InitializeAsync_with_existing_brush_query_prefills_and_submit_updates_query()
|
||||
{
|
||||
var api = new RecordingApi
|
||||
{
|
||||
HairPrestations = new List<HairPrestationDto>
|
||||
{
|
||||
new() { Id = 30, Title = "Femme · Cheveux longs", Details = "Coupe · Brushing" },
|
||||
new() { Id = 31, Title = "Homme · Cheveux courts", Details = "Coupe" },
|
||||
}
|
||||
};
|
||||
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);
|
||||
|
||||
await vm.InitializeAsync(new BillingQueryDetailsDto
|
||||
{
|
||||
Id = 77,
|
||||
BillingCode = "Brush",
|
||||
ActivityCode = "brush",
|
||||
PerformerId = "perf-2",
|
||||
ClientId = "cli-1",
|
||||
EventDate = new DateTime(2026, 9, 2, 14, 30, 0, DateTimeKind.Utc),
|
||||
Consent = true,
|
||||
Status = QueryStatus.Accepted,
|
||||
PrestationId = 30,
|
||||
AdditionalInfo = "Ancienne note",
|
||||
Location = new BillingLocationDto
|
||||
{
|
||||
Address = "1 rue du Test",
|
||||
Latitude = 48.8566,
|
||||
Longitude = 2.3522,
|
||||
}
|
||||
});
|
||||
|
||||
vm.SelectedPrestation = vm.AvailablePrestations[1];
|
||||
vm.AdditionalInfo = "Note mise à jour";
|
||||
await vm.SubmitCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.Equal(HttpMethod.Put, api.LastMethod);
|
||||
Assert.Equal("https://business.example/api/v1/billing/Brush/77", api.LastPath);
|
||||
Assert.True(vm.IsEditingExisting);
|
||||
Assert.Equal("Mettre à jour la commande", vm.SubmitLabel);
|
||||
|
||||
using var json = JsonDocument.Parse(JsonSerializer.Serialize(api.LastBody));
|
||||
Assert.Equal(77, json.RootElement.GetProperty("Id").GetInt32());
|
||||
Assert.Equal(31, json.RootElement.GetProperty("PrestationId").GetInt32());
|
||||
Assert.Equal("Note mise à jour", json.RootElement.GetProperty("AdditionalInfo").GetString());
|
||||
Assert.Equal((int)QueryStatus.Accepted, json.RootElement.GetProperty("Status").GetInt32());
|
||||
}
|
||||
|
||||
private sealed class RecordingApi : IYavscApiClient
|
||||
{
|
||||
public HttpClient Http { get; } = new();
|
||||
public HttpMethod? LastMethod { get; private set; }
|
||||
public string? LastPath { get; private set; }
|
||||
public object? LastBody { get; private set; }
|
||||
public List<HairPrestationDto>? HairPrestations { get; init; }
|
||||
|
||||
public Task<T> CallAsync<T>(HttpMethod method, string path, object? body = null, CancellationToken ct = default)
|
||||
{
|
||||
LastMethod = method;
|
||||
LastPath = path;
|
||||
LastBody = body;
|
||||
if (typeof(T) == typeof(List<HairPrestationDto>))
|
||||
|
|
@ -152,6 +208,7 @@ public class BillingCommandPageViewModelTests
|
|||
|
||||
public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default)
|
||||
{
|
||||
LastMethod = method;
|
||||
LastPath = path;
|
||||
LastBody = body;
|
||||
return Task.CompletedTask;
|
||||
|
|
|
|||
|
|
@ -22,9 +22,33 @@ public class BillingQueriesPageViewModelTests
|
|||
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);
|
||||
Assert.Equal(3, vm.Queries.Count);
|
||||
Assert.Contains(vm.Queries, q => q.Description == "Rendez-vous #1");
|
||||
Assert.Contains("3 commande", vm.StatusMessage, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RefreshAsync_in_readonly_ongoing_mode_keeps_only_ongoing_statuses_and_disables_open()
|
||||
{
|
||||
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,
|
||||
isReadOnly: true,
|
||||
ongoingOnly: true);
|
||||
|
||||
await vm.InitializeAsync();
|
||||
|
||||
Assert.Equal(2, vm.Queries.Count);
|
||||
Assert.All(vm.Queries, q => Assert.DoesNotContain("Rejected", q.StatusLabel, StringComparison.OrdinalIgnoreCase));
|
||||
Assert.Contains("lecture seule", vm.StatusMessage, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.False(vm.CanOpenDetails);
|
||||
|
||||
vm.SelectedQuery = vm.Queries[0];
|
||||
Assert.False(vm.OpenSelectedQueryCommand.CanExecute(null));
|
||||
}
|
||||
|
||||
private sealed class StubBillingApi : IYavscApiClient
|
||||
|
|
@ -70,6 +94,26 @@ public class BillingQueriesPageViewModelTests
|
|||
Status = QueryStatus.Accepted,
|
||||
Description = "Autre performer",
|
||||
EventDate = new DateTime(2026, 9, 3, 10, 0, 0, DateTimeKind.Utc),
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 14,
|
||||
ActivityCode = "dev",
|
||||
PerformerId = "perf-1",
|
||||
ClientId = "cli-1",
|
||||
Status = QueryStatus.InProgress,
|
||||
Description = "En cours",
|
||||
EventDate = new DateTime(2026, 9, 4, 10, 0, 0, DateTimeKind.Utc),
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = 15,
|
||||
ActivityCode = "dev",
|
||||
PerformerId = "perf-1",
|
||||
ClientId = "cli-1",
|
||||
Status = QueryStatus.Rejected,
|
||||
Description = "Rejetée",
|
||||
EventDate = new DateTime(2026, 9, 5, 10, 0, 0, DateTimeKind.Utc),
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,12 @@ public partial class BillingCommandPageViewModel : ViewModelBase
|
|||
[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 string Title => Form.Title;
|
||||
public string PerformerLabel => Performer.UserName;
|
||||
public string ActivityLabel => Activity.Name;
|
||||
|
|
@ -73,6 +79,8 @@ public partial class BillingCommandPageViewModel : ViewModelBase
|
|||
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."
|
||||
|
|
@ -108,10 +116,21 @@ public partial class BillingCommandPageViewModel : ViewModelBase
|
|||
StatusMessage = SupportMessage;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
partial void OnExistingQueryIdChanged(long? value)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsEditingExisting));
|
||||
OnPropertyChanged(nameof(SubmitLabel));
|
||||
}
|
||||
|
||||
public async Task InitializeAsync(BillingQueryDetailsDto? existingQuery = null)
|
||||
{
|
||||
if (!IsBrush && !IsMultiBrush)
|
||||
{
|
||||
if (existingQuery is not null)
|
||||
{
|
||||
ApplyExistingQuery(existingQuery);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -139,6 +158,11 @@ public partial class BillingCommandPageViewModel : ViewModelBase
|
|||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
|
||||
if (existingQuery is not null)
|
||||
{
|
||||
ApplyExistingQuery(existingQuery);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
|
@ -196,18 +220,44 @@ public partial class BillingCommandPageViewModel : ViewModelBase
|
|||
Longitude = 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 = location.Address,
|
||||
Latitude = location.Latitude,
|
||||
Longitude = location.Longitude,
|
||||
}
|
||||
};
|
||||
|
||||
if (IsRdv)
|
||||
{
|
||||
await _billingClient.CreateAsync(Form.ActionName, new
|
||||
if (IsEditingExisting)
|
||||
{
|
||||
ActivityCode = Activity.Code,
|
||||
PerformerId = Performer.PerformerId,
|
||||
Consent,
|
||||
EventDate = eventDate,
|
||||
Location = location,
|
||||
Reason = Reason.Trim(),
|
||||
Status = QueryStatus.Inserted,
|
||||
}).ConfigureAwait(true);
|
||||
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 = location,
|
||||
Reason = payload.Reason,
|
||||
Status = payload.Status,
|
||||
}).ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
else if (IsBrush)
|
||||
{
|
||||
|
|
@ -217,17 +267,26 @@ public partial class BillingCommandPageViewModel : ViewModelBase
|
|||
return;
|
||||
}
|
||||
|
||||
await _billingClient.CreateAsync(Form.ActionName, new
|
||||
payload.PrestationId = SelectedPrestation.Id;
|
||||
|
||||
if (IsEditingExisting)
|
||||
{
|
||||
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);
|
||||
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 = location,
|
||||
PrestationId = SelectedPrestation.Id,
|
||||
AdditionalInfo = string.IsNullOrWhiteSpace(AdditionalInfo) ? null : AdditionalInfo.Trim(),
|
||||
Status = payload.Status,
|
||||
}).ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
else if (IsMultiBrush)
|
||||
{
|
||||
|
|
@ -238,19 +297,30 @@ public partial class BillingCommandPageViewModel : ViewModelBase
|
|||
return;
|
||||
}
|
||||
|
||||
await _billingClient.CreateAsync(Form.ActionName, new
|
||||
payload.PrestationIds = selectedPrestations.Select(x => x.Id).ToList();
|
||||
|
||||
if (IsEditingExisting)
|
||||
{
|
||||
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);
|
||||
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 = location,
|
||||
Prestations = selectedPrestations.Select(x => new { PrestationId = x.Id }).ToList(),
|
||||
Status = payload.Status,
|
||||
}).ConfigureAwait(true);
|
||||
}
|
||||
}
|
||||
|
||||
StatusMessage = $"Commande transmise sur {BillingRoute} pour {Performer.UserName}.";
|
||||
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)
|
||||
{
|
||||
|
|
@ -266,6 +336,47 @@ public partial class BillingCommandPageViewModel : ViewModelBase
|
|||
}
|
||||
}
|
||||
|
||||
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);
|
||||
LongitudeText = existingQuery.Location.Longitude.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
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<long>()
|
||||
: new HashSet<long>(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(
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ 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.Api.Client;
|
||||
using Yavsc.Abstract.Workflow;
|
||||
|
||||
|
|
@ -18,18 +21,26 @@ public partial class BillingQueriesPageViewModel : ViewModelBase
|
|||
public ActivityBrowseItemDto Activity { get; }
|
||||
public ActivityUserDisplayItem Performer { get; }
|
||||
public CommandFormSummaryDto Form { get; }
|
||||
public bool IsReadOnly { get; }
|
||||
public bool OngoingOnly { get; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<BillingQueryDisplayItem> Queries { get; set; } = new();
|
||||
|
||||
[ObservableProperty, NotifyCanExecuteChangedFor(nameof(OpenSelectedQueryCommand))]
|
||||
public partial BillingQueryDisplayItem? SelectedQuery { get; set; }
|
||||
|
||||
[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 Title => IsReadOnly
|
||||
? $"Demandes en cours ({Form.Title})"
|
||||
: $"Commandes {Form.Title}";
|
||||
public string ContextLabel => $"{Performer.UserName} · {Activity.Name}";
|
||||
public bool CanOpenDetails => !IsReadOnly;
|
||||
|
||||
public override bool CanNavigateNext
|
||||
{
|
||||
|
|
@ -47,16 +58,22 @@ public partial class BillingQueriesPageViewModel : ViewModelBase
|
|||
ActivityBrowseItemDto activity,
|
||||
ActivityUserDisplayItem performer,
|
||||
CommandFormSummaryDto form,
|
||||
BillingApiClient billingClient)
|
||||
BillingApiClient billingClient,
|
||||
bool isReadOnly = false,
|
||||
bool ongoingOnly = false)
|
||||
{
|
||||
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));
|
||||
IsReadOnly = isReadOnly;
|
||||
OngoingOnly = ongoingOnly;
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => RefreshAsync();
|
||||
|
||||
private bool CanOpenSelectedQuery() => !IsReadOnly && SelectedQuery is not null;
|
||||
|
||||
[RelayCommand]
|
||||
public async Task RefreshAsync()
|
||||
{
|
||||
|
|
@ -66,15 +83,14 @@ public partial class BillingQueriesPageViewModel : ViewModelBase
|
|||
var list = await _billingClient.GetQuerySummariesAsync(Form.ActionName).ConfigureAwait(true);
|
||||
var filtered = (list ?? new())
|
||||
.Where(q => q.ActivityCode == Activity.Code && q.PerformerId == Performer.PerformerId)
|
||||
.Where(q => !OngoingOnly || IsOngoingStatus(q.Status))
|
||||
.OrderByDescending(q => q.EventDate ?? DateTime.MinValue)
|
||||
.ThenByDescending(q => q.Id)
|
||||
.Select(BillingQueryDisplayItem.FromDto)
|
||||
.ToList();
|
||||
|
||||
Queries = new ObservableCollection<BillingQueryDisplayItem>(filtered);
|
||||
StatusMessage = filtered.Count == 0
|
||||
? "Aucune commande trouvée pour ce formulaire."
|
||||
: $"{filtered.Count} commande(s) chargée(s).";
|
||||
StatusMessage = BuildLoadedStatusMessage(filtered.Count);
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
||||
{
|
||||
|
|
@ -91,4 +107,67 @@ public partial class BillingQueriesPageViewModel : ViewModelBase
|
|||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanOpenSelectedQuery))]
|
||||
public async Task OpenSelectedQueryAsync()
|
||||
{
|
||||
if (IsReadOnly)
|
||||
{
|
||||
StatusMessage = "Mode lecture seule: l'ouverture en modification est désactivée.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (SelectedQuery is null)
|
||||
{
|
||||
StatusMessage = "Sélectionnez une commande.";
|
||||
return;
|
||||
}
|
||||
|
||||
var app = (App?)Application.Current;
|
||||
if (app is null)
|
||||
{
|
||||
throw new InvalidOperationException("Application PostIt indisponible.");
|
||||
}
|
||||
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
var details = await _billingClient.GetQueryAsync(Form.ActionName, SelectedQuery.Id).ConfigureAwait(true);
|
||||
var vm = new BillingCommandPageViewModel(Activity, Performer, Form, _billingClient);
|
||||
await vm.InitializeAsync(details).ConfigureAwait(true);
|
||||
await app.PushPageAsync(vm).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'ouverture: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private string BuildLoadedStatusMessage(int count)
|
||||
{
|
||||
if (count == 0)
|
||||
{
|
||||
return OngoingOnly
|
||||
? "Aucune demande en cours pour ce formulaire."
|
||||
: "Aucune commande trouvée pour ce formulaire.";
|
||||
}
|
||||
|
||||
if (OngoingOnly)
|
||||
{
|
||||
return $"{count} demande(s) en cours chargée(s) (lecture seule).";
|
||||
}
|
||||
|
||||
return $"{count} commande(s) chargée(s).";
|
||||
}
|
||||
|
||||
private static bool IsOngoingStatus(QueryStatus status)
|
||||
=> status is QueryStatus.Inserted or QueryStatus.Accepted or QueryStatus.InProgress;
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ public partial class CommandFormsPageViewModel : ViewModelBase
|
|||
[ObservableProperty]
|
||||
public partial ObservableCollection<CommandFormSummaryDto> Forms { get; set; }
|
||||
|
||||
[ObservableProperty, NotifyCanExecuteChangedFor(nameof(OpenSelectedFormCommand)), NotifyCanExecuteChangedFor(nameof(OpenQueriesCommand))]
|
||||
[ObservableProperty, NotifyCanExecuteChangedFor(nameof(OpenSelectedFormCommand)), NotifyCanExecuteChangedFor(nameof(OpenQueriesCommand)), NotifyCanExecuteChangedFor(nameof(OpenOngoingQueriesCommand))]
|
||||
public partial CommandFormSummaryDto? SelectedForm { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
|
|
@ -64,6 +64,8 @@ public partial class CommandFormsPageViewModel : ViewModelBase
|
|||
|
||||
private bool CanOpenQueries() => SelectedForm is not null;
|
||||
|
||||
private bool CanOpenOngoingQueries() => SelectedForm is not null;
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanOpenSelectedForm))]
|
||||
private async Task OpenSelectedFormAsync()
|
||||
{
|
||||
|
|
@ -103,4 +105,30 @@ public partial class CommandFormsPageViewModel : ViewModelBase
|
|||
await vm.InitializeAsync();
|
||||
await app.PushPageAsync(vm);
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanOpenOngoingQueries))]
|
||||
private async Task OpenOngoingQueriesAsync()
|
||||
{
|
||||
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,
|
||||
isReadOnly: true,
|
||||
ongoingOnly: true);
|
||||
await vm.InitializeAsync();
|
||||
await app.PushPageAsync(vm);
|
||||
}
|
||||
}
|
||||
|
|
@ -108,7 +108,7 @@
|
|||
|
||||
<Grid Grid.Row="13" Grid.ColumnSpan="2" ColumnDefinitions="Auto,12,*,Auto">
|
||||
<Button Grid.Column="0"
|
||||
Content="Poster la commande"
|
||||
Content="{Binding SubmitLabel}"
|
||||
Command="{Binding SubmitCommand}"
|
||||
IsEnabled="{Binding IsSupported}" />
|
||||
<TextBox Grid.Column="2"
|
||||
|
|
|
|||
|
|
@ -12,9 +12,12 @@
|
|||
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="8" Margin="0,12,0,12">
|
||||
<Button Content="Rafraîchir" Command="{Binding RefreshCommand}" />
|
||||
<Button Content="Ouvrir la commande"
|
||||
Command="{Binding OpenSelectedQueryCommand}"
|
||||
IsVisible="{Binding CanOpenDetails}" />
|
||||
</StackPanel>
|
||||
|
||||
<ListBox Grid.Row="2" ItemsSource="{Binding Queries}">
|
||||
<ListBox Grid.Row="2" ItemsSource="{Binding Queries}" SelectedItem="{Binding SelectedQuery, Mode=TwoWay}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:BillingQueryDisplayItem">
|
||||
<Border BorderThickness="0,0,0,1" BorderBrush="#22000000" Padding="0,0,0,10" Margin="0,0,0,10">
|
||||
|
|
|
|||
|
|
@ -31,14 +31,17 @@
|
|||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<Grid Grid.Row="3" Margin="0,12,0,0" ColumnDefinitions="Auto,8,Auto,12,*,Auto">
|
||||
<Grid Grid.Row="3" Margin="0,12,0,0" ColumnDefinitions="Auto,8,Auto,8,Auto,12,*">
|
||||
<Button Grid.Column="0"
|
||||
Content="Ouvrir le formulaire"
|
||||
Command="{Binding OpenSelectedFormCommand}" />
|
||||
<Button Grid.Column="2"
|
||||
Content="Voir les commandes"
|
||||
Command="{Binding OpenQueriesCommand}" />
|
||||
<TextBox Grid.Column="4"
|
||||
<Button Grid.Column="4"
|
||||
Content="Demandes en cours (lecture seule)"
|
||||
Command="{Binding OpenOngoingQueriesCommand}" />
|
||||
<TextBox Grid.Column="6"
|
||||
Text="{Binding StatusMessage}"
|
||||
IsReadOnly="True"
|
||||
AcceptsReturn="True"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Yavsc.Models.Billing;
|
||||
using Yavsc.Models.Haircut;
|
||||
using Yavsc;
|
||||
|
||||
namespace Yavsc.Api.Client;
|
||||
|
||||
|
|
@ -69,5 +72,283 @@ public sealed class BillingApiClient
|
|||
return items;
|
||||
}
|
||||
|
||||
public async Task<BillingQueryDetailsDto> GetQueryAsync(string billingCode, long queryId, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(billingCode))
|
||||
throw new ArgumentException("Billing code is required.", nameof(billingCode));
|
||||
if (queryId <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(queryId));
|
||||
|
||||
var code = billingCode.Trim();
|
||||
var path = Absolute($"{PathPrefix}/{Uri.EscapeDataString(code)}/{queryId}");
|
||||
|
||||
if (string.Equals(code, BillingCodes.Rdv, StringComparison.Ordinal))
|
||||
{
|
||||
var dto = await _api.CallAsync<RdvQueryResponse>(HttpMethod.Get, path, ct: ct).ConfigureAwait(false);
|
||||
return MapRdv(dto, code);
|
||||
}
|
||||
|
||||
if (string.Equals(code, BillingCodes.Brush, StringComparison.Ordinal))
|
||||
{
|
||||
var dto = await _api.CallAsync<HairCutQueryResponse>(HttpMethod.Get, path, ct: ct).ConfigureAwait(false);
|
||||
return MapBrush(dto, code);
|
||||
}
|
||||
|
||||
if (string.Equals(code, BillingCodes.MBrush, StringComparison.Ordinal))
|
||||
{
|
||||
var dto = await _api.CallAsync<HairMultiCutQueryResponse>(HttpMethod.Get, path, ct: ct).ConfigureAwait(false);
|
||||
return MapMBrush(dto, code);
|
||||
}
|
||||
|
||||
throw new NotSupportedException($"Billing code '{code}' is not supported.");
|
||||
}
|
||||
|
||||
public Task UpdateAsync(string billingCode, long queryId, BillingQueryDetailsDto payload, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(billingCode))
|
||||
throw new ArgumentException("Billing code is required.", nameof(billingCode));
|
||||
if (queryId <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(queryId));
|
||||
if (payload is null)
|
||||
throw new ArgumentNullException(nameof(payload));
|
||||
|
||||
var code = billingCode.Trim();
|
||||
|
||||
return _api.CallAsync(
|
||||
HttpMethod.Put,
|
||||
Absolute($"{PathPrefix}/{Uri.EscapeDataString(code)}/{queryId}"),
|
||||
body: BuildUpdatePayload(code, queryId, payload),
|
||||
ct: ct);
|
||||
}
|
||||
|
||||
private string Absolute(string relativePath) => new Uri(_baseAddress, relativePath).ToString();
|
||||
|
||||
private static BillingQueryDetailsDto MapRdv(RdvQueryResponse dto, string billingCode)
|
||||
{
|
||||
return new BillingQueryDetailsDto
|
||||
{
|
||||
Id = dto.Id,
|
||||
BillingCode = billingCode,
|
||||
ActivityCode = dto.ActivityCode ?? string.Empty,
|
||||
PerformerId = dto.PerformerId ?? string.Empty,
|
||||
ClientId = dto.ClientId ?? string.Empty,
|
||||
Description = dto.Description ?? string.Empty,
|
||||
Consent = dto.Consent,
|
||||
EventDate = dto.EventDate,
|
||||
Status = dto.Status,
|
||||
Reason = dto.Reason ?? string.Empty,
|
||||
Provisional = dto.Provisional,
|
||||
Location = dto.Location is null
|
||||
? null
|
||||
: new BillingLocationDto
|
||||
{
|
||||
Address = dto.Location.Address ?? string.Empty,
|
||||
Latitude = dto.Location.Latitude,
|
||||
Longitude = dto.Location.Longitude,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static BillingQueryDetailsDto MapBrush(HairCutQueryResponse dto, string billingCode)
|
||||
{
|
||||
return new BillingQueryDetailsDto
|
||||
{
|
||||
Id = dto.Id,
|
||||
BillingCode = billingCode,
|
||||
ActivityCode = dto.ActivityCode ?? string.Empty,
|
||||
PerformerId = dto.PerformerId ?? string.Empty,
|
||||
ClientId = dto.ClientId ?? string.Empty,
|
||||
Description = dto.Description ?? string.Empty,
|
||||
Consent = dto.Consent,
|
||||
EventDate = dto.EventDate,
|
||||
Status = dto.Status,
|
||||
AdditionalInfo = dto.AdditionalInfo ?? string.Empty,
|
||||
Provisional = dto.Provisional,
|
||||
PrestationId = dto.PrestationId,
|
||||
Location = dto.Location is null
|
||||
? null
|
||||
: new BillingLocationDto
|
||||
{
|
||||
Address = dto.Location.Address ?? string.Empty,
|
||||
Latitude = dto.Location.Latitude,
|
||||
Longitude = dto.Location.Longitude,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static BillingQueryDetailsDto MapMBrush(HairMultiCutQueryResponse dto, string billingCode)
|
||||
{
|
||||
return new BillingQueryDetailsDto
|
||||
{
|
||||
Id = dto.Id,
|
||||
BillingCode = billingCode,
|
||||
ActivityCode = dto.ActivityCode ?? string.Empty,
|
||||
PerformerId = dto.PerformerId ?? string.Empty,
|
||||
ClientId = dto.ClientId ?? string.Empty,
|
||||
Description = dto.Description ?? string.Empty,
|
||||
Consent = dto.Consent,
|
||||
EventDate = dto.EventDate,
|
||||
Status = dto.Status,
|
||||
Provisional = dto.Provisional,
|
||||
PrestationIds = (dto.Prestations ?? new List<HairPrestationCollectionItemResponse>())
|
||||
.Select(p => p.PrestationId)
|
||||
.Where(id => id > 0)
|
||||
.ToList(),
|
||||
Location = dto.Location is null
|
||||
? null
|
||||
: new BillingLocationDto
|
||||
{
|
||||
Address = dto.Location.Address ?? string.Empty,
|
||||
Latitude = dto.Location.Latitude,
|
||||
Longitude = dto.Location.Longitude,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static object BuildUpdatePayload(string billingCode, long queryId, BillingQueryDetailsDto payload)
|
||||
{
|
||||
if (string.Equals(billingCode, BillingCodes.Rdv, StringComparison.Ordinal))
|
||||
{
|
||||
if (payload.EventDate is null)
|
||||
throw new ArgumentException("EventDate is required for Rdv.", nameof(payload));
|
||||
|
||||
return new
|
||||
{
|
||||
Id = queryId,
|
||||
ActivityCode = payload.ActivityCode,
|
||||
PerformerId = payload.PerformerId,
|
||||
ClientId = payload.ClientId,
|
||||
Consent = payload.Consent,
|
||||
EventDate = payload.EventDate.Value,
|
||||
Location = ToLocationPayload(payload.Location),
|
||||
Reason = payload.Reason,
|
||||
Status = payload.Status,
|
||||
Provisional = payload.Provisional,
|
||||
Description = payload.Description,
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(billingCode, BillingCodes.Brush, StringComparison.Ordinal))
|
||||
{
|
||||
if (payload.PrestationId is null || payload.PrestationId <= 0)
|
||||
throw new ArgumentException("PrestationId is required for Brush.", nameof(payload));
|
||||
|
||||
return new
|
||||
{
|
||||
Id = queryId,
|
||||
ActivityCode = payload.ActivityCode,
|
||||
PerformerId = payload.PerformerId,
|
||||
ClientId = payload.ClientId,
|
||||
Consent = payload.Consent,
|
||||
EventDate = payload.EventDate,
|
||||
Location = ToLocationPayload(payload.Location),
|
||||
PrestationId = payload.PrestationId.Value,
|
||||
AdditionalInfo = payload.AdditionalInfo,
|
||||
Status = payload.Status,
|
||||
Provisional = payload.Provisional,
|
||||
Description = payload.Description,
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(billingCode, BillingCodes.MBrush, StringComparison.Ordinal))
|
||||
{
|
||||
if (payload.EventDate is null)
|
||||
throw new ArgumentException("EventDate is required for MBrush.", nameof(payload));
|
||||
if (payload.PrestationIds is null || payload.PrestationIds.Count == 0)
|
||||
throw new ArgumentException("At least one prestation is required for MBrush.", nameof(payload));
|
||||
|
||||
return new
|
||||
{
|
||||
Id = queryId,
|
||||
ActivityCode = payload.ActivityCode,
|
||||
PerformerId = payload.PerformerId,
|
||||
ClientId = payload.ClientId,
|
||||
Consent = payload.Consent,
|
||||
EventDate = payload.EventDate.Value,
|
||||
Location = ToLocationPayload(payload.Location),
|
||||
Prestations = payload.PrestationIds
|
||||
.Where(id => id > 0)
|
||||
.Select(id => new { PrestationId = id })
|
||||
.ToList(),
|
||||
Status = payload.Status,
|
||||
Provisional = payload.Provisional,
|
||||
Description = payload.Description,
|
||||
};
|
||||
}
|
||||
|
||||
throw new NotSupportedException($"Billing code '{billingCode}' is not supported.");
|
||||
}
|
||||
|
||||
private static object? ToLocationPayload(BillingLocationDto? location)
|
||||
{
|
||||
if (location is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
Address = location.Address,
|
||||
Latitude = location.Latitude,
|
||||
Longitude = location.Longitude,
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class BillingLocationResponse
|
||||
{
|
||||
public string? Address { get; set; }
|
||||
public double Latitude { get; set; }
|
||||
public double Longitude { get; set; }
|
||||
}
|
||||
|
||||
private sealed class RdvQueryResponse
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string? ActivityCode { get; set; }
|
||||
public string? PerformerId { get; set; }
|
||||
public string? ClientId { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public bool Consent { get; set; }
|
||||
public DateTime EventDate { get; set; }
|
||||
public QueryStatus Status { get; set; }
|
||||
public string? Reason { get; set; }
|
||||
public decimal? Provisional { get; set; }
|
||||
public BillingLocationResponse? Location { get; set; }
|
||||
}
|
||||
|
||||
private sealed class HairCutQueryResponse
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string? ActivityCode { get; set; }
|
||||
public string? PerformerId { get; set; }
|
||||
public string? ClientId { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public bool Consent { get; set; }
|
||||
public DateTime? EventDate { get; set; }
|
||||
public QueryStatus Status { get; set; }
|
||||
public decimal? Provisional { get; set; }
|
||||
public long PrestationId { get; set; }
|
||||
public string? AdditionalInfo { get; set; }
|
||||
public BillingLocationResponse? Location { get; set; }
|
||||
}
|
||||
|
||||
private sealed class HairMultiCutQueryResponse
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string? ActivityCode { get; set; }
|
||||
public string? PerformerId { get; set; }
|
||||
public string? ClientId { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public bool Consent { get; set; }
|
||||
public DateTime EventDate { get; set; }
|
||||
public QueryStatus Status { get; set; }
|
||||
public decimal? Provisional { get; set; }
|
||||
public BillingLocationResponse? Location { get; set; }
|
||||
public List<HairPrestationCollectionItemResponse>? Prestations { get; set; }
|
||||
}
|
||||
|
||||
private sealed class HairPrestationCollectionItemResponse
|
||||
{
|
||||
public long PrestationId { get; set; }
|
||||
}
|
||||
}
|
||||
35
src/Yavsc.Api.Client/Dtos/BillingQueryDetailsDto.cs
Normal file
35
src/Yavsc.Api.Client/Dtos/BillingQueryDetailsDto.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Yavsc;
|
||||
|
||||
namespace Yavsc.Api.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Normalized billing-query shape used by PostIt when opening an existing
|
||||
/// command from history.
|
||||
/// </summary>
|
||||
public sealed class BillingQueryDetailsDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string BillingCode { get; set; } = string.Empty;
|
||||
public string ActivityCode { get; set; } = string.Empty;
|
||||
public string PerformerId { get; set; } = string.Empty;
|
||||
public string ClientId { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public bool Consent { get; set; } = true;
|
||||
public DateTime? EventDate { get; set; }
|
||||
public QueryStatus Status { get; set; } = QueryStatus.Inserted;
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
public string AdditionalInfo { get; set; } = string.Empty;
|
||||
public decimal? Provisional { get; set; }
|
||||
public BillingLocationDto? Location { get; set; }
|
||||
public long? PrestationId { get; set; }
|
||||
public List<long> PrestationIds { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class BillingLocationDto
|
||||
{
|
||||
public string Address { get; set; } = string.Empty;
|
||||
public double Latitude { get; set; }
|
||||
public double Longitude { get; set; }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue