Compare commits
No commits in common. "f4271b4052016ae18f888f0945e72f527b65ab82" and "7b9274515344bb149da640486ba2fdd28026f1e6" have entirely different histories.
f4271b4052
...
7b92745153
41 changed files with 146 additions and 3130 deletions
|
|
@ -4,17 +4,10 @@
|
||||||
|
|
||||||
### Added
|
### 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
|
### Changed
|
||||||
|
|
||||||
* [PostIt] La page détail billing se préremplit depuis une commande existante (Rdv, Brush, MBrush) et passe en mode mise à jour.
|
|
||||||
|
|
||||||
### Fixed
|
### 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
|
## [1.0.8-rc9] - unstable
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ Trois principes non négociables traversent tous les jalons :
|
||||||
>
|
>
|
||||||
> Chaque jalon a un **critère de sortie** vérifiable.
|
> Chaque jalon a un **critère de sortie** vérifiable.
|
||||||
|
|
||||||
### Jalon 0 — Fondations techniques
|
### Jalon 0 — Fondations techniques *(en cours)*
|
||||||
|
|
||||||
> Cible : pouvoir parler du domaine sans se battre avec le runtime.
|
> Cible : pouvoir parler du domaine sans se battre avec le runtime.
|
||||||
|
|
||||||
|
|
@ -81,7 +81,7 @@ Trois principes non négociables traversent tous les jalons :
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Jalon 1 — Prestation signée de bout en bout *(en cours)*
|
### Jalon 1 — Prestation signée de bout en bout
|
||||||
|
|
||||||
> Cible : un projet client/fournisseur aboutit à un **devis signé par les deux parties**, traçable, avec notifications.
|
> Cible : un projet client/fournisseur aboutit à un **devis signé par les deux parties**, traçable, avec notifications.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,17 +12,12 @@ public class ActivitiesPageViewModelTests
|
||||||
{
|
{
|
||||||
var api = new StubActivityApi();
|
var api = new StubActivityApi();
|
||||||
var client = new ActivityApiClient(api, "https://business.example/api/v1/");
|
var client = new ActivityApiClient(api, "https://business.example/api/v1/");
|
||||||
var billingClient = new BillingApiClient(api, "https://business.example/api/v1/");
|
|
||||||
|
|
||||||
await client.GetCatalogAsync("brush", TestContext.Current.CancellationToken);
|
await client.GetCatalogAsync("brush", TestContext.Current.CancellationToken);
|
||||||
await client.GetUsersAsync("brush-pro", TestContext.Current.CancellationToken);
|
await client.GetUsersAsync("brush-pro", TestContext.Current.CancellationToken);
|
||||||
await billingClient.CreateAsync("Rdv", new { Foo = "Bar" }, TestContext.Current.CancellationToken);
|
|
||||||
await billingClient.GetQuerySummariesAsync("Rdv", TestContext.Current.CancellationToken);
|
|
||||||
|
|
||||||
Assert.Equal("https://business.example/api/v1/activity/catalog?parentCode=brush", api.Paths[0]);
|
Assert.Equal("https://business.example/api/v1/activity/catalog?parentCode=brush", api.Paths[0]);
|
||||||
Assert.Equal("https://business.example/api/v1/activity/brush-pro/users", api.Paths[1]);
|
Assert.Equal("https://business.example/api/v1/activity/brush-pro/users", api.Paths[1]);
|
||||||
Assert.Equal("https://business.example/api/v1/billing/Rdv", api.Paths[2]);
|
|
||||||
Assert.Equal("https://business.example/api/v1/billing/Rdv", api.Paths[3]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -30,8 +25,7 @@ public class ActivitiesPageViewModelTests
|
||||||
{
|
{
|
||||||
var api = new StubActivityApi();
|
var api = new StubActivityApi();
|
||||||
var client = new ActivityApiClient(api, "https://business.example/api/v1/");
|
var client = new ActivityApiClient(api, "https://business.example/api/v1/");
|
||||||
var billingClient = new BillingApiClient(api, "https://business.example/api/v1/");
|
var vm = new ActivitiesPageViewModel(client);
|
||||||
var vm = new ActivitiesPageViewModel(client, billingClient);
|
|
||||||
|
|
||||||
await vm.RefreshAsync();
|
await vm.RefreshAsync();
|
||||||
|
|
||||||
|
|
@ -82,10 +76,6 @@ public class ActivitiesPageViewModelTests
|
||||||
Name = "Brush",
|
Name = "Brush",
|
||||||
Description = "Coiffure à domicile",
|
Description = "Coiffure à domicile",
|
||||||
PerformerCount = 1,
|
PerformerCount = 1,
|
||||||
Forms = new List<CommandFormSummaryDto>
|
|
||||||
{
|
|
||||||
new() { Id = 1, ActionName = "Rdv", Title = "Rendez-vous" }
|
|
||||||
},
|
|
||||||
Children = new List<ActivityBrowseItemDto>
|
Children = new List<ActivityBrowseItemDto>
|
||||||
{
|
{
|
||||||
new()
|
new()
|
||||||
|
|
@ -95,10 +85,6 @@ public class ActivitiesPageViewModelTests
|
||||||
Description = "Spécialisation premium",
|
Description = "Spécialisation premium",
|
||||||
ParentCode = "brush",
|
ParentCode = "brush",
|
||||||
PerformerCount = 1,
|
PerformerCount = 1,
|
||||||
Forms = new List<CommandFormSummaryDto>
|
|
||||||
{
|
|
||||||
new() { Id = 2, ActionName = "Rdv", Title = "Rendez-vous premium" }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,219 +0,0 @@
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text.Json;
|
|
||||||
using PostIt.ViewModels;
|
|
||||||
using Yavsc;
|
|
||||||
using Yavsc.Abstract.Workflow;
|
|
||||||
using Yavsc.Api.Client;
|
|
||||||
using Yavsc.Models.Haircut;
|
|
||||||
|
|
||||||
namespace PostIt.Tests;
|
|
||||||
|
|
||||||
public class BillingCommandPageViewModelTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public async Task SubmitAsync_posts_rdv_payload_to_selected_billing_route()
|
|
||||||
{
|
|
||||||
var api = new RecordingApi();
|
|
||||||
var client = new BillingApiClient(api, "https://business.example/api/v1/");
|
|
||||||
var vm = new BillingCommandPageViewModel(
|
|
||||||
new ActivityBrowseItemDto { Code = "dev", Name = "Développement" },
|
|
||||||
new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" },
|
|
||||||
new CommandFormSummaryDto { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" },
|
|
||||||
client)
|
|
||||||
{
|
|
||||||
EventDateText = "2026-09-02 14:30",
|
|
||||||
Reason = "Point de cadrage",
|
|
||||||
Address = "1 rue du Test",
|
|
||||||
LatitudeText = "48.8566",
|
|
||||||
LongitudeText = "2.3522",
|
|
||||||
Consent = true,
|
|
||||||
};
|
|
||||||
|
|
||||||
await vm.SubmitCommand.ExecuteAsync(null);
|
|
||||||
|
|
||||||
Assert.Equal("https://business.example/api/v1/billing/Rdv", api.LastPath);
|
|
||||||
Assert.NotNull(api.LastBody);
|
|
||||||
|
|
||||||
using var json = JsonDocument.Parse(JsonSerializer.Serialize(api.LastBody));
|
|
||||||
Assert.Equal("dev", json.RootElement.GetProperty("ActivityCode").GetString());
|
|
||||||
Assert.Equal("perf-1", json.RootElement.GetProperty("PerformerId").GetString());
|
|
||||||
Assert.Equal("Point de cadrage", json.RootElement.GetProperty("Reason").GetString());
|
|
||||||
Assert.Equal((int)QueryStatus.Inserted, json.RootElement.GetProperty("Status").GetInt32());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task SubmitAsync_refuses_unsupported_billing_code()
|
|
||||||
{
|
|
||||||
var api = new RecordingApi();
|
|
||||||
var client = new BillingApiClient(api, "https://business.example/api/v1/");
|
|
||||||
var vm = new BillingCommandPageViewModel(
|
|
||||||
new ActivityBrowseItemDto { Code = "book", Name = "Book" },
|
|
||||||
new ActivityUserDisplayItem { PerformerId = "perf-2", UserName = "Bob" },
|
|
||||||
new CommandFormSummaryDto { Id = 13, ActionName = "Book", Title = "Réservation" },
|
|
||||||
client);
|
|
||||||
|
|
||||||
await vm.SubmitCommand.ExecuteAsync(null);
|
|
||||||
|
|
||||||
Assert.Null(api.LastPath);
|
|
||||||
Assert.Contains("n'est pas encore pris en charge", vm.StatusMessage, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task InitializeAsync_loads_prestations_for_brush_and_submit_posts_selected_prestation()
|
|
||||||
{
|
|
||||||
var api = new RecordingApi
|
|
||||||
{
|
|
||||||
HairPrestations = new List<HairPrestationDto>
|
|
||||||
{
|
|
||||||
new() { Id = 10, Title = "Femme · Cheveux mi-longs", Details = "Coupe · Brushing" },
|
|
||||||
new() { Id = 11, Title = "Homme · Cheveux courts", Details = "Coupe · Coiffage" },
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var client = new BillingApiClient(api, "https://business.example/api/v1/");
|
|
||||||
var vm = new BillingCommandPageViewModel(
|
|
||||||
new ActivityBrowseItemDto { Code = "brush", Name = "Brush" },
|
|
||||||
new ActivityUserDisplayItem { PerformerId = "perf-2", UserName = "Bob" },
|
|
||||||
new CommandFormSummaryDto { Id = 13, ActionName = "Brush", Title = "Coupe" },
|
|
||||||
client)
|
|
||||||
{
|
|
||||||
EventDateText = "2026-09-02 14:30",
|
|
||||||
Address = "1 rue du Test",
|
|
||||||
LatitudeText = "48.8566",
|
|
||||||
LongitudeText = "2.3522",
|
|
||||||
Consent = true,
|
|
||||||
AdditionalInfo = "Prévoir shampoing",
|
|
||||||
};
|
|
||||||
|
|
||||||
await vm.InitializeAsync();
|
|
||||||
vm.SelectedPrestation = vm.AvailablePrestations[1];
|
|
||||||
await vm.SubmitCommand.ExecuteAsync(null);
|
|
||||||
|
|
||||||
Assert.Equal("https://business.example/api/v1/billing/Brush", api.LastPath);
|
|
||||||
using var json = JsonDocument.Parse(JsonSerializer.Serialize(api.LastBody));
|
|
||||||
Assert.Equal(11, json.RootElement.GetProperty("PrestationId").GetInt32());
|
|
||||||
Assert.Equal("Prévoir shampoing", json.RootElement.GetProperty("AdditionalInfo").GetString());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task InitializeAsync_loads_prestations_for_mbrush_and_submit_posts_selected_prestations()
|
|
||||||
{
|
|
||||||
var api = new RecordingApi
|
|
||||||
{
|
|
||||||
HairPrestations = new List<HairPrestationDto>
|
|
||||||
{
|
|
||||||
new() { Id = 21, Title = "Femme · Cheveux longs", Details = "Coupe · Couleur" },
|
|
||||||
new() { Id = 22, Title = "Enfant · Cheveux courts", Details = "Coupe · Sans technique" },
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var client = new BillingApiClient(api, "https://business.example/api/v1/");
|
|
||||||
var vm = new BillingCommandPageViewModel(
|
|
||||||
new ActivityBrowseItemDto { Code = "mbrush", Name = "MBrush" },
|
|
||||||
new ActivityUserDisplayItem { PerformerId = "perf-3", UserName = "Cara" },
|
|
||||||
new CommandFormSummaryDto { Id = 14, ActionName = "MBrush", Title = "Coupe groupée" },
|
|
||||||
client)
|
|
||||||
{
|
|
||||||
EventDateText = "2026-09-03 10:00",
|
|
||||||
Address = "2 rue du Test",
|
|
||||||
LatitudeText = "48.8567",
|
|
||||||
LongitudeText = "2.3523",
|
|
||||||
Consent = true,
|
|
||||||
};
|
|
||||||
|
|
||||||
await vm.InitializeAsync();
|
|
||||||
vm.MultiPrestations[0].IsSelected = true;
|
|
||||||
vm.MultiPrestations[1].IsSelected = true;
|
|
||||||
await vm.SubmitCommand.ExecuteAsync(null);
|
|
||||||
|
|
||||||
Assert.Equal("https://business.example/api/v1/billing/MBrush", api.LastPath);
|
|
||||||
using var json = JsonDocument.Parse(JsonSerializer.Serialize(api.LastBody));
|
|
||||||
var prestations = json.RootElement.GetProperty("Prestations");
|
|
||||||
Assert.Equal(2, prestations.GetArrayLength());
|
|
||||||
Assert.Equal(21, prestations[0].GetProperty("PrestationId").GetInt32());
|
|
||||||
Assert.Equal(22, prestations[1].GetProperty("PrestationId").GetInt32());
|
|
||||||
}
|
|
||||||
|
|
||||||
[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>))
|
|
||||||
{
|
|
||||||
return Task.FromResult((T)(object)(HairPrestations ?? new List<HairPrestationDto>()));
|
|
||||||
}
|
|
||||||
return Task.FromResult(default(T)!);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
LastMethod = method;
|
|
||||||
LastPath = path;
|
|
||||||
LastBody = body;
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
using System.Net.Http;
|
|
||||||
using PostIt.ViewModels;
|
|
||||||
using Yavsc;
|
|
||||||
using Yavsc.Abstract.Workflow;
|
|
||||||
using Yavsc.Api.Client;
|
|
||||||
|
|
||||||
namespace PostIt.Tests;
|
|
||||||
|
|
||||||
public class BillingQueriesPageViewModelTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public async Task RefreshAsync_filters_queries_by_selected_activity_and_performer()
|
|
||||||
{
|
|
||||||
var api = new StubBillingApi();
|
|
||||||
var client = new BillingApiClient(api, "https://business.example/api/v1/");
|
|
||||||
var vm = new BillingQueriesPageViewModel(
|
|
||||||
new ActivityBrowseItemDto { Code = "dev", Name = "Développement" },
|
|
||||||
new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" },
|
|
||||||
new CommandFormSummaryDto { Id = 1, ActionName = "Rdv", Title = "Rendez-vous" },
|
|
||||||
client);
|
|
||||||
|
|
||||||
await vm.InitializeAsync();
|
|
||||||
|
|
||||||
Assert.Equal("https://business.example/api/v1/billing/Rdv", api.Paths.Single());
|
|
||||||
Assert.Equal(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
|
|
||||||
{
|
|
||||||
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 = 11,
|
|
||||||
ActivityCode = "dev",
|
|
||||||
PerformerId = "perf-1",
|
|
||||||
ClientId = "cli-1",
|
|
||||||
Status = QueryStatus.Inserted,
|
|
||||||
Description = "Rendez-vous #1",
|
|
||||||
Reason = "Point de cadrage",
|
|
||||||
EventDate = new DateTime(2026, 9, 1, 10, 0, 0, DateTimeKind.Utc),
|
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Id = 12,
|
|
||||||
ActivityCode = "other",
|
|
||||||
PerformerId = "perf-1",
|
|
||||||
ClientId = "cli-1",
|
|
||||||
Status = QueryStatus.Accepted,
|
|
||||||
Description = "Autre activité",
|
|
||||||
EventDate = new DateTime(2026, 9, 2, 10, 0, 0, DateTimeKind.Utc),
|
|
||||||
},
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Id = 13,
|
|
||||||
ActivityCode = "dev",
|
|
||||||
PerformerId = "perf-2",
|
|
||||||
ClientId = "cli-1",
|
|
||||||
Status = QueryStatus.Accepted,
|
|
||||||
Description = "Autre performer",
|
|
||||||
EventDate = new DateTime(2026, 9, 3, 10, 0, 0, DateTimeKind.Utc),
|
|
||||||
},
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return Task.FromResult((T)(object)data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult(default(T)!);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
Paths.Add(path);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -24,7 +24,6 @@ public static class ServiceCollectionHelpers
|
||||||
var blogAclClient = new BlogAclApiClient(api, settings.BlogsApiUrl);
|
var blogAclClient = new BlogAclApiClient(api, settings.BlogsApiUrl);
|
||||||
var userSearchClient = new UserSearchClient(api, settings.BlogsApiUrl);
|
var userSearchClient = new UserSearchClient(api, settings.BlogsApiUrl);
|
||||||
var activityClient = new ActivityApiClient(api, settings.BusinessApiUrl);
|
var activityClient = new ActivityApiClient(api, settings.BusinessApiUrl);
|
||||||
var billingClient = new BillingApiClient(api, settings.BusinessApiUrl);
|
|
||||||
var userDirectory = new UserDirectory(userSearchClient);
|
var userDirectory = new UserDirectory(userSearchClient);
|
||||||
|
|
||||||
// Vues
|
// Vues
|
||||||
|
|
@ -48,9 +47,6 @@ public static class ServiceCollectionHelpers
|
||||||
services.AddSingleton<SignaturePage>();
|
services.AddSingleton<SignaturePage>();
|
||||||
services.AddSingleton<CirclesPage>();
|
services.AddSingleton<CirclesPage>();
|
||||||
services.AddSingleton<ActivitiesPage>();
|
services.AddSingleton<ActivitiesPage>();
|
||||||
services.AddTransient<CommandFormsPage>();
|
|
||||||
services.AddTransient<BillingCommandPage>();
|
|
||||||
services.AddTransient<BillingQueriesPage>();
|
|
||||||
// ViewModels
|
// ViewModels
|
||||||
services.AddSingleton(settings);
|
services.AddSingleton(settings);
|
||||||
services.AddSingleton<YavscApiClient>(api);
|
services.AddSingleton<YavscApiClient>(api);
|
||||||
|
|
@ -59,13 +55,11 @@ public static class ServiceCollectionHelpers
|
||||||
services.AddSingleton(blogAclClient);
|
services.AddSingleton(blogAclClient);
|
||||||
services.AddSingleton(userSearchClient);
|
services.AddSingleton(userSearchClient);
|
||||||
services.AddSingleton(activityClient);
|
services.AddSingleton(activityClient);
|
||||||
services.AddSingleton(billingClient);
|
|
||||||
services.AddSingleton<IUserDirectory>(userDirectory);
|
services.AddSingleton<IUserDirectory>(userDirectory);
|
||||||
services.AddSingleton<HomePageViewModel>();
|
services.AddSingleton<HomePageViewModel>();
|
||||||
services.AddSingleton<SignaturePageViewModel>();
|
services.AddSingleton<SignaturePageViewModel>();
|
||||||
services.AddSingleton<CirclesPageViewModel>();
|
services.AddSingleton<CirclesPageViewModel>();
|
||||||
services.AddSingleton<ActivitiesPageViewModel>();
|
services.AddSingleton<ActivitiesPageViewModel>();
|
||||||
services.AddTransient<SelectableHairPrestationItem>();
|
|
||||||
|
|
||||||
// Dialogs (modal-light pages): the ViewLocator resolves
|
// Dialogs (modal-light pages): the ViewLocator resolves
|
||||||
// them when a caller pushes a PostAclDialogViewModel or
|
// them when a caller pushes a PostAclDialogViewModel or
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,6 @@ public class ViewLocator : IDataTemplate
|
||||||
Settings => services.GetRequiredService<SettingsPage>(),
|
Settings => services.GetRequiredService<SettingsPage>(),
|
||||||
HomePageViewModel => services.GetRequiredService<HomePage>(),
|
HomePageViewModel => services.GetRequiredService<HomePage>(),
|
||||||
ActivitiesPageViewModel => services.GetRequiredService<ActivitiesPage>(),
|
ActivitiesPageViewModel => services.GetRequiredService<ActivitiesPage>(),
|
||||||
CommandFormsPageViewModel => services.GetRequiredService<CommandFormsPage>(),
|
|
||||||
BillingCommandPageViewModel => services.GetRequiredService<BillingCommandPage>(),
|
|
||||||
BillingQueriesPageViewModel => services.GetRequiredService<BillingQueriesPage>(),
|
|
||||||
SignaturePageViewModel => services.GetRequiredService<SignaturePage>(),
|
SignaturePageViewModel => services.GetRequiredService<SignaturePage>(),
|
||||||
AddCircleMemberDialogViewModel => services.GetRequiredService<AddCircleMemberDialog>(),
|
AddCircleMemberDialogViewModel => services.GetRequiredService<AddCircleMemberDialog>(),
|
||||||
CirclesPageViewModel => services.GetRequiredService<CirclesPage>(),
|
CirclesPageViewModel => services.GetRequiredService<CirclesPage>(),
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,8 @@ using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using PostIt.Helpers;
|
|
||||||
using Yavsc.Abstract.Workflow;
|
using Yavsc.Abstract.Workflow;
|
||||||
using Yavsc.Api.Client;
|
using Yavsc.Api.Client;
|
||||||
|
|
||||||
|
|
@ -16,7 +14,6 @@ namespace PostIt.ViewModels;
|
||||||
public partial class ActivitiesPageViewModel : ViewModelBase
|
public partial class ActivitiesPageViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
private readonly ActivityApiClient _client;
|
private readonly ActivityApiClient _client;
|
||||||
private readonly BillingApiClient _billingClient;
|
|
||||||
private bool _syncingSelection;
|
private bool _syncingSelection;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
|
@ -34,9 +31,6 @@ public partial class ActivitiesPageViewModel : ViewModelBase
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial ObservableCollection<ActivityUserDisplayItem> Performers { get; set; } = new();
|
public partial ObservableCollection<ActivityUserDisplayItem> Performers { get; set; } = new();
|
||||||
|
|
||||||
[ObservableProperty, NotifyCanExecuteChangedFor(nameof(OpenCommandFormsCommand))]
|
|
||||||
public partial ActivityUserDisplayItem? SelectedPerformer { get; set; }
|
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial bool IsBusy { get; set; }
|
public partial bool IsBusy { get; set; }
|
||||||
|
|
||||||
|
|
@ -46,7 +40,6 @@ public partial class ActivitiesPageViewModel : ViewModelBase
|
||||||
public ActivityBrowseItemDto? CurrentActivity => SelectedSpecialization ?? SelectedActivity;
|
public ActivityBrowseItemDto? CurrentActivity => SelectedSpecialization ?? SelectedActivity;
|
||||||
public string SelectedActivityLabel => SelectedActivity?.Name ?? "(aucune activité)";
|
public string SelectedActivityLabel => SelectedActivity?.Name ?? "(aucune activité)";
|
||||||
public string CurrentActivityLabel => CurrentActivity?.Name ?? "(aucune)";
|
public string CurrentActivityLabel => CurrentActivity?.Name ?? "(aucune)";
|
||||||
public int CurrentFormCount => CurrentActivity?.Forms?.Count ?? 0;
|
|
||||||
|
|
||||||
public override bool CanNavigateNext
|
public override bool CanNavigateNext
|
||||||
{
|
{
|
||||||
|
|
@ -60,10 +53,9 @@ public partial class ActivitiesPageViewModel : ViewModelBase
|
||||||
protected set { _ = value; }
|
protected set { _ = value; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public ActivitiesPageViewModel(ActivityApiClient client, BillingApiClient billingClient)
|
public ActivitiesPageViewModel(ActivityApiClient client)
|
||||||
{
|
{
|
||||||
_client = client ?? throw new ArgumentNullException(nameof(client));
|
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
_billingClient = billingClient ?? throw new ArgumentNullException(nameof(billingClient));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnSelectedActivityChanged(ActivityBrowseItemDto? value)
|
partial void OnSelectedActivityChanged(ActivityBrowseItemDto? value)
|
||||||
|
|
@ -162,13 +154,11 @@ public partial class ActivitiesPageViewModel : ViewModelBase
|
||||||
OnPropertyChanged(nameof(CurrentActivity));
|
OnPropertyChanged(nameof(CurrentActivity));
|
||||||
OnPropertyChanged(nameof(SelectedActivityLabel));
|
OnPropertyChanged(nameof(SelectedActivityLabel));
|
||||||
OnPropertyChanged(nameof(CurrentActivityLabel));
|
OnPropertyChanged(nameof(CurrentActivityLabel));
|
||||||
OnPropertyChanged(nameof(CurrentFormCount));
|
|
||||||
Specializations = new ObservableCollection<ActivityBrowseItemDto>(activity?.Children ?? new());
|
Specializations = new ObservableCollection<ActivityBrowseItemDto>(activity?.Children ?? new());
|
||||||
|
|
||||||
if (activity is null)
|
if (activity is null)
|
||||||
{
|
{
|
||||||
Performers = new ObservableCollection<ActivityUserDisplayItem>();
|
Performers = new ObservableCollection<ActivityUserDisplayItem>();
|
||||||
SelectedPerformer = null;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,7 +179,6 @@ public partial class ActivitiesPageViewModel : ViewModelBase
|
||||||
|
|
||||||
OnPropertyChanged(nameof(CurrentActivity));
|
OnPropertyChanged(nameof(CurrentActivity));
|
||||||
OnPropertyChanged(nameof(CurrentActivityLabel));
|
OnPropertyChanged(nameof(CurrentActivityLabel));
|
||||||
OnPropertyChanged(nameof(CurrentFormCount));
|
|
||||||
|
|
||||||
if (specialization is null)
|
if (specialization is null)
|
||||||
{
|
{
|
||||||
|
|
@ -211,47 +200,21 @@ public partial class ActivitiesPageViewModel : ViewModelBase
|
||||||
var list = await _client.GetUsersAsync(activity.Code);
|
var list = await _client.GetUsersAsync(activity.Code);
|
||||||
Performers = new ObservableCollection<ActivityUserDisplayItem>((list ?? new())
|
Performers = new ObservableCollection<ActivityUserDisplayItem>((list ?? new())
|
||||||
.Select(ActivityUserDisplayItem.FromDto));
|
.Select(ActivityUserDisplayItem.FromDto));
|
||||||
SelectedPerformer = null;
|
|
||||||
StatusMessage = $"{activity.Name} · {Performers.Count} utilisateur(s)";
|
StatusMessage = $"{activity.Name} · {Performers.Count} utilisateur(s)";
|
||||||
}
|
}
|
||||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
||||||
{
|
{
|
||||||
Performers = new ObservableCollection<ActivityUserDisplayItem>();
|
Performers = new ObservableCollection<ActivityUserDisplayItem>();
|
||||||
SelectedPerformer = null;
|
|
||||||
StatusMessage = "Accès refusé pour les activités (scope 'api'). Déconnectez puis reconnectez-vous.";
|
StatusMessage = "Accès refusé pour les activités (scope 'api'). Déconnectez puis reconnectez-vous.";
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Performers = new ObservableCollection<ActivityUserDisplayItem>();
|
Performers = new ObservableCollection<ActivityUserDisplayItem>();
|
||||||
SelectedPerformer = null;
|
|
||||||
StatusMessage = $"Erreur: {ex.Message}";
|
StatusMessage = $"Erreur: {ex.Message}";
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
IsBusy = false;
|
IsBusy = false;
|
||||||
OpenCommandFormsCommand.NotifyCanExecuteChanged();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CanOpenCommandForms()
|
|
||||||
=> SelectedPerformer is not null && CurrentActivity?.Forms?.Count > 0;
|
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanOpenCommandForms))]
|
|
||||||
private async Task OpenCommandFormsAsync()
|
|
||||||
{
|
|
||||||
if (SelectedPerformer is null || CurrentActivity is null)
|
|
||||||
{
|
|
||||||
StatusMessage = "Sélectionnez un utilisateur et une activité avec formulaire.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var app = (App?)Application.Current;
|
|
||||||
if (app is null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Application PostIt indisponible.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var vm = new CommandFormsPageViewModel(CurrentActivity, SelectedPerformer, _billingClient);
|
|
||||||
await app.PushPageAsync(vm);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,399 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using Yavsc;
|
|
||||||
using Yavsc.Abstract.Workflow;
|
|
||||||
using Yavsc.Api.Client;
|
|
||||||
using Yavsc.Models.Billing;
|
|
||||||
using Yavsc.Models.Haircut;
|
|
||||||
using Yavsc.Models.Relationship;
|
|
||||||
|
|
||||||
namespace PostIt.ViewModels;
|
|
||||||
|
|
||||||
public partial class BillingCommandPageViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly BillingApiClient _billingClient;
|
|
||||||
|
|
||||||
public ActivityBrowseItemDto Activity { get; }
|
|
||||||
public ActivityUserDisplayItem Performer { get; }
|
|
||||||
public CommandFormSummaryDto Form { get; }
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
public partial bool IsBusy { get; set; }
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
public partial string StatusMessage { get; set; }
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
public partial string EventDateText { get; set; }
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
public partial string Reason { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
public partial string Address { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
public partial string LatitudeText { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
public partial string LongitudeText { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
public partial bool Consent { get; set; } = true;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
public partial ObservableCollection<HairPrestationDto> AvailablePrestations { get; set; } = new();
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
public partial HairPrestationDto? SelectedPrestation { get; set; }
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
public partial ObservableCollection<SelectableHairPrestationItem> MultiPrestations { get; set; } = new();
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
public partial string AdditionalInfo { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
public partial long? ExistingQueryId { get; set; }
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
public partial QueryStatus CommandStatus { get; set; } = QueryStatus.Inserted;
|
|
||||||
|
|
||||||
public string Title => Form.Title;
|
|
||||||
public string PerformerLabel => Performer.UserName;
|
|
||||||
public string ActivityLabel => Activity.Name;
|
|
||||||
public bool IsSupported => IsRdv || IsBrush || IsMultiBrush;
|
|
||||||
public bool IsRdv => string.Equals(Form.ActionName, BillingCodes.Rdv, StringComparison.Ordinal);
|
|
||||||
public bool IsBrush => string.Equals(Form.ActionName, BillingCodes.Brush, StringComparison.Ordinal);
|
|
||||||
public bool IsMultiBrush => string.Equals(Form.ActionName, BillingCodes.MBrush, StringComparison.Ordinal);
|
|
||||||
public bool ShowsReason => IsRdv;
|
|
||||||
public bool ShowsAdditionalInfo => IsBrush;
|
|
||||||
public bool ShowsSinglePrestation => IsBrush;
|
|
||||||
public bool ShowsMultiplePrestations => IsMultiBrush;
|
|
||||||
public string BillingRoute => $"/billing/{Form.ActionName}";
|
|
||||||
public bool IsEditingExisting => ExistingQueryId.HasValue;
|
|
||||||
public string SubmitLabel => IsEditingExisting ? "Mettre à jour la commande" : "Poster la commande";
|
|
||||||
public string SupportMessage => IsSupported
|
|
||||||
? IsRdv
|
|
||||||
? "Complétez les informations du rendez-vous puis postez la commande."
|
|
||||||
: IsBrush
|
|
||||||
? "Choisissez une prestation coiffure puis postez la commande."
|
|
||||||
: "Choisissez une ou plusieurs prestations coiffure puis postez la commande."
|
|
||||||
: $"Le formulaire {Form.ActionName} n'est pas encore pris en charge dans PostIt.";
|
|
||||||
|
|
||||||
public override bool CanNavigateNext
|
|
||||||
{
|
|
||||||
get => false;
|
|
||||||
protected set { _ = value; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool CanNavigatePrevious
|
|
||||||
{
|
|
||||||
get => true;
|
|
||||||
protected set { _ = value; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public BillingCommandPageViewModel(
|
|
||||||
ActivityBrowseItemDto activity,
|
|
||||||
ActivityUserDisplayItem performer,
|
|
||||||
CommandFormSummaryDto form,
|
|
||||||
BillingApiClient billingClient)
|
|
||||||
{
|
|
||||||
Activity = activity ?? throw new ArgumentNullException(nameof(activity));
|
|
||||||
Performer = performer ?? throw new ArgumentNullException(nameof(performer));
|
|
||||||
Form = form ?? throw new ArgumentNullException(nameof(form));
|
|
||||||
_billingClient = billingClient ?? throw new ArgumentNullException(nameof(billingClient));
|
|
||||||
|
|
||||||
EventDateText = DateTime.Now.AddDays(1).ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture);
|
|
||||||
StatusMessage = SupportMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
partial void OnExistingQueryIdChanged(long? value)
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(IsEditingExisting));
|
|
||||||
OnPropertyChanged(nameof(SubmitLabel));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task InitializeAsync(BillingQueryDetailsDto? existingQuery = null)
|
|
||||||
{
|
|
||||||
if (!IsBrush && !IsMultiBrush)
|
|
||||||
{
|
|
||||||
if (existingQuery is not null)
|
|
||||||
{
|
|
||||||
ApplyExistingQuery(existingQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
IsBusy = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var prestations = await _billingClient.GetHairPrestationsAsync(Form.ActionName).ConfigureAwait(true);
|
|
||||||
AvailablePrestations = new ObservableCollection<HairPrestationDto>(prestations ?? new List<HairPrestationDto>());
|
|
||||||
SelectedPrestation = AvailablePrestations.FirstOrDefault();
|
|
||||||
MultiPrestations = new ObservableCollection<SelectableHairPrestationItem>(AvailablePrestations.Select(SelectableHairPrestationItem.FromDto));
|
|
||||||
|
|
||||||
StatusMessage = AvailablePrestations.Count == 0
|
|
||||||
? "Aucune prestation coiffure disponible."
|
|
||||||
: SupportMessage;
|
|
||||||
}
|
|
||||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
|
||||||
{
|
|
||||||
StatusMessage = "Accès refusé au catalogue de prestations (scope 'api'). Déconnectez puis reconnectez-vous.";
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
StatusMessage = $"Erreur lors du chargement des prestations: {ex.Message}";
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
IsBusy = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingQuery is not null)
|
|
||||||
{
|
|
||||||
ApplyExistingQuery(existingQuery);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task SubmitAsync()
|
|
||||||
{
|
|
||||||
if (!IsSupported)
|
|
||||||
{
|
|
||||||
StatusMessage = SupportMessage;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Consent)
|
|
||||||
{
|
|
||||||
StatusMessage = "Le consentement est requis pour poster la commande.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryParseEventDate(out var eventDate))
|
|
||||||
{
|
|
||||||
StatusMessage = "La date doit être saisie au format yyyy-MM-dd HH:mm.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsRdv && string.IsNullOrWhiteSpace(Reason))
|
|
||||||
{
|
|
||||||
StatusMessage = "Le motif du rendez-vous est requis.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(Address))
|
|
||||||
{
|
|
||||||
StatusMessage = "L'adresse du rendez-vous est requise.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryParseCoordinate(LatitudeText, out var latitude))
|
|
||||||
{
|
|
||||||
StatusMessage = "Latitude invalide.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryParseCoordinate(LongitudeText, out var longitude))
|
|
||||||
{
|
|
||||||
StatusMessage = "Longitude invalide.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
IsBusy = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var location = new Location
|
|
||||||
{
|
|
||||||
Address = Address.Trim(),
|
|
||||||
Latitude = latitude,
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
if (IsEditingExisting)
|
|
||||||
{
|
|
||||||
await _billingClient.UpdateAsync(Form.ActionName, ExistingQueryId!.Value, payload).ConfigureAwait(true);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _billingClient.CreateAsync(Form.ActionName, new
|
|
||||||
{
|
|
||||||
ActivityCode = Activity.Code,
|
|
||||||
PerformerId = Performer.PerformerId,
|
|
||||||
Consent,
|
|
||||||
EventDate = eventDate,
|
|
||||||
Location = location,
|
|
||||||
Reason = payload.Reason,
|
|
||||||
Status = payload.Status,
|
|
||||||
}).ConfigureAwait(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (IsBrush)
|
|
||||||
{
|
|
||||||
if (SelectedPrestation is null)
|
|
||||||
{
|
|
||||||
StatusMessage = "Sélectionnez une prestation coiffure.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
payload.PrestationId = SelectedPrestation.Id;
|
|
||||||
|
|
||||||
if (IsEditingExisting)
|
|
||||||
{
|
|
||||||
await _billingClient.UpdateAsync(Form.ActionName, ExistingQueryId!.Value, payload).ConfigureAwait(true);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _billingClient.CreateAsync(Form.ActionName, new
|
|
||||||
{
|
|
||||||
ActivityCode = Activity.Code,
|
|
||||||
PerformerId = Performer.PerformerId,
|
|
||||||
Consent,
|
|
||||||
EventDate = (DateTime?)eventDate,
|
|
||||||
Location = location,
|
|
||||||
PrestationId = SelectedPrestation.Id,
|
|
||||||
AdditionalInfo = string.IsNullOrWhiteSpace(AdditionalInfo) ? null : AdditionalInfo.Trim(),
|
|
||||||
Status = payload.Status,
|
|
||||||
}).ConfigureAwait(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (IsMultiBrush)
|
|
||||||
{
|
|
||||||
var selectedPrestations = MultiPrestations.Where(x => x.IsSelected).ToList();
|
|
||||||
if (selectedPrestations.Count == 0)
|
|
||||||
{
|
|
||||||
StatusMessage = "Sélectionnez au moins une prestation coiffure.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
payload.PrestationIds = selectedPrestations.Select(x => x.Id).ToList();
|
|
||||||
|
|
||||||
if (IsEditingExisting)
|
|
||||||
{
|
|
||||||
await _billingClient.UpdateAsync(Form.ActionName, ExistingQueryId!.Value, payload).ConfigureAwait(true);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _billingClient.CreateAsync(Form.ActionName, new
|
|
||||||
{
|
|
||||||
ActivityCode = Activity.Code,
|
|
||||||
PerformerId = Performer.PerformerId,
|
|
||||||
Consent,
|
|
||||||
EventDate = eventDate,
|
|
||||||
Location = location,
|
|
||||||
Prestations = selectedPrestations.Select(x => new { PrestationId = x.Id }).ToList(),
|
|
||||||
Status = payload.Status,
|
|
||||||
}).ConfigureAwait(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StatusMessage = IsEditingExisting
|
|
||||||
? $"Commande #{ExistingQueryId} mise à jour sur {BillingRoute} pour {Performer.UserName}."
|
|
||||||
: $"Commande transmise sur {BillingRoute} pour {Performer.UserName}.";
|
|
||||||
}
|
|
||||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
|
||||||
{
|
|
||||||
StatusMessage = "Accès refusé au billing (scope 'api'). Déconnectez puis reconnectez-vous.";
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
StatusMessage = $"Erreur lors de l'envoi: {ex.Message}";
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
IsBusy = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
EventDateText,
|
|
||||||
CultureInfo.CurrentCulture,
|
|
||||||
DateTimeStyles.AssumeLocal,
|
|
||||||
out eventDate)
|
|
||||||
|| DateTime.TryParse(
|
|
||||||
EventDateText,
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
DateTimeStyles.AssumeLocal,
|
|
||||||
out eventDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryParseCoordinate(string text, out double value)
|
|
||||||
{
|
|
||||||
return double.TryParse(text, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.CurrentCulture, out value)
|
|
||||||
|| double.TryParse(text, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,173 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
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;
|
|
||||||
|
|
||||||
namespace PostIt.ViewModels;
|
|
||||||
|
|
||||||
public partial class BillingQueriesPageViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly BillingApiClient _billingClient;
|
|
||||||
|
|
||||||
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 => IsReadOnly
|
|
||||||
? $"Demandes en cours ({Form.Title})"
|
|
||||||
: $"Commandes {Form.Title}";
|
|
||||||
public string ContextLabel => $"{Performer.UserName} · {Activity.Name}";
|
|
||||||
public bool CanOpenDetails => !IsReadOnly;
|
|
||||||
|
|
||||||
public override bool CanNavigateNext
|
|
||||||
{
|
|
||||||
get => false;
|
|
||||||
protected set { _ = value; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool CanNavigatePrevious
|
|
||||||
{
|
|
||||||
get => true;
|
|
||||||
protected set { _ = value; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public BillingQueriesPageViewModel(
|
|
||||||
ActivityBrowseItemDto activity,
|
|
||||||
ActivityUserDisplayItem performer,
|
|
||||||
CommandFormSummaryDto form,
|
|
||||||
BillingApiClient billingClient,
|
|
||||||
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()
|
|
||||||
{
|
|
||||||
IsBusy = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var list = await _billingClient.GetQuerySummariesAsync(Form.ActionName).ConfigureAwait(true);
|
|
||||||
var filtered = (list ?? new())
|
|
||||||
.Where(q => q.ActivityCode == Activity.Code && q.PerformerId == Performer.PerformerId)
|
|
||||||
.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 = BuildLoadedStatusMessage(filtered.Count);
|
|
||||||
}
|
|
||||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
|
||||||
{
|
|
||||||
Queries = new ObservableCollection<BillingQueryDisplayItem>();
|
|
||||||
StatusMessage = "Accès refusé au billing (scope 'api'). Déconnectez puis reconnectez-vous.";
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Queries = new ObservableCollection<BillingQueryDisplayItem>();
|
|
||||||
StatusMessage = $"Erreur: {ex.Message}";
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
using System;
|
|
||||||
using Yavsc.Api.Client;
|
|
||||||
|
|
||||||
namespace PostIt.ViewModels;
|
|
||||||
|
|
||||||
public sealed class BillingQueryDisplayItem
|
|
||||||
{
|
|
||||||
public long Id { get; init; }
|
|
||||||
public string Description { get; init; } = string.Empty;
|
|
||||||
public string Summary { get; init; } = string.Empty;
|
|
||||||
public string StatusLabel { get; init; } = string.Empty;
|
|
||||||
public string EventDateLabel { get; init; } = string.Empty;
|
|
||||||
public string BillingCode { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
public static BillingQueryDisplayItem FromDto(BillingQuerySummaryDto dto)
|
|
||||||
{
|
|
||||||
var summary = !string.IsNullOrWhiteSpace(dto.Reason)
|
|
||||||
? dto.Reason
|
|
||||||
: !string.IsNullOrWhiteSpace(dto.AdditionalInfo)
|
|
||||||
? dto.AdditionalInfo
|
|
||||||
: dto.Description;
|
|
||||||
|
|
||||||
return new BillingQueryDisplayItem
|
|
||||||
{
|
|
||||||
Id = dto.Id,
|
|
||||||
Description = string.IsNullOrWhiteSpace(dto.Description)
|
|
||||||
? $"Commande #{dto.Id}"
|
|
||||||
: dto.Description,
|
|
||||||
Summary = summary,
|
|
||||||
StatusLabel = dto.Status.ToString(),
|
|
||||||
EventDateLabel = dto.EventDate?.ToLocalTime().ToString("g") ?? "Date non précisée",
|
|
||||||
BillingCode = dto.BillingCode,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Avalonia;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using PostIt.Helpers;
|
|
||||||
using Yavsc.Abstract.Workflow;
|
|
||||||
using Yavsc.Api.Client;
|
|
||||||
|
|
||||||
namespace PostIt.ViewModels;
|
|
||||||
|
|
||||||
public partial class CommandFormsPageViewModel : ViewModelBase
|
|
||||||
{
|
|
||||||
private readonly BillingApiClient _billingClient;
|
|
||||||
|
|
||||||
public ActivityBrowseItemDto Activity { get; }
|
|
||||||
public ActivityUserDisplayItem Performer { get; }
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
public partial ObservableCollection<CommandFormSummaryDto> Forms { get; set; }
|
|
||||||
|
|
||||||
[ObservableProperty, NotifyCanExecuteChangedFor(nameof(OpenSelectedFormCommand)), NotifyCanExecuteChangedFor(nameof(OpenQueriesCommand)), NotifyCanExecuteChangedFor(nameof(OpenOngoingQueriesCommand))]
|
|
||||||
public partial CommandFormSummaryDto? SelectedForm { get; set; }
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
public partial string StatusMessage { get; set; }
|
|
||||||
|
|
||||||
public string Title => $"Formulaires pour {Performer.UserName}";
|
|
||||||
public string ContextLabel => $"{Activity.Name} · {Forms.Count} formulaire(s)";
|
|
||||||
|
|
||||||
public override bool CanNavigateNext
|
|
||||||
{
|
|
||||||
get => false;
|
|
||||||
protected set { _ = value; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool CanNavigatePrevious
|
|
||||||
{
|
|
||||||
get => true;
|
|
||||||
protected set { _ = value; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public CommandFormsPageViewModel(
|
|
||||||
ActivityBrowseItemDto activity,
|
|
||||||
ActivityUserDisplayItem performer,
|
|
||||||
BillingApiClient billingClient)
|
|
||||||
{
|
|
||||||
Activity = activity ?? throw new ArgumentNullException(nameof(activity));
|
|
||||||
Performer = performer ?? throw new ArgumentNullException(nameof(performer));
|
|
||||||
_billingClient = billingClient ?? throw new ArgumentNullException(nameof(billingClient));
|
|
||||||
|
|
||||||
Forms = new ObservableCollection<CommandFormSummaryDto>((activity.Forms ?? new())
|
|
||||||
.OrderBy(f => f.Title)
|
|
||||||
.ThenBy(f => f.ActionName));
|
|
||||||
SelectedForm = Forms.FirstOrDefault();
|
|
||||||
StatusMessage = Forms.Count == 0
|
|
||||||
? "Aucun formulaire n'est disponible pour cette activité."
|
|
||||||
: "Choisissez le formulaire à utiliser.";
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CanOpenSelectedForm() => SelectedForm is not null;
|
|
||||||
|
|
||||||
private bool CanOpenQueries() => SelectedForm is not null;
|
|
||||||
|
|
||||||
private bool CanOpenOngoingQueries() => SelectedForm is not null;
|
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanOpenSelectedForm))]
|
|
||||||
private async Task OpenSelectedFormAsync()
|
|
||||||
{
|
|
||||||
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 BillingCommandPageViewModel(Activity, Performer, SelectedForm, _billingClient);
|
|
||||||
await vm.InitializeAsync();
|
|
||||||
await app.PushPageAsync(vm);
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanOpenQueries))]
|
|
||||||
private async Task OpenQueriesAsync()
|
|
||||||
{
|
|
||||||
if (SelectedForm is null)
|
|
||||||
{
|
|
||||||
StatusMessage = "Sélectionnez un formulaire.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var app = (App?)Application.Current;
|
|
||||||
if (app is null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Application PostIt indisponible.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var vm = new BillingQueriesPageViewModel(Activity, Performer, SelectedForm, _billingClient);
|
|
||||||
await vm.InitializeAsync();
|
|
||||||
await app.PushPageAsync(vm);
|
|
||||||
}
|
|
||||||
|
|
||||||
[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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using Yavsc.Models.Haircut;
|
|
||||||
|
|
||||||
namespace PostIt.ViewModels;
|
|
||||||
|
|
||||||
public partial class SelectableHairPrestationItem : ObservableObject
|
|
||||||
{
|
|
||||||
public long Id { get; init; }
|
|
||||||
public string Title { get; init; } = string.Empty;
|
|
||||||
public string Details { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
public partial bool IsSelected { get; set; }
|
|
||||||
|
|
||||||
public static SelectableHairPrestationItem FromDto(HairPrestationDto dto)
|
|
||||||
=> new()
|
|
||||||
{
|
|
||||||
Id = dto.Id,
|
|
||||||
Title = dto.Title,
|
|
||||||
Details = dto.Details,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -65,14 +65,12 @@
|
||||||
</ListBox>
|
</ListBox>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid Grid.Column="4" RowDefinitions="Auto,Auto,*,Auto">
|
<Grid Grid.Column="4" RowDefinitions="Auto,Auto,*">
|
||||||
<TextBlock Grid.Row="0" Text="Utilisateurs" FontWeight="Bold" Margin="0,0,0,8" />
|
<TextBlock Grid.Row="0" Text="Utilisateurs" FontWeight="Bold" Margin="0,0,0,8" />
|
||||||
<TextBlock Grid.Row="1"
|
<TextBlock Grid.Row="1"
|
||||||
Text="{Binding CurrentActivityLabel, StringFormat='Activité affichée : {0}'}"
|
Text="{Binding CurrentActivityLabel, StringFormat='Activité affichée : {0}'}"
|
||||||
FontSize="11" Opacity="0.7" Margin="0,0,0,8" />
|
FontSize="11" Opacity="0.7" Margin="0,0,0,8" />
|
||||||
<ListBox Grid.Row="2"
|
<ListBox Grid.Row="2" ItemsSource="{Binding Performers}">
|
||||||
ItemsSource="{Binding Performers}"
|
|
||||||
SelectedItem="{Binding SelectedPerformer, Mode=TwoWay}">
|
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:ActivityUserDisplayItem">
|
<DataTemplate x:DataType="vm:ActivityUserDisplayItem">
|
||||||
<StackPanel Spacing="2" Margin="0,0,0,8">
|
<StackPanel Spacing="2" Margin="0,0,0,8">
|
||||||
|
|
@ -106,10 +104,6 @@
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
<Button Grid.Row="3"
|
|
||||||
Margin="0,8,0,0"
|
|
||||||
Content="Voir les formulaires"
|
|
||||||
Command="{Binding OpenCommandFormsCommand}" />
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,131 +0,0 @@
|
||||||
<ContentPage xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:vm="using:PostIt.ViewModels"
|
|
||||||
x:Class="PostIt.Views.BillingCommandPage"
|
|
||||||
x:DataType="vm:BillingCommandPageViewModel"
|
|
||||||
Header="Commande billing">
|
|
||||||
<ScrollViewer>
|
|
||||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="Auto,*" Margin="12">
|
|
||||||
<TextBlock Grid.Row="0" Grid.ColumnSpan="2"
|
|
||||||
Text="{Binding Title}"
|
|
||||||
FontSize="18"
|
|
||||||
FontWeight="Bold" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="1" Grid.ColumnSpan="2"
|
|
||||||
Margin="0,4,0,12"
|
|
||||||
Text="{Binding SupportMessage}"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
Opacity="0.8" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="2" Text="Utilisateur" VerticalAlignment="Center" Margin="0,0,12,8" />
|
|
||||||
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding PerformerLabel}" IsReadOnly="True" Margin="0,0,0,8" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="3" Text="Activité" VerticalAlignment="Center" Margin="0,0,12,8" />
|
|
||||||
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding ActivityLabel}" IsReadOnly="True" Margin="0,0,0,8" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="4" Text="Date" VerticalAlignment="Center" Margin="0,0,12,8" />
|
|
||||||
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding EventDateText, Mode=TwoWay}" Margin="0,0,0,8" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="5"
|
|
||||||
Text="Motif"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Margin="0,0,12,8"
|
|
||||||
IsVisible="{Binding ShowsReason}" />
|
|
||||||
<TextBox Grid.Row="5"
|
|
||||||
Grid.Column="1"
|
|
||||||
Text="{Binding Reason, Mode=TwoWay}"
|
|
||||||
Margin="0,0,0,8"
|
|
||||||
IsVisible="{Binding ShowsReason}" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="6" Text="Adresse" VerticalAlignment="Center" Margin="0,0,12,8" />
|
|
||||||
<TextBox Grid.Row="6" Grid.Column="1" Text="{Binding Address, Mode=TwoWay}" Margin="0,0,0,8" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="7" Text="Latitude" VerticalAlignment="Center" Margin="0,0,12,8" />
|
|
||||||
<TextBox Grid.Row="7" Grid.Column="1" Text="{Binding LatitudeText, Mode=TwoWay}" Margin="0,0,0,8" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="8" Text="Longitude" VerticalAlignment="Center" Margin="0,0,12,8" />
|
|
||||||
<TextBox Grid.Row="8" Grid.Column="1" Text="{Binding LongitudeText, Mode=TwoWay}" Margin="0,0,0,8" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="9"
|
|
||||||
Text="Prestation"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Margin="0,0,12,8"
|
|
||||||
IsVisible="{Binding ShowsSinglePrestation}" />
|
|
||||||
<ComboBox Grid.Row="9"
|
|
||||||
Grid.Column="1"
|
|
||||||
ItemsSource="{Binding AvailablePrestations}"
|
|
||||||
SelectedItem="{Binding SelectedPrestation, Mode=TwoWay}"
|
|
||||||
Margin="0,0,0,8"
|
|
||||||
IsVisible="{Binding ShowsSinglePrestation}">
|
|
||||||
<ComboBox.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<StackPanel Spacing="2">
|
|
||||||
<TextBlock Text="{Binding Title}" FontWeight="Bold" />
|
|
||||||
<TextBlock Text="{Binding Details}" FontSize="11" Opacity="0.7" TextWrapping="Wrap" />
|
|
||||||
</StackPanel>
|
|
||||||
</DataTemplate>
|
|
||||||
</ComboBox.ItemTemplate>
|
|
||||||
</ComboBox>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="10"
|
|
||||||
Text="Prestations"
|
|
||||||
VerticalAlignment="Top"
|
|
||||||
Margin="0,0,12,8"
|
|
||||||
IsVisible="{Binding ShowsMultiplePrestations}" />
|
|
||||||
<ListBox Grid.Row="10"
|
|
||||||
Grid.Column="1"
|
|
||||||
ItemsSource="{Binding MultiPrestations}"
|
|
||||||
IsVisible="{Binding ShowsMultiplePrestations}"
|
|
||||||
MaxHeight="180"
|
|
||||||
Margin="0,0,0,8">
|
|
||||||
<ListBox.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}" Margin="0,0,0,8">
|
|
||||||
<StackPanel Spacing="2">
|
|
||||||
<TextBlock Text="{Binding Title}" FontWeight="Bold" />
|
|
||||||
<TextBlock Text="{Binding Details}" FontSize="11" Opacity="0.7" TextWrapping="Wrap" />
|
|
||||||
</StackPanel>
|
|
||||||
</CheckBox>
|
|
||||||
</DataTemplate>
|
|
||||||
</ListBox.ItemTemplate>
|
|
||||||
</ListBox>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="11"
|
|
||||||
Text="Informations"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Margin="0,0,12,8"
|
|
||||||
IsVisible="{Binding ShowsAdditionalInfo}" />
|
|
||||||
<TextBox Grid.Row="11"
|
|
||||||
Grid.Column="1"
|
|
||||||
Text="{Binding AdditionalInfo, Mode=TwoWay}"
|
|
||||||
Margin="0,0,0,8"
|
|
||||||
IsVisible="{Binding ShowsAdditionalInfo}" />
|
|
||||||
|
|
||||||
<CheckBox Grid.Row="12" Grid.ColumnSpan="2"
|
|
||||||
Content="Je consens à la création de cette commande"
|
|
||||||
IsChecked="{Binding Consent, Mode=TwoWay}"
|
|
||||||
Margin="0,4,0,12" />
|
|
||||||
|
|
||||||
<Grid Grid.Row="13" Grid.ColumnSpan="2" ColumnDefinitions="Auto,12,*,Auto">
|
|
||||||
<Button Grid.Column="0"
|
|
||||||
Content="{Binding SubmitLabel}"
|
|
||||||
Command="{Binding SubmitCommand}"
|
|
||||||
IsEnabled="{Binding IsSupported}" />
|
|
||||||
<TextBox Grid.Column="2"
|
|
||||||
Text="{Binding StatusMessage}"
|
|
||||||
IsReadOnly="True"
|
|
||||||
AcceptsReturn="True"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
BorderThickness="0"
|
|
||||||
BorderBrush="Transparent"
|
|
||||||
Padding="0"
|
|
||||||
Background="Transparent"
|
|
||||||
VerticalAlignment="Center" />
|
|
||||||
<ProgressBar Grid.Column="3"
|
|
||||||
Width="120"
|
|
||||||
IsIndeterminate="True"
|
|
||||||
IsVisible="{Binding IsBusy}" />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</ScrollViewer>
|
|
||||||
</ContentPage>
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
|
|
||||||
namespace PostIt.Views;
|
|
||||||
|
|
||||||
public partial class BillingCommandPage : ContentPage
|
|
||||||
{
|
|
||||||
public BillingCommandPage()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeComponent()
|
|
||||||
{
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
<ContentPage xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:vm="using:PostIt.ViewModels"
|
|
||||||
x:Class="PostIt.Views.BillingQueriesPage"
|
|
||||||
x:DataType="vm:BillingQueriesPageViewModel"
|
|
||||||
Header="Commandes billing">
|
|
||||||
<Grid RowDefinitions="Auto,Auto,*,Auto" Margin="12">
|
|
||||||
<StackPanel Grid.Row="0" Spacing="2">
|
|
||||||
<TextBlock Text="{Binding Title}" FontSize="18" FontWeight="Bold" />
|
|
||||||
<TextBlock Text="{Binding ContextLabel}" Opacity="0.75" />
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<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}" 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">
|
|
||||||
<StackPanel Spacing="3">
|
|
||||||
<TextBlock Text="{Binding Description}" FontWeight="Bold" />
|
|
||||||
<TextBlock Text="{Binding Summary}" TextWrapping="Wrap" FontSize="12" Opacity="0.8" />
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
|
||||||
<TextBlock Text="{Binding EventDateLabel}" FontSize="11" Opacity="0.7" />
|
|
||||||
<TextBlock Text="{Binding StatusLabel}" FontSize="11" Opacity="0.7" />
|
|
||||||
<TextBlock Text="{Binding BillingCode}" FontSize="11" Opacity="0.6" />
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</DataTemplate>
|
|
||||||
</ListBox.ItemTemplate>
|
|
||||||
</ListBox>
|
|
||||||
|
|
||||||
<Grid Grid.Row="3" ColumnDefinitions="*,Auto" Margin="0,12,0,0">
|
|
||||||
<TextBox Grid.Column="0"
|
|
||||||
Text="{Binding StatusMessage}"
|
|
||||||
IsReadOnly="True"
|
|
||||||
AcceptsReturn="True"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
BorderThickness="0"
|
|
||||||
BorderBrush="Transparent"
|
|
||||||
Padding="0"
|
|
||||||
Background="Transparent"
|
|
||||||
VerticalAlignment="Center" />
|
|
||||||
<ProgressBar Grid.Column="1" Width="120" IsIndeterminate="True" IsVisible="{Binding IsBusy}" />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</ContentPage>
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
|
|
||||||
namespace PostIt.Views;
|
|
||||||
|
|
||||||
public partial class BillingQueriesPage : ContentPage
|
|
||||||
{
|
|
||||||
public BillingQueriesPage()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeComponent()
|
|
||||||
{
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
<ContentPage xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:vm="using:PostIt.ViewModels"
|
|
||||||
xmlns:wf="using:Yavsc.Abstract.Workflow"
|
|
||||||
x:Class="PostIt.Views.CommandFormsPage"
|
|
||||||
x:DataType="vm:CommandFormsPageViewModel"
|
|
||||||
Header="Formulaires">
|
|
||||||
<Grid RowDefinitions="Auto,Auto,*,Auto" Margin="12">
|
|
||||||
<TextBlock Grid.Row="0"
|
|
||||||
Text="{Binding Title}"
|
|
||||||
FontSize="18"
|
|
||||||
FontWeight="Bold" />
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="1"
|
|
||||||
Margin="0,4,0,12"
|
|
||||||
Text="{Binding ContextLabel}"
|
|
||||||
Opacity="0.75" />
|
|
||||||
|
|
||||||
<ListBox Grid.Row="2"
|
|
||||||
ItemsSource="{Binding Forms}"
|
|
||||||
SelectedItem="{Binding SelectedForm, Mode=TwoWay}">
|
|
||||||
<ListBox.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="wf:CommandFormSummaryDto">
|
|
||||||
<StackPanel Spacing="2" Margin="0,0,0,10">
|
|
||||||
<TextBlock Text="{Binding Title}" FontWeight="Bold" />
|
|
||||||
<TextBlock Text="{Binding ActionName, StringFormat='Route billing : /billing/{0}'}"
|
|
||||||
FontSize="11"
|
|
||||||
Opacity="0.7" />
|
|
||||||
</StackPanel>
|
|
||||||
</DataTemplate>
|
|
||||||
</ListBox.ItemTemplate>
|
|
||||||
</ListBox>
|
|
||||||
|
|
||||||
<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}" />
|
|
||||||
<Button Grid.Column="4"
|
|
||||||
Content="Demandes en cours (lecture seule)"
|
|
||||||
Command="{Binding OpenOngoingQueriesCommand}" />
|
|
||||||
<TextBox Grid.Column="6"
|
|
||||||
Text="{Binding StatusMessage}"
|
|
||||||
IsReadOnly="True"
|
|
||||||
AcceptsReturn="True"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
BorderThickness="0"
|
|
||||||
BorderBrush="Transparent"
|
|
||||||
Padding="0"
|
|
||||||
Background="Transparent"
|
|
||||||
VerticalAlignment="Center" />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</ContentPage>
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
|
|
||||||
namespace PostIt.Views;
|
|
||||||
|
|
||||||
public partial class CommandFormsPage : ContentPage
|
|
||||||
{
|
|
||||||
public CommandFormsPage()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeComponent()
|
|
||||||
{
|
|
||||||
AvaloniaXamlLoader.Load(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
namespace Yavsc.Models.Haircut;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Lightweight hair-prestation description exposed to API clients.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class HairPrestationDto
|
|
||||||
{
|
|
||||||
public long Id { get; set; }
|
|
||||||
public string Title { get; set; } = string.Empty;
|
|
||||||
public string Details { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
@ -10,9 +10,7 @@ namespace Yavsc.Api.Client;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// HTTP client for browsing business activities and their performers.
|
/// HTTP client for browsing business activities and their performers.
|
||||||
/// Uses absolute URLs so it can coexist with other Yavsc clients that
|
/// Uses absolute URLs so it can coexist with other Yavsc clients that
|
||||||
/// target a different API host on the same shared transport. The same
|
/// target a different API host on the same shared transport.
|
||||||
/// activity payload also carries the eligible billing forms for a
|
|
||||||
/// selected performer/activity pair.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ActivityApiClient
|
public sealed class ActivityApiClient
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,354 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// HTTP client for posting commands to the business billing routes.
|
|
||||||
/// Uses absolute URLs so it can coexist with blog-targeting clients on
|
|
||||||
/// the same shared transport.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class BillingApiClient
|
|
||||||
{
|
|
||||||
private const string PathPrefix = "billing";
|
|
||||||
|
|
||||||
private readonly IYavscApiClient _api;
|
|
||||||
private readonly Uri _baseAddress;
|
|
||||||
|
|
||||||
public BillingApiClient(IYavscApiClient api, string businessBaseAddress)
|
|
||||||
{
|
|
||||||
_api = api ?? throw new ArgumentNullException(nameof(api));
|
|
||||||
if (string.IsNullOrWhiteSpace(businessBaseAddress))
|
|
||||||
throw new ArgumentException("Base address is required.", nameof(businessBaseAddress));
|
|
||||||
|
|
||||||
_baseAddress = new Uri(businessBaseAddress, UriKind.Absolute);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task CreateAsync(string billingCode, object payload, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(billingCode))
|
|
||||||
throw new ArgumentException("Billing code is required.", nameof(billingCode));
|
|
||||||
|
|
||||||
return _api.CallAsync(
|
|
||||||
HttpMethod.Post,
|
|
||||||
Absolute($"{PathPrefix}/{Uri.EscapeDataString(billingCode)}"),
|
|
||||||
body: payload,
|
|
||||||
ct: ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<List<HairPrestationDto>> GetHairPrestationsAsync(string billingCode, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(billingCode))
|
|
||||||
throw new ArgumentException("Billing code is required.", nameof(billingCode));
|
|
||||||
|
|
||||||
return _api.CallAsync<List<HairPrestationDto>>(
|
|
||||||
HttpMethod.Get,
|
|
||||||
Absolute($"{PathPrefix}/{Uri.EscapeDataString(billingCode)}/prestations"),
|
|
||||||
ct: ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<BillingQuerySummaryDto>> GetQuerySummariesAsync(string billingCode, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(billingCode))
|
|
||||||
throw new ArgumentException("Billing code is required.", nameof(billingCode));
|
|
||||||
|
|
||||||
var items = await _api.CallAsync<List<BillingQuerySummaryDto>>(
|
|
||||||
HttpMethod.Get,
|
|
||||||
Absolute($"{PathPrefix}/{Uri.EscapeDataString(billingCode)}"),
|
|
||||||
ct: ct) ?? new List<BillingQuerySummaryDto>();
|
|
||||||
|
|
||||||
foreach (var item in items)
|
|
||||||
{
|
|
||||||
item.BillingCode = billingCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
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; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
using System;
|
|
||||||
using Yavsc;
|
|
||||||
|
|
||||||
namespace Yavsc.Api.Client;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Lightweight billing-query projection consumed by PostIt list views.
|
|
||||||
/// Extra JSON fields from concrete query types are ignored.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class BillingQuerySummaryDto
|
|
||||||
{
|
|
||||||
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 QueryStatus Status { get; set; }
|
|
||||||
public string Description { get; set; } = string.Empty;
|
|
||||||
public DateTime? EventDate { get; set; }
|
|
||||||
public string Reason { get; set; } = string.Empty;
|
|
||||||
public string AdditionalInfo { get; set; } = string.Empty;
|
|
||||||
public decimal? Provisional { get; set; }
|
|
||||||
}
|
|
||||||
|
|
@ -6,7 +6,6 @@ using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using Yavsc.Controllers;
|
using Yavsc.Controllers;
|
||||||
using Yavsc.Models;
|
using Yavsc.Models;
|
||||||
using Yavsc.Models.Haircut;
|
|
||||||
using Yavsc.Models.Relationship;
|
using Yavsc.Models.Relationship;
|
||||||
using Yavsc.Models.Workflow;
|
using Yavsc.Models.Workflow;
|
||||||
using Yavsc.Tests.Shared;
|
using Yavsc.Tests.Shared;
|
||||||
|
|
@ -189,138 +188,6 @@ public sealed class ApiWebServerFixture : WebHostFixture
|
||||||
db.SaveChanges();
|
db.SaveChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ResetAndSeedRdvQueryGraph()
|
|
||||||
{
|
|
||||||
ResetAndSeedActivityGraph();
|
|
||||||
|
|
||||||
using var scope = Services.CreateScope();
|
|
||||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
|
||||||
|
|
||||||
var location = db.Locations.Single(l => l.Address == "1 rue du Test");
|
|
||||||
|
|
||||||
db.RdvQueries.Add(new RdvQuery
|
|
||||||
{
|
|
||||||
ActivityCode = "dev",
|
|
||||||
ClientId = "alice",
|
|
||||||
PerformerId = "alice",
|
|
||||||
Consent = true,
|
|
||||||
EventDate = DateTime.UtcNow.AddDays(1),
|
|
||||||
Location = location,
|
|
||||||
Reason = "Initial rendez-vous",
|
|
||||||
Status = Yavsc.QueryStatus.Inserted,
|
|
||||||
});
|
|
||||||
|
|
||||||
db.SaveChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ResetAndSeedHaircutGraph()
|
|
||||||
{
|
|
||||||
ResetAndSeedActivityGraph();
|
|
||||||
|
|
||||||
using var scope = Services.CreateScope();
|
|
||||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
|
||||||
|
|
||||||
var location = db.Locations.Single(l => l.Address == "1 rue du Test");
|
|
||||||
|
|
||||||
if (!db.Activities.Any(a => a.Code == "brush"))
|
|
||||||
{
|
|
||||||
db.Activities.Add(new Activity
|
|
||||||
{
|
|
||||||
Code = "brush",
|
|
||||||
Name = "Brush",
|
|
||||||
Hidden = false,
|
|
||||||
DateCreated = DateTime.UtcNow,
|
|
||||||
DateModified = DateTime.UtcNow,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!db.Activities.Any(a => a.Code == "mbrush"))
|
|
||||||
{
|
|
||||||
db.Activities.Add(new Activity
|
|
||||||
{
|
|
||||||
Code = "mbrush",
|
|
||||||
Name = "MBrush",
|
|
||||||
Hidden = false,
|
|
||||||
DateCreated = DateTime.UtcNow,
|
|
||||||
DateModified = DateTime.UtcNow,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!db.BrusherProfile.Any(p => p.UserId == "alice"))
|
|
||||||
{
|
|
||||||
db.BrusherProfile.Add(new BrusherProfile
|
|
||||||
{
|
|
||||||
UserId = "alice",
|
|
||||||
ActionDistance = 25,
|
|
||||||
WomenLongCutPrice = 50m,
|
|
||||||
WomenHalfCutPrice = 40m,
|
|
||||||
WomenShortCutPrice = 30m,
|
|
||||||
ManCutPrice = 20m,
|
|
||||||
KidCutPrice = 15m,
|
|
||||||
ShampooPrice = 5m,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var prestation1 = new HairPrestation
|
|
||||||
{
|
|
||||||
Gender = HairCutGenders.Women,
|
|
||||||
Length = HairLength.HalfLong,
|
|
||||||
Cut = true,
|
|
||||||
Shampoo = true,
|
|
||||||
Dressing = HairDressings.Brushing,
|
|
||||||
Tech = HairTechnos.NoTech,
|
|
||||||
Cares = false,
|
|
||||||
Taints = new List<HairTaintInstance>(),
|
|
||||||
};
|
|
||||||
var prestation2 = new HairPrestation
|
|
||||||
{
|
|
||||||
Gender = HairCutGenders.Man,
|
|
||||||
Length = HairLength.Short,
|
|
||||||
Cut = true,
|
|
||||||
Shampoo = false,
|
|
||||||
Dressing = HairDressings.Brushing,
|
|
||||||
Tech = HairTechnos.NoTech,
|
|
||||||
Cares = false,
|
|
||||||
Taints = new List<HairTaintInstance>(),
|
|
||||||
};
|
|
||||||
|
|
||||||
db.HairPrestation.AddRange(prestation1, prestation2);
|
|
||||||
db.SaveChanges();
|
|
||||||
|
|
||||||
db.HairCutQueries.Add(new HairCutQuery
|
|
||||||
{
|
|
||||||
ActivityCode = "brush",
|
|
||||||
ClientId = "alice",
|
|
||||||
PerformerId = "alice",
|
|
||||||
Consent = true,
|
|
||||||
EventDate = DateTime.UtcNow.AddDays(3),
|
|
||||||
Location = location,
|
|
||||||
PrestationId = prestation1.Id,
|
|
||||||
Prestation = prestation1,
|
|
||||||
AdditionalInfo = "Coupe test",
|
|
||||||
Status = Yavsc.QueryStatus.Inserted,
|
|
||||||
Description = "Haircut seed",
|
|
||||||
});
|
|
||||||
|
|
||||||
db.HairMultiCutQueries.Add(new HairMultiCutQuery
|
|
||||||
{
|
|
||||||
ActivityCode = "mbrush",
|
|
||||||
ClientId = "alice",
|
|
||||||
PerformerId = "alice",
|
|
||||||
Consent = true,
|
|
||||||
EventDate = DateTime.UtcNow.AddDays(4),
|
|
||||||
Location = location,
|
|
||||||
Prestations = new List<HairPrestationCollectionItem>
|
|
||||||
{
|
|
||||||
new() { PrestationId = prestation1.Id, Prestation = prestation1 },
|
|
||||||
new() { PrestationId = prestation2.Id, Prestation = prestation2 },
|
|
||||||
},
|
|
||||||
Status = Yavsc.QueryStatus.Inserted,
|
|
||||||
});
|
|
||||||
|
|
||||||
db.SaveChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Dispose()
|
public override void Dispose()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using System.Net.Http.Json;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Yavsc.Api.Test.Fixtures;
|
|
||||||
using Yavsc.Models;
|
|
||||||
using Yavsc.Models.Haircut;
|
|
||||||
using Yavsc.Models.Relationship;
|
|
||||||
using Yavsc.Tests.Shared;
|
|
||||||
|
|
||||||
namespace Yavsc.Api.Test;
|
|
||||||
|
|
||||||
[Collection("Yavsc Api")]
|
|
||||||
public sealed class HairCutQueryApiControllerTests : IClassFixture<ApiWebServerFixture>
|
|
||||||
{
|
|
||||||
private readonly ApiWebServerFixture _fixture;
|
|
||||||
|
|
||||||
public HairCutQueryApiControllerTests(ApiWebServerFixture fixture)
|
|
||||||
{
|
|
||||||
_fixture = fixture;
|
|
||||||
}
|
|
||||||
|
|
||||||
private HttpClient NewClient(string subject = "alice", string scope = "api")
|
|
||||||
{
|
|
||||||
var handler = new HttpClientHandler
|
|
||||||
{
|
|
||||||
ServerCertificateCustomValidationCallback = (_, _, _, _) => true
|
|
||||||
};
|
|
||||||
|
|
||||||
var http = new HttpClient(handler)
|
|
||||||
{
|
|
||||||
BaseAddress = new Uri(_fixture.BaseAddress)
|
|
||||||
};
|
|
||||||
http.DefaultRequestHeaders.Authorization =
|
|
||||||
new AuthenticationHeaderValue("Bearer", TestTokenIssuer.Issue(subject, scope));
|
|
||||||
return http;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Billing_brush_route_supports_crud()
|
|
||||||
{
|
|
||||||
_fixture.ResetAndSeedHaircutGraph();
|
|
||||||
using var http = NewClient();
|
|
||||||
|
|
||||||
var prestationId = await GetPrestationIdAsync();
|
|
||||||
|
|
||||||
var createPayload = new HairCutQuery
|
|
||||||
{
|
|
||||||
ActivityCode = "brush",
|
|
||||||
PerformerId = "alice",
|
|
||||||
Consent = true,
|
|
||||||
EventDate = DateTime.UtcNow.AddDays(5),
|
|
||||||
Location = new Location
|
|
||||||
{
|
|
||||||
Address = "2 rue de la Coupe",
|
|
||||||
Latitude = 48.8570,
|
|
||||||
Longitude = 2.3525,
|
|
||||||
},
|
|
||||||
PrestationId = prestationId,
|
|
||||||
AdditionalInfo = "Brushing test",
|
|
||||||
Status = QueryStatus.Inserted,
|
|
||||||
Description = "Haircut create",
|
|
||||||
};
|
|
||||||
|
|
||||||
var createResponse = await http.PostAsJsonAsync("/api/v1/billing/Brush", createPayload, TestContext.Current.CancellationToken);
|
|
||||||
if (createResponse.StatusCode != HttpStatusCode.Created)
|
|
||||||
{
|
|
||||||
var body = await createResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
|
|
||||||
Assert.Fail($"Unexpected status {createResponse.StatusCode}: {body}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var created = await createResponse.Content.ReadFromJsonAsync<HairCutQuery>(TestContext.Current.CancellationToken);
|
|
||||||
Assert.NotNull(created);
|
|
||||||
Assert.NotEqual(0, created!.Id);
|
|
||||||
Assert.Equal("alice", created.ClientId);
|
|
||||||
|
|
||||||
var getResponse = await http.GetAsync($"/api/v1/billing/Brush/{created.Id}", TestContext.Current.CancellationToken);
|
|
||||||
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
|
|
||||||
|
|
||||||
var fetched = await getResponse.Content.ReadFromJsonAsync<HairCutQuery>(TestContext.Current.CancellationToken);
|
|
||||||
Assert.NotNull(fetched);
|
|
||||||
Assert.Equal(created.Id, fetched!.Id);
|
|
||||||
Assert.Equal("Brushing test", fetched.AdditionalInfo);
|
|
||||||
|
|
||||||
fetched.AdditionalInfo = "Brushing modifié";
|
|
||||||
var putResponse = await http.PutAsJsonAsync($"/api/v1/billing/Brush/{fetched.Id}", fetched, TestContext.Current.CancellationToken);
|
|
||||||
Assert.Equal(HttpStatusCode.NoContent, putResponse.StatusCode);
|
|
||||||
|
|
||||||
var deleteResponse = await http.DeleteAsync($"/api/v1/billing/Brush/{fetched.Id}", TestContext.Current.CancellationToken);
|
|
||||||
Assert.Equal(HttpStatusCode.OK, deleteResponse.StatusCode);
|
|
||||||
|
|
||||||
var missingResponse = await http.GetAsync($"/api/v1/billing/Brush/{fetched.Id}", TestContext.Current.CancellationToken);
|
|
||||||
Assert.Equal(HttpStatusCode.NotFound, missingResponse.StatusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Billing_brush_route_exposes_prestation_catalog()
|
|
||||||
{
|
|
||||||
_fixture.ResetAndSeedHaircutGraph();
|
|
||||||
using var http = NewClient();
|
|
||||||
|
|
||||||
var response = await http.GetAsync("/api/v1/billing/Brush/prestations", TestContext.Current.CancellationToken);
|
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
||||||
|
|
||||||
var catalog = await response.Content.ReadFromJsonAsync<List<HairPrestationDto>>(TestContext.Current.CancellationToken);
|
|
||||||
Assert.NotNull(catalog);
|
|
||||||
Assert.NotEmpty(catalog!);
|
|
||||||
Assert.All(catalog!, item => Assert.False(string.IsNullOrWhiteSpace(item.Title)));
|
|
||||||
Assert.Contains(catalog!, item => item.Title == "Femme · Cheveux mi-longs"
|
|
||||||
&& item.Details == "Coupe · Brushing · Aucune technique spécifique · Shampoing · Sans soins");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<long> GetPrestationIdAsync()
|
|
||||||
{
|
|
||||||
using var scope = _fixture.Services.CreateScope();
|
|
||||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
|
||||||
return await db.HairPrestation.Select(p => p.Id).FirstAsync(TestContext.Current.CancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using System.Net.Http.Json;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Yavsc.Api.Test.Fixtures;
|
|
||||||
using Yavsc.Models;
|
|
||||||
using Yavsc.Models.Haircut;
|
|
||||||
using Yavsc.Models.Relationship;
|
|
||||||
using Yavsc.Tests.Shared;
|
|
||||||
|
|
||||||
namespace Yavsc.Api.Test;
|
|
||||||
|
|
||||||
[Collection("Yavsc Api")]
|
|
||||||
public sealed class HairMultiCutQueryApiControllerTests : IClassFixture<ApiWebServerFixture>
|
|
||||||
{
|
|
||||||
private readonly ApiWebServerFixture _fixture;
|
|
||||||
|
|
||||||
public HairMultiCutQueryApiControllerTests(ApiWebServerFixture fixture)
|
|
||||||
{
|
|
||||||
_fixture = fixture;
|
|
||||||
}
|
|
||||||
|
|
||||||
private HttpClient NewClient(string subject = "alice", string scope = "api")
|
|
||||||
{
|
|
||||||
var handler = new HttpClientHandler
|
|
||||||
{
|
|
||||||
ServerCertificateCustomValidationCallback = (_, _, _, _) => true
|
|
||||||
};
|
|
||||||
|
|
||||||
var http = new HttpClient(handler)
|
|
||||||
{
|
|
||||||
BaseAddress = new Uri(_fixture.BaseAddress)
|
|
||||||
};
|
|
||||||
http.DefaultRequestHeaders.Authorization =
|
|
||||||
new AuthenticationHeaderValue("Bearer", TestTokenIssuer.Issue(subject, scope));
|
|
||||||
return http;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Billing_mbrush_route_supports_crud()
|
|
||||||
{
|
|
||||||
_fixture.ResetAndSeedHaircutGraph();
|
|
||||||
using var http = NewClient();
|
|
||||||
|
|
||||||
var prestationIds = await GetPrestationIdsAsync();
|
|
||||||
|
|
||||||
var createPayload = new HairMultiCutQuery
|
|
||||||
{
|
|
||||||
ActivityCode = "mbrush",
|
|
||||||
PerformerId = "alice",
|
|
||||||
Consent = true,
|
|
||||||
EventDate = DateTime.UtcNow.AddDays(6),
|
|
||||||
Location = new Location
|
|
||||||
{
|
|
||||||
Address = "3 rue du Groupe",
|
|
||||||
Latitude = 48.8580,
|
|
||||||
Longitude = 2.3530,
|
|
||||||
},
|
|
||||||
Prestations = prestationIds.Select(id => new HairPrestationCollectionItem { PrestationId = id }).ToList(),
|
|
||||||
Status = QueryStatus.Inserted,
|
|
||||||
};
|
|
||||||
|
|
||||||
var createResponse = await http.PostAsJsonAsync("/api/v1/billing/MBrush", createPayload, TestContext.Current.CancellationToken);
|
|
||||||
if (createResponse.StatusCode != HttpStatusCode.Created)
|
|
||||||
{
|
|
||||||
var body = await createResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
|
|
||||||
Assert.Fail($"Unexpected status {createResponse.StatusCode}: {body}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var created = await createResponse.Content.ReadFromJsonAsync<HairMultiCutQuery>(TestContext.Current.CancellationToken);
|
|
||||||
Assert.NotNull(created);
|
|
||||||
Assert.NotEqual(0, created!.Id);
|
|
||||||
Assert.Equal("alice", created.ClientId);
|
|
||||||
Assert.Equal(2, created.Prestations.Count);
|
|
||||||
|
|
||||||
var getResponse = await http.GetAsync($"/api/v1/billing/MBrush/{created.Id}", TestContext.Current.CancellationToken);
|
|
||||||
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
|
|
||||||
|
|
||||||
var fetched = await getResponse.Content.ReadFromJsonAsync<HairMultiCutQuery>(TestContext.Current.CancellationToken);
|
|
||||||
Assert.NotNull(fetched);
|
|
||||||
Assert.Equal(created.Id, fetched!.Id);
|
|
||||||
Assert.Equal(2, fetched.Prestations.Count);
|
|
||||||
|
|
||||||
fetched.Status = QueryStatus.Accepted;
|
|
||||||
var putResponse = await http.PutAsJsonAsync($"/api/v1/billing/MBrush/{fetched.Id}", fetched, TestContext.Current.CancellationToken);
|
|
||||||
Assert.Equal(HttpStatusCode.NoContent, putResponse.StatusCode);
|
|
||||||
|
|
||||||
var deleteResponse = await http.DeleteAsync($"/api/v1/billing/MBrush/{fetched.Id}", TestContext.Current.CancellationToken);
|
|
||||||
Assert.Equal(HttpStatusCode.OK, deleteResponse.StatusCode);
|
|
||||||
|
|
||||||
var missingResponse = await http.GetAsync($"/api/v1/billing/MBrush/{fetched.Id}", TestContext.Current.CancellationToken);
|
|
||||||
Assert.Equal(HttpStatusCode.NotFound, missingResponse.StatusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Billing_mbrush_route_exposes_prestation_catalog()
|
|
||||||
{
|
|
||||||
_fixture.ResetAndSeedHaircutGraph();
|
|
||||||
using var http = NewClient();
|
|
||||||
|
|
||||||
var response = await http.GetAsync("/api/v1/billing/MBrush/prestations", TestContext.Current.CancellationToken);
|
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
||||||
|
|
||||||
var catalog = await response.Content.ReadFromJsonAsync<List<HairPrestationDto>>(TestContext.Current.CancellationToken);
|
|
||||||
Assert.NotNull(catalog);
|
|
||||||
Assert.NotEmpty(catalog!);
|
|
||||||
Assert.All(catalog!, item => Assert.False(string.IsNullOrWhiteSpace(item.Details)));
|
|
||||||
Assert.Contains(catalog!, item => item.Title == "Femme · Cheveux mi-longs"
|
|
||||||
&& item.Details == "Coupe · Brushing · Aucune technique spécifique · Shampoing · Sans soins");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<long>> GetPrestationIdsAsync()
|
|
||||||
{
|
|
||||||
using var scope = _fixture.Services.CreateScope();
|
|
||||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
|
||||||
return await db.HairPrestation
|
|
||||||
.OrderBy(p => p.Id)
|
|
||||||
.Select(p => p.Id)
|
|
||||||
.Take(2)
|
|
||||||
.ToListAsync(TestContext.Current.CancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
using System.Net;
|
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using System.Net.Http.Json;
|
|
||||||
using Yavsc;
|
|
||||||
using Yavsc.Api.Test.Fixtures;
|
|
||||||
using Yavsc.Models.Workflow;
|
|
||||||
using Yavsc.Tests.Shared;
|
|
||||||
|
|
||||||
namespace Yavsc.Api.Test;
|
|
||||||
|
|
||||||
[Collection("Yavsc Api")]
|
|
||||||
public sealed class RdvQueryApiControllerTests : IClassFixture<ApiWebServerFixture>
|
|
||||||
{
|
|
||||||
private readonly ApiWebServerFixture _fixture;
|
|
||||||
|
|
||||||
public RdvQueryApiControllerTests(ApiWebServerFixture fixture)
|
|
||||||
{
|
|
||||||
_fixture = fixture;
|
|
||||||
}
|
|
||||||
|
|
||||||
private HttpClient NewClient(string subject = "alice", string scope = "api")
|
|
||||||
{
|
|
||||||
var handler = new HttpClientHandler
|
|
||||||
{
|
|
||||||
ServerCertificateCustomValidationCallback = (_, _, _, _) => true
|
|
||||||
};
|
|
||||||
|
|
||||||
var http = new HttpClient(handler)
|
|
||||||
{
|
|
||||||
BaseAddress = new Uri(_fixture.BaseAddress)
|
|
||||||
};
|
|
||||||
http.DefaultRequestHeaders.Authorization =
|
|
||||||
new AuthenticationHeaderValue("Bearer", TestTokenIssuer.Issue(subject, scope));
|
|
||||||
return http;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Billing_rdv_route_supports_crud()
|
|
||||||
{
|
|
||||||
_fixture.ResetAndSeedRdvQueryGraph();
|
|
||||||
using var http = NewClient();
|
|
||||||
|
|
||||||
var createPayload = new RdvQuery
|
|
||||||
{
|
|
||||||
ActivityCode = "dev",
|
|
||||||
PerformerId = "alice",
|
|
||||||
Consent = true,
|
|
||||||
EventDate = DateTime.UtcNow.AddDays(2),
|
|
||||||
Location = new Yavsc.Models.Relationship.Location
|
|
||||||
{
|
|
||||||
Address = "1 rue du Test",
|
|
||||||
Latitude = 48.8566,
|
|
||||||
Longitude = 2.3522,
|
|
||||||
},
|
|
||||||
Reason = "Second rendez-vous",
|
|
||||||
Status = QueryStatus.Inserted,
|
|
||||||
};
|
|
||||||
|
|
||||||
var createResponse = await http.PostAsJsonAsync("/api/v1/billing/Rdv", createPayload, TestContext.Current.CancellationToken);
|
|
||||||
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
|
|
||||||
|
|
||||||
var created = await createResponse.Content.ReadFromJsonAsync<RdvQuery>(TestContext.Current.CancellationToken);
|
|
||||||
Assert.NotNull(created);
|
|
||||||
Assert.NotEqual(0, created!.Id);
|
|
||||||
Assert.Equal("alice", created.ClientId);
|
|
||||||
|
|
||||||
var getResponse = await http.GetAsync($"/api/v1/billing/Rdv/{created.Id}", TestContext.Current.CancellationToken);
|
|
||||||
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
|
|
||||||
|
|
||||||
var fetched = await getResponse.Content.ReadFromJsonAsync<RdvQuery>(TestContext.Current.CancellationToken);
|
|
||||||
Assert.NotNull(fetched);
|
|
||||||
Assert.Equal(created.Id, fetched!.Id);
|
|
||||||
Assert.Equal("Second rendez-vous", fetched.Reason);
|
|
||||||
|
|
||||||
fetched.Reason = "Rendez-vous modifié";
|
|
||||||
var putResponse = await http.PutAsJsonAsync($"/api/v1/billing/Rdv/{fetched.Id}", fetched, TestContext.Current.CancellationToken);
|
|
||||||
Assert.Equal(HttpStatusCode.NoContent, putResponse.StatusCode);
|
|
||||||
|
|
||||||
var deleteResponse = await http.DeleteAsync($"/api/v1/billing/Rdv/{fetched.Id}", TestContext.Current.CancellationToken);
|
|
||||||
Assert.Equal(HttpStatusCode.OK, deleteResponse.StatusCode);
|
|
||||||
|
|
||||||
var missingResponse = await http.GetAsync($"/api/v1/billing/Rdv/{fetched.Id}", TestContext.Current.CancellationToken);
|
|
||||||
Assert.Equal(HttpStatusCode.NotFound, missingResponse.StatusCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,234 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Yavsc.Models;
|
|
||||||
using Yavsc.Models.Billing;
|
|
||||||
using Yavsc.Models.Haircut;
|
|
||||||
using Yavsc.Models.Relationship;
|
|
||||||
using Yavsc.Server.Helpers;
|
|
||||||
|
|
||||||
namespace Yavsc.Controllers;
|
|
||||||
|
|
||||||
[Authorize]
|
|
||||||
[Produces("application/json")]
|
|
||||||
[Route(Constants.APIPrefix + "/billing/" + BillingCodes.Brush)]
|
|
||||||
public class HairCutQueryApiController : Controller
|
|
||||||
{
|
|
||||||
private readonly ApplicationDbContext _context;
|
|
||||||
|
|
||||||
public HairCutQueryApiController(ApplicationDbContext context)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public async Task<IActionResult> GetQueries(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var uid = User.GetUserId();
|
|
||||||
|
|
||||||
var queries = await _context.HairCutQueries
|
|
||||||
.AsNoTracking()
|
|
||||||
.Include(q => q.Prestation)
|
|
||||||
.Include(q => q.Location)
|
|
||||||
.Include(q => q.Client)
|
|
||||||
.Include(q => q.PerformerProfile)
|
|
||||||
.Where(q => q.ClientId == uid || q.PerformerId == uid)
|
|
||||||
.OrderByDescending(q => q.Id)
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return Ok(queries);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("prestations")]
|
|
||||||
public async Task<IActionResult> GetPrestations(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var prestations = await _context.HairPrestation
|
|
||||||
.AsNoTracking()
|
|
||||||
.OrderBy(p => p.Gender)
|
|
||||||
.ThenBy(p => p.Length)
|
|
||||||
.ThenBy(p => p.Tech)
|
|
||||||
.Select(p => ToDto(p))
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return Ok(prestations);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}", Name = "GetBillingHairCutQuery")]
|
|
||||||
public async Task<IActionResult> GetQuery([FromRoute] long id, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var uid = User.GetUserId();
|
|
||||||
|
|
||||||
var query = await _context.HairCutQueries
|
|
||||||
.Include(q => q.Prestation)
|
|
||||||
.Include(q => q.Location)
|
|
||||||
.Include(q => q.Client)
|
|
||||||
.Include(q => q.PerformerProfile)
|
|
||||||
.SingleOrDefaultAsync(q => q.Id == id, cancellationToken);
|
|
||||||
|
|
||||||
if (query is null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.ClientId != uid && query.PerformerId != uid && !User.IsInRole(Constants.AdminGroupName))
|
|
||||||
{
|
|
||||||
return Forbid();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<IActionResult> PostQuery([FromBody] HairCutQuery query, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var uid = User.GetUserId();
|
|
||||||
if (string.IsNullOrWhiteSpace(query.ClientId))
|
|
||||||
{
|
|
||||||
query.ClientId = uid;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModelState.Remove("ClientId");
|
|
||||||
ModelState.Remove("Prestation");
|
|
||||||
|
|
||||||
if (query.ClientId != uid && !User.IsInRole(Constants.AdminGroupName))
|
|
||||||
{
|
|
||||||
ModelState.AddModelError("ClientId", "You can only create your own HairCutQuery");
|
|
||||||
return BadRequest(ModelState);
|
|
||||||
}
|
|
||||||
|
|
||||||
query.Prestation = await _context.HairPrestation
|
|
||||||
.SingleOrDefaultAsync(p => p.Id == query.PrestationId, cancellationToken);
|
|
||||||
if (query.Prestation is null)
|
|
||||||
{
|
|
||||||
ModelState.AddModelError("PrestationId", "Unknown hair prestation.");
|
|
||||||
return BadRequest(ModelState);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ModelState.IsValid)
|
|
||||||
{
|
|
||||||
return BadRequest(ModelState);
|
|
||||||
}
|
|
||||||
|
|
||||||
query.Location = await ResolveLocationAsync(query.Location, cancellationToken);
|
|
||||||
|
|
||||||
_context.HairCutQueries.Add(query);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _context.SaveChangesAsync(User.GetUserId(), cancellationToken);
|
|
||||||
}
|
|
||||||
catch (DbUpdateException)
|
|
||||||
{
|
|
||||||
if (QueryExists(query.Id))
|
|
||||||
{
|
|
||||||
return Conflict();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
return CreatedAtRoute("GetBillingHairCutQuery", new { id = query.Id }, query);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("{id}")]
|
|
||||||
public async Task<IActionResult> PutQuery([FromRoute] long id, [FromBody] HairCutQuery query, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var existing = await _context.HairCutQueries
|
|
||||||
.Include(q => q.Location)
|
|
||||||
.SingleOrDefaultAsync(q => q.Id == id, cancellationToken);
|
|
||||||
|
|
||||||
if (existing is null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
var uid = User.GetUserId();
|
|
||||||
if (existing.ClientId != uid && !User.IsInRole(Constants.AdminGroupName))
|
|
||||||
{
|
|
||||||
return Forbid();
|
|
||||||
}
|
|
||||||
|
|
||||||
var prestation = await _context.HairPrestation
|
|
||||||
.SingleOrDefaultAsync(p => p.Id == query.PrestationId, cancellationToken);
|
|
||||||
if (prestation is null)
|
|
||||||
{
|
|
||||||
ModelState.AddModelError("PrestationId", "Unknown hair prestation.");
|
|
||||||
return BadRequest(ModelState);
|
|
||||||
}
|
|
||||||
|
|
||||||
existing.ActivityCode = query.ActivityCode;
|
|
||||||
existing.PerformerId = query.PerformerId;
|
|
||||||
existing.Consent = query.Consent;
|
|
||||||
existing.EventDate = query.EventDate;
|
|
||||||
existing.AdditionalInfo = query.AdditionalInfo;
|
|
||||||
existing.Status = query.Status;
|
|
||||||
existing.Provisional = query.Provisional;
|
|
||||||
existing.PrestationId = prestation.Id;
|
|
||||||
existing.Prestation = prestation;
|
|
||||||
existing.Location = await ResolveLocationAsync(query.Location, cancellationToken);
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync(User.GetUserId(), cancellationToken);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("{id}")]
|
|
||||||
public async Task<IActionResult> DeleteQuery([FromRoute] long id, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var uid = User.GetUserId();
|
|
||||||
|
|
||||||
var query = await _context.HairCutQueries
|
|
||||||
.SingleOrDefaultAsync(q => q.Id == id, cancellationToken);
|
|
||||||
|
|
||||||
if (query is null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.ClientId != uid && !User.IsInRole(Constants.AdminGroupName))
|
|
||||||
{
|
|
||||||
return Forbid();
|
|
||||||
}
|
|
||||||
|
|
||||||
_context.HairCutQueries.Remove(query);
|
|
||||||
await _context.SaveChangesAsync(User.GetUserId(), cancellationToken);
|
|
||||||
|
|
||||||
return Ok(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Location> ResolveLocationAsync(Location candidate, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (candidate is null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var existingLocation = await _context.Locations.FirstOrDefaultAsync(
|
|
||||||
x => x.Address == candidate.Address
|
|
||||||
&& x.Longitude == candidate.Longitude
|
|
||||||
&& x.Latitude == candidate.Latitude,
|
|
||||||
cancellationToken);
|
|
||||||
|
|
||||||
if (existingLocation is not null)
|
|
||||||
{
|
|
||||||
return existingLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
_context.Attach(candidate);
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool QueryExists(long id)
|
|
||||||
{
|
|
||||||
return _context.HairCutQueries.Any(e => e.Id == id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HairPrestationDto ToDto(HairPrestation prestation)
|
|
||||||
{
|
|
||||||
return new HairPrestationDto
|
|
||||||
{
|
|
||||||
Id = prestation.Id,
|
|
||||||
Title = prestation.GetDisplayTitle(),
|
|
||||||
Details = prestation.GetDisplayDetails(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,286 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Yavsc.Models;
|
|
||||||
using Yavsc.Models.Billing;
|
|
||||||
using Yavsc.Models.Haircut;
|
|
||||||
using Yavsc.Models.Relationship;
|
|
||||||
using Yavsc.Server.Helpers;
|
|
||||||
|
|
||||||
namespace Yavsc.Controllers;
|
|
||||||
|
|
||||||
[Authorize]
|
|
||||||
[Produces("application/json")]
|
|
||||||
[Route(Constants.APIPrefix + "/billing/" + BillingCodes.MBrush)]
|
|
||||||
public class HairMultiCutQueryApiController : Controller
|
|
||||||
{
|
|
||||||
private readonly ApplicationDbContext _context;
|
|
||||||
|
|
||||||
public HairMultiCutQueryApiController(ApplicationDbContext context)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public async Task<IActionResult> GetQueries(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var uid = User.GetUserId();
|
|
||||||
|
|
||||||
var queries = await _context.HairMultiCutQueries
|
|
||||||
.AsNoTracking()
|
|
||||||
.Include(q => q.Prestations)
|
|
||||||
.ThenInclude(p => p.Prestation)
|
|
||||||
.Include(q => q.Location)
|
|
||||||
.Include(q => q.Client)
|
|
||||||
.Include(q => q.PerformerProfile)
|
|
||||||
.Where(q => q.ClientId == uid || q.PerformerId == uid)
|
|
||||||
.OrderByDescending(q => q.Id)
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return Ok(queries.Select(SanitizeForResponse).ToList());
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("prestations")]
|
|
||||||
public async Task<IActionResult> GetPrestations(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var prestations = await _context.HairPrestation
|
|
||||||
.AsNoTracking()
|
|
||||||
.OrderBy(p => p.Gender)
|
|
||||||
.ThenBy(p => p.Length)
|
|
||||||
.ThenBy(p => p.Tech)
|
|
||||||
.Select(p => new HairPrestationDto
|
|
||||||
{
|
|
||||||
Id = p.Id,
|
|
||||||
Title = p.GetDisplayTitle(),
|
|
||||||
Details = p.GetDisplayDetails()
|
|
||||||
})
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return Ok(prestations);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}", Name = "GetBillingHairMultiCutQuery")]
|
|
||||||
public async Task<IActionResult> GetQuery([FromRoute] long id, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var uid = User.GetUserId();
|
|
||||||
|
|
||||||
var query = await _context.HairMultiCutQueries
|
|
||||||
.Include(q => q.Prestations)
|
|
||||||
.ThenInclude(p => p.Prestation)
|
|
||||||
.Include(q => q.Location)
|
|
||||||
.Include(q => q.Client)
|
|
||||||
.Include(q => q.PerformerProfile)
|
|
||||||
.SingleOrDefaultAsync(q => q.Id == id, cancellationToken);
|
|
||||||
|
|
||||||
if (query is null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.ClientId != uid && query.PerformerId != uid && !User.IsInRole(Constants.AdminGroupName))
|
|
||||||
{
|
|
||||||
return Forbid();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(SanitizeForResponse(query));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<IActionResult> PostQuery([FromBody] HairMultiCutQuery query, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var uid = User.GetUserId();
|
|
||||||
if (string.IsNullOrWhiteSpace(query.ClientId))
|
|
||||||
{
|
|
||||||
query.ClientId = uid;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModelState.Remove("ClientId");
|
|
||||||
|
|
||||||
if (query.ClientId != uid && !User.IsInRole(Constants.AdminGroupName))
|
|
||||||
{
|
|
||||||
ModelState.AddModelError("ClientId", "You can only create your own HairMultiCutQuery");
|
|
||||||
return BadRequest(ModelState);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.Prestations is null || query.Prestations.Count == 0)
|
|
||||||
{
|
|
||||||
ModelState.AddModelError("Prestations", "At least one hair prestation is required.");
|
|
||||||
return BadRequest(ModelState);
|
|
||||||
}
|
|
||||||
|
|
||||||
var prestationItems = await ResolvePrestationsAsync(query.Prestations, cancellationToken);
|
|
||||||
if (prestationItems is null)
|
|
||||||
{
|
|
||||||
ModelState.AddModelError("Prestations", "One or more hair prestations are unknown.");
|
|
||||||
return BadRequest(ModelState);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ModelState.IsValid)
|
|
||||||
{
|
|
||||||
return BadRequest(ModelState);
|
|
||||||
}
|
|
||||||
|
|
||||||
query.Prestations = prestationItems;
|
|
||||||
query.Location = await ResolveLocationAsync(query.Location, cancellationToken);
|
|
||||||
_context.HairMultiCutQueries.Add(query);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _context.SaveChangesAsync(User.GetUserId(), cancellationToken);
|
|
||||||
}
|
|
||||||
catch (DbUpdateException)
|
|
||||||
{
|
|
||||||
if (QueryExists(query.Id))
|
|
||||||
{
|
|
||||||
return Conflict();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
return CreatedAtRoute("GetBillingHairMultiCutQuery", new { id = query.Id }, SanitizeForResponse(query));
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("{id}")]
|
|
||||||
public async Task<IActionResult> PutQuery([FromRoute] long id, [FromBody] HairMultiCutQuery query, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var existing = await _context.HairMultiCutQueries
|
|
||||||
.Include(q => q.Prestations)
|
|
||||||
.SingleOrDefaultAsync(q => q.Id == id, cancellationToken);
|
|
||||||
|
|
||||||
if (existing is null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
var uid = User.GetUserId();
|
|
||||||
if (existing.ClientId != uid && !User.IsInRole(Constants.AdminGroupName))
|
|
||||||
{
|
|
||||||
return Forbid();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.Prestations is null || query.Prestations.Count == 0)
|
|
||||||
{
|
|
||||||
ModelState.AddModelError("Prestations", "At least one hair prestation is required.");
|
|
||||||
return BadRequest(ModelState);
|
|
||||||
}
|
|
||||||
|
|
||||||
var prestationItems = await ResolvePrestationsAsync(query.Prestations, cancellationToken);
|
|
||||||
if (prestationItems is null)
|
|
||||||
{
|
|
||||||
ModelState.AddModelError("Prestations", "One or more hair prestations are unknown.");
|
|
||||||
return BadRequest(ModelState);
|
|
||||||
}
|
|
||||||
|
|
||||||
_context.RemoveRange(existing.Prestations);
|
|
||||||
existing.ActivityCode = query.ActivityCode;
|
|
||||||
existing.PerformerId = query.PerformerId;
|
|
||||||
existing.Consent = query.Consent;
|
|
||||||
existing.EventDate = query.EventDate;
|
|
||||||
existing.Status = query.Status;
|
|
||||||
existing.Provisional = query.Provisional;
|
|
||||||
existing.Prestations = prestationItems;
|
|
||||||
existing.Location = await ResolveLocationAsync(query.Location, cancellationToken);
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync(User.GetUserId(), cancellationToken);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("{id}")]
|
|
||||||
public async Task<IActionResult> DeleteQuery([FromRoute] long id, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var uid = User.GetUserId();
|
|
||||||
|
|
||||||
var query = await _context.HairMultiCutQueries
|
|
||||||
.Include(q => q.Prestations)
|
|
||||||
.SingleOrDefaultAsync(q => q.Id == id, cancellationToken);
|
|
||||||
|
|
||||||
if (query is null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.ClientId != uid && !User.IsInRole(Constants.AdminGroupName))
|
|
||||||
{
|
|
||||||
return Forbid();
|
|
||||||
}
|
|
||||||
|
|
||||||
_context.RemoveRange(query.Prestations);
|
|
||||||
_context.HairMultiCutQueries.Remove(query);
|
|
||||||
await _context.SaveChangesAsync(User.GetUserId(), cancellationToken);
|
|
||||||
|
|
||||||
return Ok(SanitizeForResponse(query));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<HairPrestationCollectionItem>> ResolvePrestationsAsync(
|
|
||||||
IEnumerable<HairPrestationCollectionItem> requestedItems,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var ids = requestedItems
|
|
||||||
.Select(x => x.PrestationId)
|
|
||||||
.Where(x => x > 0)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
if (ids.Length == 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var prestations = await _context.HairPrestation
|
|
||||||
.Where(p => ids.Contains(p.Id))
|
|
||||||
.ToDictionaryAsync(p => p.Id, cancellationToken);
|
|
||||||
|
|
||||||
if (prestations.Count != ids.Distinct().Count())
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return requestedItems
|
|
||||||
.Select(item => new HairPrestationCollectionItem
|
|
||||||
{
|
|
||||||
PrestationId = item.PrestationId,
|
|
||||||
Prestation = prestations[item.PrestationId],
|
|
||||||
})
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Location> ResolveLocationAsync(Location candidate, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (candidate is null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var existingLocation = await _context.Locations.FirstOrDefaultAsync(
|
|
||||||
x => x.Address == candidate.Address
|
|
||||||
&& x.Longitude == candidate.Longitude
|
|
||||||
&& x.Latitude == candidate.Latitude,
|
|
||||||
cancellationToken);
|
|
||||||
|
|
||||||
if (existingLocation is not null)
|
|
||||||
{
|
|
||||||
return existingLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
_context.Attach(candidate);
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool QueryExists(long id)
|
|
||||||
{
|
|
||||||
return _context.HairMultiCutQueries.Any(e => e.Id == id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HairMultiCutQuery SanitizeForResponse(HairMultiCutQuery query)
|
|
||||||
{
|
|
||||||
if (query.Prestations is not null)
|
|
||||||
{
|
|
||||||
foreach (var item in query.Prestations)
|
|
||||||
{
|
|
||||||
item.Query = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,189 +0,0 @@
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Yavsc.Models;
|
|
||||||
using Yavsc.Models.Billing;
|
|
||||||
using Yavsc.Models.Workflow;
|
|
||||||
using Yavsc.Server.Helpers;
|
|
||||||
|
|
||||||
namespace Yavsc.Controllers;
|
|
||||||
|
|
||||||
[Authorize]
|
|
||||||
[Produces("application/json")]
|
|
||||||
[Route(Constants.APIPrefix + "/billing/" + BillingCodes.Rdv)]
|
|
||||||
public class RdvQueryApiController : Controller
|
|
||||||
{
|
|
||||||
private readonly ApplicationDbContext _context;
|
|
||||||
|
|
||||||
public RdvQueryApiController(ApplicationDbContext context)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public async Task<IActionResult> GetQueries(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var uid = User.GetUserId();
|
|
||||||
|
|
||||||
var queries = await _context.RdvQueries
|
|
||||||
.AsNoTracking()
|
|
||||||
.Include(q => q.Location)
|
|
||||||
.Include(q => q.Client)
|
|
||||||
.Include(q => q.PerformerProfile)
|
|
||||||
.Where(q => q.ClientId == uid || q.PerformerId == uid)
|
|
||||||
.OrderByDescending(q => q.Id)
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
return Ok(queries);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}", Name = "GetRdvQuery")]
|
|
||||||
public async Task<IActionResult> GetQuery([FromRoute] long id, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var uid = User.GetUserId();
|
|
||||||
|
|
||||||
var query = await _context.RdvQueries
|
|
||||||
.Include(q => q.Location)
|
|
||||||
.Include(q => q.Client)
|
|
||||||
.Include(q => q.PerformerProfile)
|
|
||||||
.SingleOrDefaultAsync(q => q.Id == id, cancellationToken);
|
|
||||||
|
|
||||||
if (query is null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.ClientId != uid && query.PerformerId != uid && !User.IsInRole(Constants.AdminGroupName))
|
|
||||||
{
|
|
||||||
return Forbid();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<IActionResult> PostQuery([FromBody] RdvQuery query, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var uid = User.GetUserId();
|
|
||||||
if (string.IsNullOrWhiteSpace(query.ClientId))
|
|
||||||
{
|
|
||||||
query.ClientId = uid;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModelState.Remove("ClientId");
|
|
||||||
|
|
||||||
if (query.ClientId != uid && !User.IsInRole(Constants.AdminGroupName))
|
|
||||||
{
|
|
||||||
ModelState.AddModelError("ClientId", "You can only create your own RdvQuery");
|
|
||||||
return BadRequest(ModelState);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ModelState.IsValid)
|
|
||||||
{
|
|
||||||
return BadRequest(ModelState);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.Location is not null)
|
|
||||||
{
|
|
||||||
var existingLocation = await _context.Locations.FirstOrDefaultAsync(
|
|
||||||
x => x.Address == query.Location.Address
|
|
||||||
&& x.Longitude == query.Location.Longitude
|
|
||||||
&& x.Latitude == query.Location.Latitude,
|
|
||||||
cancellationToken);
|
|
||||||
|
|
||||||
if (existingLocation is not null)
|
|
||||||
{
|
|
||||||
query.Location = existingLocation;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_context.Attach(query.Location);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_context.RdvQueries.Add(query);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _context.SaveChangesAsync(User.GetUserId(), cancellationToken);
|
|
||||||
}
|
|
||||||
catch (DbUpdateException)
|
|
||||||
{
|
|
||||||
if (QueryExists(query.Id))
|
|
||||||
{
|
|
||||||
return Conflict();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
return CreatedAtRoute("GetRdvQuery", new { id = query.Id }, query);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("{id}")]
|
|
||||||
public async Task<IActionResult> PutQuery([FromRoute] long id, [FromBody] RdvQuery query, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (!ModelState.IsValid)
|
|
||||||
{
|
|
||||||
return BadRequest(ModelState);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id != query.Id)
|
|
||||||
{
|
|
||||||
return BadRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
var uid = User.GetUserId();
|
|
||||||
if (query.ClientId != uid && !User.IsInRole(Constants.AdminGroupName))
|
|
||||||
{
|
|
||||||
return Forbid();
|
|
||||||
}
|
|
||||||
|
|
||||||
_context.Entry(query).State = EntityState.Modified;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _context.SaveChangesAsync(User.GetUserId(), cancellationToken);
|
|
||||||
}
|
|
||||||
catch (DbUpdateConcurrencyException)
|
|
||||||
{
|
|
||||||
if (!QueryExists(id))
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("{id}")]
|
|
||||||
public async Task<IActionResult> DeleteQuery([FromRoute] long id, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var uid = User.GetUserId();
|
|
||||||
|
|
||||||
var query = await _context.RdvQueries
|
|
||||||
.SingleOrDefaultAsync(q => q.Id == id, cancellationToken);
|
|
||||||
|
|
||||||
if (query is null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.ClientId != uid && !User.IsInRole(Constants.AdminGroupName))
|
|
||||||
{
|
|
||||||
return Forbid();
|
|
||||||
}
|
|
||||||
|
|
||||||
_context.RdvQueries.Remove(query);
|
|
||||||
await _context.SaveChangesAsync(User.GetUserId(), cancellationToken);
|
|
||||||
|
|
||||||
return Ok(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool QueryExists(long id)
|
|
||||||
{
|
|
||||||
return _context.RdvQueries.Any(e => e.Id == id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
120
src/Yavsc.Org/Controllers/Contracting/FormsController.cs
Normal file
120
src/Yavsc.Org/Controllers/Contracting/FormsController.cs
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Yavsc.Models;
|
||||||
|
using Yavsc.Models.Forms;
|
||||||
|
using Yavsc.Server.Helpers;
|
||||||
|
|
||||||
|
namespace Yavsc.Controllers
|
||||||
|
{
|
||||||
|
public class FormsController : Controller
|
||||||
|
{
|
||||||
|
private readonly ApplicationDbContext _context;
|
||||||
|
|
||||||
|
public FormsController(ApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: Forms
|
||||||
|
public async Task<IActionResult> Index()
|
||||||
|
{
|
||||||
|
return View(await _context.Form.ToListAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: Forms/Details/5
|
||||||
|
public async Task<IActionResult> Details(string id)
|
||||||
|
{
|
||||||
|
if (id == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
Form form = await _context.Form.SingleAsync(m => m.Id == id);
|
||||||
|
if (form == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return View(form);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: Forms/Create
|
||||||
|
public IActionResult Create()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: Forms/Create
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> Create(Form form)
|
||||||
|
{
|
||||||
|
if (ModelState.IsValid)
|
||||||
|
{
|
||||||
|
_context.Form.Add(form);
|
||||||
|
await _context.SaveChangesAsync(User.GetUserId());
|
||||||
|
return RedirectToAction("Index");
|
||||||
|
}
|
||||||
|
return View(form);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: Forms/Edit/5
|
||||||
|
public async Task<IActionResult> Edit(string id)
|
||||||
|
{
|
||||||
|
if (id == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
Form form = await _context.Form.SingleAsync(m => m.Id == id);
|
||||||
|
if (form == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
return View(form);
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: Forms/Edit/5
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> Edit(Form form)
|
||||||
|
{
|
||||||
|
if (ModelState.IsValid)
|
||||||
|
{
|
||||||
|
_context.Update(form);
|
||||||
|
await _context.SaveChangesAsync(User.GetUserId());
|
||||||
|
return RedirectToAction("Index");
|
||||||
|
}
|
||||||
|
return View(form);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: Forms/Delete/5
|
||||||
|
[ActionName("Delete")]
|
||||||
|
public async Task<IActionResult> Delete(string id)
|
||||||
|
{
|
||||||
|
if (id == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
Form form = await _context.Form.SingleAsync(m => m.Id == id);
|
||||||
|
if (form == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return View(form);
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: Forms/Delete/5
|
||||||
|
[HttpPost, ActionName("Delete")]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> DeleteConfirmed(string id)
|
||||||
|
{
|
||||||
|
Form form = await _context.Form.SingleAsync(m => m.Id == id);
|
||||||
|
_context.Form.Remove(form);
|
||||||
|
await _context.SaveChangesAsync(User.GetUserId());
|
||||||
|
return RedirectToAction("Index");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ namespace Yavsc.Models
|
||||||
using Blog;
|
using Blog;
|
||||||
using Chat;
|
using Chat;
|
||||||
using Drawing;
|
using Drawing;
|
||||||
|
using Forms;
|
||||||
using Haircut;
|
using Haircut;
|
||||||
using Identity;
|
using Identity;
|
||||||
using IT.Evolution;
|
using IT.Evolution;
|
||||||
|
|
@ -351,6 +352,8 @@ namespace Yavsc.Models
|
||||||
|
|
||||||
public DbSet<CommandForm> CommandForm { get; set; }
|
public DbSet<CommandForm> CommandForm { get; set; }
|
||||||
|
|
||||||
|
public DbSet<Form> Form { get; set; }
|
||||||
|
|
||||||
public DbSet<Ban> Ban { get; set; }
|
public DbSet<Ban> Ban { get; set; }
|
||||||
|
|
||||||
public DbSet<HairTaint> HairTaint { get; set; }
|
public DbSet<HairTaint> HairTaint { get; set; }
|
||||||
|
|
|
||||||
12
src/Yavsc.Server/Models/Forms/Form.cs
Normal file
12
src/Yavsc.Server/Models/Forms/Form.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Yavsc.Models.Forms
|
||||||
|
{
|
||||||
|
|
||||||
|
public class Form
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public string Id {get; set;}
|
||||||
|
public string Summary { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,10 +4,8 @@ namespace Yavsc.Models.Haircut
|
||||||
{
|
{
|
||||||
public enum HairDressings {
|
public enum HairDressings {
|
||||||
|
|
||||||
[Display(Name="Coiffage")]
|
|
||||||
Coiffage,
|
Coiffage,
|
||||||
|
|
||||||
[Display(Name="Brushing")]
|
|
||||||
Brushing,
|
Brushing,
|
||||||
|
|
||||||
[Display(Name="Mise en plis")]
|
[Display(Name="Mise en plis")]
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,12 @@
|
||||||
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Yavsc.Models.Haircut
|
namespace Yavsc.Models.Haircut
|
||||||
{
|
{
|
||||||
public enum HairLength : int
|
public enum HairLength : int
|
||||||
{
|
{
|
||||||
[Display(Name="Cheveux mi-longs")]
|
|
||||||
HalfLong=0,
|
HalfLong=0,
|
||||||
|
|
||||||
[Display(Name="Cheveux courts")]
|
|
||||||
Short=1,
|
Short=1,
|
||||||
|
|
||||||
[Display(Name="Cheveux longs")]
|
|
||||||
Long=2
|
Long=2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Reflection;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Yavsc.Models.Haircut
|
namespace Yavsc.Models.Haircut
|
||||||
|
|
@ -41,41 +40,6 @@ namespace Yavsc.Models.Haircut
|
||||||
[Display(Name="Soins")]
|
[Display(Name="Soins")]
|
||||||
public bool Cares { get; set; }
|
public bool Cares { get; set; }
|
||||||
|
|
||||||
public string GetDisplayTitle()
|
|
||||||
{
|
|
||||||
return $"{GetEnumDisplayName(Gender)} · {GetEnumDisplayName(Length)}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetDisplayDetails()
|
|
||||||
{
|
|
||||||
return string.Join(" · ", new[]
|
|
||||||
{
|
|
||||||
FormatFlag(nameof(Cut), Cut),
|
|
||||||
GetEnumDisplayName(Dressing),
|
|
||||||
GetEnumDisplayName(Tech),
|
|
||||||
FormatFlag(nameof(Shampoo), Shampoo),
|
|
||||||
FormatFlag(nameof(Cares), Cares),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatFlag(string propertyName, bool enabled)
|
|
||||||
{
|
|
||||||
var label = GetPropertyDisplayName(propertyName);
|
|
||||||
return enabled ? label : $"Sans {label.ToLowerInvariant()}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetPropertyDisplayName(string propertyName)
|
|
||||||
{
|
|
||||||
var property = typeof(HairPrestation).GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
|
|
||||||
return property?.GetCustomAttribute<DisplayAttribute>()?.GetName() ?? propertyName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetEnumDisplayName<TEnum>(TEnum value) where TEnum : struct, Enum
|
|
||||||
{
|
|
||||||
var member = typeof(TEnum).GetMember(value.ToString()).FirstOrDefault();
|
|
||||||
return member?.GetCustomAttribute<DisplayAttribute>()?.GetName() ?? value.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
public class HairTaintInstance {
|
public class HairTaintInstance {
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,12 @@ namespace Yavsc.Models.Haircut
|
||||||
[Display(Name="Couleur")]
|
[Display(Name="Couleur")]
|
||||||
Color,
|
Color,
|
||||||
|
|
||||||
[Display(Name="Permanente")]
|
[Display(Name="Permantante")]
|
||||||
Permanent,
|
Permanent,
|
||||||
[Display(Name="Défrisage")]
|
[Display(Name="Défrisage")]
|
||||||
Defris,
|
Defris,
|
||||||
[Display(Name="Mêches")]
|
[Display(Name="Mêches")]
|
||||||
Mech,
|
Mech,
|
||||||
|
|
||||||
[Display(Name="Balayage")]
|
|
||||||
Balayage
|
Balayage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,7 @@ namespace Yavsc.Models.Workflow
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var localizer = ResourcesHelpers.GlobalLocalizer;
|
string type = ResourcesHelpers.GlobalLocalizer[this.GetType().Name];
|
||||||
string type = localizer is null ? this.GetType().Name : localizer[this.GetType().Name];
|
|
||||||
return $"{_description} {type}";
|
return $"{_description} {type}";
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ namespace Yavsc.ViewModels.FrontOffice
|
||||||
WebSite = profile.WebSite;
|
WebSite = profile.WebSite;
|
||||||
Extra = profile.Activity.Where(a => a.DoesCode != activityCode).ToArray();
|
Extra = profile.Activity.Where(a => a.DoesCode != activityCode).ToArray();
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue