refactoring the command form creation

This commit is contained in:
Paul Schneider 2026-09-04 13:19:31 +01:00
commit fc820362b9
Signed by: notazof
GPG key ID: 1DD5D838E5343B06
24 changed files with 940 additions and 698 deletions

2
.vscode/tasks.json vendored
View file

@ -7,7 +7,7 @@
"fileLocation": ["relative", "${workspaceFolder}"],
"source": "dotnet",
"pattern": {
"regexp": "^\\s+(.*)\\((\\d+),(\\d+)\\):\\s+(error|warning) (.+): (.*)$",
"regexp": "^\\s*(.*)\\((\\d+),(\\d+)\\):\\s+(error|warning) (.*)$",
"file": 1,
"line": 2,
"column": 3,

View file

@ -49,6 +49,19 @@ Les tests sont répartis en :
item « Tests d'intégration smoke par BC ».
- `src/PostIt.Tests/` — tests unitaires du client desktop PostIt.
## Onboarding assiste par agents IA
Pour accelerer la prise en main du depot avec Copilot/Plan/Explore :
- Parcours pas-a-pas : [doc/onboarding-agents.md](./doc/onboarding-agents.md)
- Playbook d'usage des agents : [doc/agent-playbook.md](./doc/agent-playbook.md)
- Matrice intentions -> agent -> preuves : [doc/agent-intent-matrix.md](./doc/agent-intent-matrix.md)
Regle minimale en contribution assistee par agent :
- expliciter l'impact architecture,
- justifier le niveau de tests execute,
- documenter les risques residuels.
## Le CHANGELOG.md
Le `CHANGELOG.md` est un document de changement de version

View file

@ -28,6 +28,10 @@ sous [`doc/`](./doc/). Voir l'[index de la documentation](./doc/README.md)
pour le sommaire complet. La racine de l'architecture est
[Architecture.md](./doc/Architecture.md).
Pour une prise en main guidee avec agents IA:
- parcours onboarding: [doc/onboarding-agents.md](./doc/onboarding-agents.md)
- playbook d'usage: [doc/agent-playbook.md](./doc/agent-playbook.md)
# Construction et déploiement

View file

@ -18,6 +18,9 @@ La racine de l'architecture est [Architecture.md](Architecture.md).
| [architecture/postit.md](architecture/postit.md) | PostIt — topologie des projets, ViewLocator custo, navigation, DI, conventions de binding |
| [architecture/decoupage-organisation.md](architecture/decoupage-organisation.md) | Découpage des projets .NET (Abstract, Server, Org, Api, Blogs, Web, Org.Tests) |
| [testing.md](testing.md) | Stratégie de test : conventions des dossiers, EF Core in-memory, auth stubs, scaffold partagé |
| [onboarding-agents.md](onboarding-agents.md) | Parcours pas-à-pas pour prise en main agents IA + architecture + tests |
| [agent-playbook.md](agent-playbook.md) | Playbook d'usage de Copilot, Plan, Explore avec scénarios et anti-patterns |
| [agent-intent-matrix.md](agent-intent-matrix.md) | Matrice intentions développeur -> agent -> preuves attendues |
## Roadmap & design exploration

View file

@ -0,0 +1,20 @@
# Matrice intentions -> agent -> preuves
Cette matrice aide a choisir rapidement l'agent adapte et a exiger
une sortie verifiable.
| Intention developpeur | Agent principal | Entrees minimales | Sortie minimale attendue | Verification |
|---|---|---|---|---|
| Comprendre un BC avant changement | Explore | BC cible, profondeur, contrainte de perimetre | Composants, points d'entree, tests relies, risques | Lire les fichiers cites + confirmer tests proposes |
| Decomposer une tache transverse | Plan | Objectif, contraintes, definition of done | Etapes ordonnees, dependances, criteres de verif | Verifier que chaque etape a une preuve observable |
| Implementer une modif locale | Copilot | Fichier cible, comportement attendu, conventions | Patch minimal, justification courte | Build/test du projet impacte |
| Ajouter un test smoke | Copilot (+Explore) | Route/endpoint, projet de test cible | Test + commande cible | Execution test cible |
| Corriger une regression | Plan + Copilot | Symptome, zone suspecte, test attendu | Fix + test NonRegression | Test rouge avant, vert apres |
| Diagnostiquer flux PostIt/OIDC | Explore + Plan | Flux, symptome, plateforme | Carte du flux + hypotheses testables | Verification manuelle + tests existants |
## Regles d'arbitrage
- Si l'intention est "comprendre": commencer par Explore.
- Si l'intention est "orchestrer": commencer par Plan.
- Si l'intention est "produire": utiliser Copilot apres cadrage.
- Si une sortie n'inclut pas de preuve, elle est incomplete.

View file

@ -1,7 +1,8 @@
using System.Net.Http;
using System.Text.Json;
using PostIt.Helpers;
using PostIt.Services;
using PostIt.ViewModels;
using PostIt.ViewModels.Commands;
using Yavsc;
using Yavsc.Abstract.Workflow;
using Yavsc.Api.Client;
@ -16,19 +17,19 @@ public class BillingCommandPageViewModelTests
{
var api = new RecordingApi();
var client = new BillingApiClient(api, "https://business.example/api/v1/");
var vm = new BillingCommandPageViewModel(
var vm =
new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" }
.CreateCommandPageViewModel(
new ActivityInfo { Code = "dev", Name = "Développement" },
new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" },
new CommandFormSummary { 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,
};
client) as RdvViewModel;
vm.EventDate = DateTime.Parse("2026-09-02 14:30");
vm.Reason = "Point de cadrage";
vm.Address = "1 rue du Test";
vm.Latitude = 48.8566;
vm.Longitude = 2.3522;
vm.Consent = true;
await vm.SubmitCommand.ExecuteAsync(null);
@ -47,10 +48,12 @@ public class BillingCommandPageViewModelTests
{
var api = new RecordingApi();
var client = new BillingApiClient(api, "https://business.example/api/v1/");
var vm = new BillingCommandPageViewModel(
var vm =
new CommandFormSummary { Id = 13, ActionName = "Book", Title = "Réservation" }
.CreateCommandPageViewModel(
new ActivityInfo { Code = "book", Name = "Book" },
new ActivityUserDisplayItem { PerformerId = "perf-2", UserName = "Bob" },
new CommandFormSummary { Id = 13, ActionName = "Book", Title = "Réservation" },
client);
await vm.SubmitCommand.ExecuteAsync(null);
@ -64,19 +67,19 @@ public class BillingCommandPageViewModelTests
{
var api = new RecordingApi();
var client = new BillingApiClient(api, "https://business.example/api/v1/");
var vm = new BillingCommandPageViewModel(
var vm =
new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" }
.CreateCommandPageViewModel(
new ActivityInfo { Code = "dev", Name = "Développement" },
new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" },
new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" },
client)
{
EventDateText = "2026-09-02 14:30",
Reason = "Point de cadrage",
Address = "1 rue du Test",
LatitudeText = string.Empty,
LongitudeText = string.Empty,
Consent = true,
};
client) as RdvViewModel;
vm.EventDate = DateTime.Parse("2026-09-02 14:30");
vm.Reason = "Point de cadrage";
vm.Address = "1 rue du Test";
vm.Latitude = 0;
vm.Longitude = 0;
vm.Consent = true;
await vm.SubmitCommand.ExecuteAsync(null);
@ -97,16 +100,17 @@ public class BillingCommandPageViewModelTests
var api = new RecordingApi();
var client = new BillingApiClient(api, "https://business.example/api/v1/");
var vm = new BillingCommandPageViewModel(
var vm =
new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" }
.CreateCommandPageViewModel(
new ActivityInfo { Code = "dev", Name = "Développement" },
new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" },
new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" },
client);
client) as RdvViewModel;
await vm.UseCurrentLocationCommand.ExecuteAsync(null);
Assert.Equal("48.8566", vm.LatitudeText);
Assert.Equal("2.3522", vm.LongitudeText);
Assert.Equal(48.8566, vm.Latitude);
Assert.Equal(2.3522, vm.Longitude);
}
finally
{
@ -126,19 +130,18 @@ public class BillingCommandPageViewModelTests
}
};
var client = new BillingApiClient(api, "https://business.example/api/v1/");
var vm = new BillingCommandPageViewModel(
var vm =
new CommandFormSummary { Id = 13, ActionName = "Brush", Title = "Coupe" }
.CreateCommandPageViewModel(
new ActivityInfo { Code = "brush", Name = "Brush" },
new ActivityUserDisplayItem { PerformerId = "perf-2", UserName = "Bob" },
new CommandFormSummary { 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",
};
client) as BrushViewModel;
vm.EventDate = DateTime.Parse("2026-09-02 14:30");
vm.Address = "1 rue du Test";
vm.Latitude = 48.8566;
vm.Longitude = 2.3522;
vm.Consent = true;
vm.AdditionalInfo = "Prévoir shampoing";
await vm.InitializeAsync();
vm.SelectedPrestation = vm.AvailablePrestations[1];
@ -162,18 +165,17 @@ public class BillingCommandPageViewModelTests
}
};
var client = new BillingApiClient(api, "https://business.example/api/v1/");
var vm = new BillingCommandPageViewModel(
var vm =
new CommandFormSummary { Id = 14, ActionName = "MBrush", Title = "Coupe groupée" }
.CreateCommandPageViewModel(
new ActivityInfo { Code = "mbrush", Name = "MBrush" },
new ActivityUserDisplayItem { PerformerId = "perf-3", UserName = "Cara" },
new CommandFormSummary { 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,
};
client) as PostIt.ViewModels.Commands.BrushViewModel;
vm.EventDate = DateTime.Parse("2026-09-03 10:00");
vm.Address = "2 rue du Test";
vm.Latitude = 48.8567;
vm.Longitude = 2.3523;
vm.Consent = true;
await vm.InitializeAsync();
vm.MultiPrestations[0].IsSelected = true;
@ -200,12 +202,12 @@ public class BillingCommandPageViewModelTests
}
};
var client = new BillingApiClient(api, "https://business.example/api/v1/");
var vm = new BillingCommandPageViewModel(
var vm =
new CommandFormSummary { Id = 13, ActionName = "Brush", Title = "Coupe" }
.CreateCommandPageViewModel(
new ActivityInfo { Code = "brush", Name = "Brush" },
new ActivityUserDisplayItem { PerformerId = "perf-2", UserName = "Bob" },
new CommandFormSummary { Id = 13, ActionName = "Brush", Title = "Coupe" },
client);
client) as BrushViewModel;
await vm.InitializeAsync(new BillingQueryDetailsDto
{
Id = 77,

View file

@ -0,0 +1,45 @@
using System;
using PostIt.ViewModels;
using Yavsc.Abstract.Workflow;
using Yavsc.Api.Client;
namespace PostIt.Helpers;
public static class FormHelpers
{
public static BillingCommandPageViewModel
CreateCommandPageViewModel(
this CommandFormSummary form,
ActivityInfo activity,
ActivityUserDisplayItem performer,
BillingApiClient billingClient)
{
string formVMName = form.ActionName + "ViewModel";
string formOnActivityVMName = activity.Code + formVMName;
var vmType = Type.GetType(formOnActivityVMName);
if (vmType == null)
{
vmType = Type.GetType(formVMName);
}
if (vmType == null)
{
throw new InvalidOperationException($"Cannot find type '{formOnActivityVMName}' or '{formVMName}'");
}
if (!typeof(BillingCommandPageViewModel).IsAssignableFrom(vmType))
{
throw new InvalidOperationException($"The type '{formOnActivityVMName}' or '{formVMName}' is not a BillingCommandPageViewModel");
}
var vm = Activator.CreateInstance(vmType);
if (vm == null)
{
throw new InvalidOperationException($"Cannot create instance of '{formOnActivityVMName}' or '{formVMName}'");
}
return vm as BillingCommandPageViewModel ?? throw new InvalidOperationException($"The type '{formOnActivityVMName}' or '{formVMName}' is not a BillingCommandPageViewModel");
}
}

View file

@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection;
using PostIt.Services;
using PostIt.ViewModels;
using PostIt.Views;
using PostIt.Views.Commands;
using Yavsc.Api.Client;
namespace PostIt.Helpers;
@ -49,7 +50,8 @@ public static class ServiceCollectionHelpers
services.AddSingleton<CirclesPage>();
services.AddSingleton<ActivitiesPage>();
services.AddTransient<CommandFormsPage>();
services.AddTransient<BillingCommandPage>();
services.AddTransient<RdvPage>();
services.AddTransient<BrushPage>();
services.AddTransient<BillingQueriesPage>();
// ViewModels
services.AddSingleton(settings);

View file

@ -4,7 +4,9 @@ using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Microsoft.Extensions.DependencyInjection;
using PostIt.ViewModels;
using PostIt.ViewModels.Commands;
using PostIt.Views;
using PostIt.Views.Commands;
namespace PostIt;
@ -41,8 +43,7 @@ public class ViewLocator : IDataTemplate
HomePageViewModel => services.GetRequiredService<HomePage>(),
ActivitiesPageViewModel => services.GetRequiredService<ActivitiesPage>(),
CommandFormsPageViewModel => services.GetRequiredService<CommandFormsPage>(),
BillingCommandPageViewModel => services.GetRequiredService<BillingCommandPage>(),
BillingQueriesPageViewModel => services.GetRequiredService<BillingQueriesPage>(),
RdvViewModel => services.GetRequiredService<RdvPage>(),
SignaturePageViewModel => services.GetRequiredService<SignaturePage>(),
AddCircleMemberDialogViewModel => services.GetRequiredService<AddCircleMemberDialog>(),
CirclesPageViewModel => services.GetRequiredService<CirclesPage>(),

View file

@ -1,490 +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 PostIt.Services;
using Yavsc;
using Yavsc.Abstract.Workflow;
using Yavsc.Api.Client;
using Yavsc.Models.Billing;
using Yavsc.Models.Haircut;
namespace PostIt.ViewModels;
public partial class BillingCommandPageViewModel : ViewModelBase
{
private readonly BillingApiClient _billingClient;
public ActivityInfo Activity { get; }
public ActivityUserDisplayItem Performer { get; }
public CommandFormSummary 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 bool CanUseCurrentLocation => IsSupported && !IsBusy;
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(
ActivityInfo activity,
ActivityUserDisplayItem performer,
CommandFormSummary 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));
}
partial void OnIsBusyChanged(bool value)
{
OnPropertyChanged(nameof(CanUseCurrentLocation));
UseCurrentLocationCommand.NotifyCanExecuteChanged();
}
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 (!TryParseCoordinates(out var latitude, out var longitude, out var coordinateError))
{
StatusMessage = coordinateError;
return;
}
IsBusy = true;
try
{
var address = Address.Trim();
var locationPayload = BuildLocationPayload(address, latitude, 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 = address,
Latitude = latitude,
Longitude = 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 = locationPayload,
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 = locationPayload,
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 = locationPayload,
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;
}
}
[RelayCommand(CanExecute = nameof(CanUseCurrentLocation))]
private async Task UseCurrentLocationAsync()
{
if (!CanUseCurrentLocation)
{
return;
}
IsBusy = true;
try
{
var result = await Platform.TryGetCurrentLocationAsync(default).ConfigureAwait(true);
if (!result.IsSuccess || !result.Latitude.HasValue || !result.Longitude.HasValue)
{
StatusMessage = result.Message;
return;
}
LatitudeText = result.Latitude.Value.ToString(CultureInfo.InvariantCulture);
LongitudeText = result.Longitude.Value.ToString(CultureInfo.InvariantCulture);
StatusMessage = string.IsNullOrWhiteSpace(Address)
? "Position récupérée. Complétez l'adresse puis envoyez la commande."
: result.Message;
}
catch (OperationCanceledException)
{
StatusMessage = "La récupération de la position a été annulée.";
}
catch (Exception ex)
{
StatusMessage = $"Impossible de récupérer la position: {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) ?? string.Empty;
LongitudeText = existingQuery.Location.Longitude?.ToString(CultureInfo.InvariantCulture) ?? string.Empty;
}
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);
}
private static object BuildLocationPayload(string address, double? latitude, double? longitude)
{
if (latitude.HasValue && longitude.HasValue)
{
return new
{
Address = address,
Latitude = latitude.Value,
Longitude = longitude.Value,
};
}
return new
{
Address = address,
};
}
private bool TryParseCoordinates(out double? latitude, out double? longitude, out string error)
{
latitude = null;
longitude = null;
error = string.Empty;
var latitudeMissing = string.IsNullOrWhiteSpace(LatitudeText);
var longitudeMissing = string.IsNullOrWhiteSpace(LongitudeText);
if (latitudeMissing && longitudeMissing)
{
return true;
}
if (latitudeMissing != longitudeMissing)
{
error = "Latitude et longitude doivent être renseignées ensemble, ou laissées vides toutes les deux.";
return false;
}
if (!TryParseCoordinate(LatitudeText, out var parsedLatitude))
{
error = "Latitude invalide.";
return false;
}
if (!TryParseCoordinate(LongitudeText, out var parsedLongitude))
{
error = "Longitude invalide.";
return false;
}
latitude = parsedLatitude;
longitude = parsedLongitude;
return true;
}
}

View file

@ -133,7 +133,7 @@ public partial class BillingQueriesPageViewModel : ViewModelBase
try
{
var details = await _billingClient.GetQueryAsync(Form.ActionName, SelectedQuery.Id).ConfigureAwait(true);
var vm = new BillingCommandPageViewModel(Activity, Performer, Form, _billingClient);
var vm = Form.CreateCommandPageViewModel(Activity, Performer, _billingClient);
await vm.InitializeAsync(details).ConfigureAwait(true);
await app.PushPageAsync(vm).ConfigureAwait(true);
}

View file

@ -81,7 +81,8 @@ public partial class CommandFormsPageViewModel : ViewModelBase
throw new InvalidOperationException("Application PostIt indisponible.");
}
var vm = SelectedForm.CreateCommandPageViewModel(Activity, Performer, _billingClient);
var vm = SelectedForm.CreateCommandPageViewModel(
Activity, Performer, _billingClient);
await vm.InitializeAsync();
await app.PushPageAsync(vm);
}

View file

@ -0,0 +1,121 @@
using System;
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;
namespace PostIt.ViewModels;
public abstract partial class BillingCommandPageViewModel : ViewModelBase
{
protected readonly BillingApiClient _billingClient;
public ActivityInfo Activity { get; }
public ActivityUserDisplayItem Performer { get; }
public CommandFormSummary Form { get; }
[ObservableProperty]
public partial bool IsBusy { get; set; }
[ObservableProperty]
public partial string StatusMessage { get; set; }
[ObservableProperty]
public partial string Reason { get; set; } = string.Empty;
[ObservableProperty]
public partial bool Consent { get; set; } = true;
[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 bool CanUseCurrentLocation => IsSupported && !IsBusy;
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(
ActivityInfo activity,
ActivityUserDisplayItem performer,
CommandFormSummary 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));
StatusMessage = SupportMessage;
}
partial void OnExistingQueryIdChanged(long? value)
{
OnPropertyChanged(nameof(IsEditingExisting));
OnPropertyChanged(nameof(SubmitLabel));
}
partial void OnIsBusyChanged(bool value)
{
OnPropertyChanged(nameof(CanUseCurrentLocation));
}
public async Task InitializeAsync(BillingQueryDetailsDto? existingQuery = null)
{
if (existingQuery is not null)
{
ApplyExistingQuery(existingQuery);
return;
}
}
protected abstract void ApplyExistingQuery(BillingQueryDetailsDto existingQuery);
[RelayCommand]
protected abstract Task SubmitAsync();
}

View file

@ -0,0 +1,182 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Yavsc.Abstract.Workflow;
using Yavsc.Api.Client;
using Yavsc.Models.Haircut;
namespace PostIt.ViewModels.Commands;
public partial class BrushViewModel : RdvViewModel
{
[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();
public BrushViewModel(ActivityInfo activity, ActivityUserDisplayItem performer, CommandFormSummary form, BillingApiClient billingClient)
: base(activity, performer, form, billingClient)
{
}
protected override async void ApplyExistingQuery(BillingQueryDetailsDto existingQuery)
{
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);
}
}
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;
}
}
protected override async Task SubmitAsync()
{
IsBusy = true;
try
{
var address = Address.Trim();
var locationPayload = BuildLocationPayload(address, Latitude, 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 = address,
Latitude = Latitude,
Longitude = Longitude,
}
};
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 = locationPayload,
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 = locationPayload,
Prestations = selectedPrestations.Select(x => new { PrestationId = x.Id }).ToList(),
Status = payload.Status,
}).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'envoi de la commande: {ex.Message}";
}
finally
{
IsBusy = false;
}
}
}

View file

@ -0,0 +1,16 @@
using PostIt.ViewModels;
namespace PostIt.ViewModels.Comands;
public class MBrushViewModel : BillingCommandPageViewModel
{
public MBrushViewModel(ActivityInfo activity, ActivityUserDisplayItem performer, CommandFormSummary form, BillingApiClient billingClient)
: base(activity, performer, form, billingClient)
{
}
protected override void ApplyExistingQuery(BillingQueryDetailsDto existingQuery)
{
throw new System.NotImplementedException();
}
}

View file

@ -0,0 +1,210 @@
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using PostIt.Services;
using Yavsc.Abstract.Workflow;
using Yavsc.Api.Client;
namespace PostIt.ViewModels.Commands;
public partial class RdvViewModel : BillingCommandPageViewModel
{
[ObservableProperty]
public partial string Address { get; set; } = string.Empty;
[ObservableProperty]
public partial double Latitude { get; set; }
[ObservableProperty]
public partial double Longitude { get; set; }
[ObservableProperty]
public partial DateTime EventDate { get; set; }
public RdvViewModel(ActivityInfo activity, ActivityUserDisplayItem performer, CommandFormSummary form, BillingApiClient billingClient)
: base(activity, performer, form, billingClient)
{
EventDate = DateTime.Now.AddDays(1);
}
protected override 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)
{
EventDate = existingQuery.EventDate.Value
.ToLocalTime();
}
if (existingQuery.Location is not null)
{
Address = existingQuery.Location.Address ?? string.Empty;
Latitude = existingQuery.Location.Latitude ?? 0;
Longitude = existingQuery.Location.Longitude ?? 0;
}
StatusMessage = $"Commande #{existingQuery.Id} chargée.";
}
[RelayCommand(CanExecute = nameof(CanUseCurrentLocation))]
private async Task UseCurrentLocationAsync()
{
if (!CanUseCurrentLocation)
{
return;
}
IsBusy = true;
try
{
var result = await Platform.TryGetCurrentLocationAsync(default).ConfigureAwait(true);
if (!result.IsSuccess || !result.Latitude.HasValue || !result.Longitude.HasValue)
{
StatusMessage = result.Message;
return;
}
Latitude = result.Latitude.Value;
Longitude = result.Longitude.Value;
StatusMessage = string.IsNullOrWhiteSpace(Address)
? "Position récupérée. Complétez l'adresse puis envoyez la commande."
: result.Message;
}
catch (OperationCanceledException)
{
StatusMessage = "La récupération de la position a été annulée.";
}
catch (Exception ex)
{
StatusMessage = $"Impossible de récupérer la position: {ex.Message}";
}
finally
{
IsBusy = false;
}
}
protected static object BuildLocationPayload(string address, double? latitude, double? longitude)
{
if (latitude.HasValue && longitude.HasValue)
{
return new
{
Address = address,
Latitude = latitude.Value,
Longitude = longitude.Value,
};
}
return new
{
Address = address,
};
}
protected override async Task SubmitAsync()
{
if (!IsSupported)
{
StatusMessage = SupportMessage;
return;
}
if (!Consent)
{
StatusMessage = "Le consentement est requis pour poster la commande.";
return;
}
if (string.IsNullOrWhiteSpace(Address))
{
StatusMessage = "L'adresse du rendez-vous est requise.";
return;
}
if (IsRdv && string.IsNullOrWhiteSpace(Reason))
{
StatusMessage = "Le motif du rendez-vous est requis.";
return;
}
IsBusy = true;
try
{
var address = Address.Trim();
var locationPayload = BuildLocationPayload(address, Latitude, 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 = address,
Latitude = Latitude,
Longitude = 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 = locationPayload,
Reason = payload.Reason,
Status = payload.Status,
}).ConfigureAwait(true);
}
}
else
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;
}
}
}

View file

@ -2,7 +2,7 @@
namespace PostIt.ViewModels;
public abstract partial class ViewModelBase : ObservableObject
public abstract class ViewModelBase : ObservableObject
{
/// <summary>
/// Gets if the user can navigate to the next page

View file

@ -32,7 +32,7 @@
ItemsSource="{Binding Activities}"
SelectedItem="{Binding SelectedActivity, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="wf:ActivityBrowseItemDto">
<DataTemplate x:DataType="wf:ActivityInfo">
<StackPanel Spacing="2" Margin="0,0,0,8">
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" FontSize="11" Opacity="0.7" />
@ -53,7 +53,7 @@
ItemsSource="{Binding Specializations}"
SelectedItem="{Binding SelectedSpecialization, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="wf:ActivityBrowseItemDto">
<DataTemplate x:DataType="wf:ActivityInfo">
<StackPanel Spacing="2" Margin="0,0,0,8">
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" FontSize="11" Opacity="0.7" />

View file

@ -1,138 +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,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" />
<Button Grid.Row="7"
Grid.ColumnSpan="2"
HorizontalAlignment="Left"
Margin="0,0,0,8"
Content="Utiliser ma position"
Command="{Binding UseCurrentLocationCommand}" />
<TextBlock Grid.Row="8" Text="Latitude" VerticalAlignment="Center" Margin="0,0,12,8" />
<TextBox Grid.Row="8" Grid.Column="1" Text="{Binding LatitudeText, Mode=TwoWay}" PlaceholderText="Optionnel" Margin="0,0,0,8" />
<TextBlock Grid.Row="9" Text="Longitude" VerticalAlignment="Center" Margin="0,0,12,8" />
<TextBox Grid.Row="9" Grid.Column="1" Text="{Binding LongitudeText, Mode=TwoWay}" PlaceholderText="Optionnel" Margin="0,0,0,8" />
<TextBlock Grid.Row="10"
Text="Prestation"
VerticalAlignment="Center"
Margin="0,0,12,8"
IsVisible="{Binding ShowsSinglePrestation}" />
<ComboBox Grid.Row="10"
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="11"
Text="Prestations"
VerticalAlignment="Top"
Margin="0,0,12,8"
IsVisible="{Binding ShowsMultiplePrestations}" />
<ListBox Grid.Row="11"
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="12"
Text="Informations"
VerticalAlignment="Center"
Margin="0,0,12,8"
IsVisible="{Binding ShowsAdditionalInfo}" />
<TextBox Grid.Row="12"
Grid.Column="1"
Text="{Binding AdditionalInfo, Mode=TwoWay}"
Margin="0,0,0,8"
IsVisible="{Binding ShowsAdditionalInfo}" />
<CheckBox Grid.Row="13" 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="14" 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>

View file

@ -20,7 +20,7 @@
ItemsSource="{Binding Forms}"
SelectedItem="{Binding SelectedForm, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="wf:CommandFormSummaryDto">
<DataTemplate x:DataType="wf:CommandFormSummary">
<StackPanel Spacing="2" Margin="0,0,0,10">
<TextBlock Text="{Binding Title}" FontWeight="Bold" />
<TextBlock Text="{Binding ActionName, StringFormat='Route billing : /billing/{0}'}"
@ -53,4 +53,4 @@
VerticalAlignment="Center" />
</Grid>
</Grid>
</ContentPage>
</ContentPage>

View file

@ -0,0 +1,142 @@
<ContentPage xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:cvm="using:PostIt.ViewModels.Commands"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="PostIt.Views.Commands.BrushPage"
x:DataType="cvm:BrushViewModel"
Header="Commande billing">
<ScrollViewer>
<Grid RowDefinitions="Auto,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" />
<DatePicker Grid.Row="4" Grid.Column="1" DayFormat="ddd dd" SelectedDate="{Binding EventDate, 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" />
<Button Grid.Row="7"
Grid.ColumnSpan="2"
HorizontalAlignment="Left"
Margin="0,0,0,8"
Content="Utiliser ma position"
Command="{Binding UseCurrentLocationCommand}" />
<TextBlock Grid.Row="8" Text="Latitude" VerticalAlignment="Center" Margin="0,0,12,8" />
<TextBox Grid.Row="8" Grid.Column="1" Text="{Binding Latitude, Mode=TwoWay}" PlaceholderText="Optionnel" Margin="0,0,0,8" />
<TextBlock Grid.Row="9" Text="Longitude" VerticalAlignment="Center" Margin="0,0,12,8" />
<TextBox Grid.Row="9" Grid.Column="1" Text="{Binding Longitude, Mode=TwoWay}" PlaceholderText="Optionnel" Margin="0,0,0,8" />
<TextBlock Grid.Row="10"
Text="Prestation"
VerticalAlignment="Center"
Margin="0,0,12,8"
IsVisible="{Binding ShowsSinglePrestation}" />
<ComboBox Grid.Row="10"
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="11"
Text="Prestations"
VerticalAlignment="Top"
Margin="0,0,12,8"
IsVisible="{Binding ShowsMultiplePrestations}" />
<ListBox Grid.Row="11"
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="12"
Text="Informations"
VerticalAlignment="Center"
Margin="0,0,12,8"
IsVisible="{Binding ShowsAdditionalInfo}" />
<TextBox Grid.Row="12"
Grid.Column="1"
Text="{Binding AdditionalInfo, Mode=TwoWay}"
Margin="0,0,0,8"
IsVisible="{Binding ShowsAdditionalInfo}" />
<CheckBox Grid.Row="13" 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="14" 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>

View file

@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace PostIt.Views.Commands;
public partial class BrushPage : ContentPage
{
public BrushPage()
{
InitializeComponent();
}
}

View file

@ -0,0 +1,95 @@
<ContentPage xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cvm="using:PostIt.ViewModels.Commands"
x:Class="PostIt.Views.Commands.RdvPage"
x:DataType="cvm:RdvViewModel"
Header="Commande billing">
<ScrollViewer>
<Grid RowDefinitions="Auto,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" />
<DatePicker Grid.Row="4" Grid.Column="1" DayFormat="ddd dd" SelectedDate="{Binding EventDate, 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" />
<Button Grid.Row="7"
Grid.ColumnSpan="2"
HorizontalAlignment="Left"
Margin="0,0,0,8"
Content="Utiliser ma position"
Command="{Binding UseCurrentLocationCommand}" />
<TextBlock Grid.Row="8" Text="Latitude" VerticalAlignment="Center" Margin="0,0,12,8" />
<TextBox Grid.Row="8" Grid.Column="1" Text="{Binding Latitude, Mode=TwoWay}" PlaceholderText="Optionnel" Margin="0,0,0,8" />
<TextBlock Grid.Row="9" Text="Longitude" VerticalAlignment="Center" Margin="0,0,12,8" />
<TextBox Grid.Row="9" Grid.Column="1" Text="{Binding Longitude, Mode=TwoWay}" PlaceholderText="Optionnel" Margin="0,0,0,8" />
<TextBlock Grid.Row="12"
Text="Informations"
VerticalAlignment="Center"
Margin="0,0,12,8"
IsVisible="{Binding ShowsAdditionalInfo}" />
<TextBox Grid.Row="12"
Grid.Column="1"
Text="{Binding AdditionalInfo, Mode=TwoWay}"
Margin="0,0,0,8"
IsVisible="{Binding ShowsAdditionalInfo}" />
<CheckBox Grid.Row="13" 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="14" 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>

View file

@ -1,11 +1,11 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace PostIt.Views;
namespace PostIt.Views.Commands;
public partial class BillingCommandPage : ContentPage
public partial class RdvPage : ContentPage
{
public BillingCommandPage()
public RdvPage()
{
InitializeComponent();
}
@ -14,4 +14,4 @@ public partial class BillingCommandPage : ContentPage
{
AvaloniaXamlLoader.Load(this);
}
}
}