Compare commits
3 commits
914e9486b5
...
6b4e52e7ce
| Author | SHA1 | Date | |
|---|---|---|---|
|
6b4e52e7ce |
|||
|
fc820362b9 |
|||
|
2a36c19610 |
38 changed files with 1313 additions and 800 deletions
2
.vscode/tasks.json
vendored
2
.vscode/tasks.json
vendored
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
20
doc/agent-intent-matrix.md
Normal file
20
doc/agent-intent-matrix.md
Normal 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.
|
||||
101
doc/agent-playbook.md
Normal file
101
doc/agent-playbook.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# Playbook d'usage des agents IA (Yavsc)
|
||||
|
||||
Ce playbook normalise l'usage de Copilot, Plan et Explore dans le depot.
|
||||
Il privilegie des sorties verifiables: fichiers, commandes tests, risques.
|
||||
|
||||
## Quand utiliser quel agent
|
||||
|
||||
- Plan: quand la tache est ambigue, transverse ou risquee.
|
||||
- Explore: quand il faut cartographier rapidement des zones du code.
|
||||
- Copilot: quand les specifications sont claires et localisees.
|
||||
|
||||
## Prompt type (base)
|
||||
|
||||
Utiliser ce squelette avant toute tache non triviale:
|
||||
|
||||
```text
|
||||
Contexte: <projet/fichier/fonction>
|
||||
Objectif: <resultat observable>
|
||||
Contraintes: <style, archi, perimetre>
|
||||
Verification: <tests exacts a lancer>
|
||||
Sortie attendue: <fichiers modifies + risques>
|
||||
```
|
||||
|
||||
## 4 scenarios de reference
|
||||
|
||||
## 1) Explorer un bounded context
|
||||
|
||||
Intention:
|
||||
- Comprendre ou implementer un changement dans un BC sans regression laterale.
|
||||
|
||||
Prompt minimal:
|
||||
```text
|
||||
Explore le BC <nom> avec profondeur medium.
|
||||
Retour: composants touches, points d'entree, tests existants et risques.
|
||||
```
|
||||
|
||||
Preuves attendues:
|
||||
- Carte des fichiers a modifier.
|
||||
- Test(s) smoke/mandatory proposes.
|
||||
|
||||
## 2) Ajouter un smoke test
|
||||
|
||||
Intention:
|
||||
- Couvrir rapidement un endpoint ou une route publique.
|
||||
|
||||
Prompt minimal:
|
||||
```text
|
||||
Propose un smoke test pour <route/endpoint> dans le projet de test approprie.
|
||||
Respecte les conventions de doc/testing.md.
|
||||
```
|
||||
|
||||
Preuves attendues:
|
||||
- Fichier test cree/modifie.
|
||||
- Commande precise pour executer le test cible.
|
||||
|
||||
## 3) Corriger une regression backend API
|
||||
|
||||
Intention:
|
||||
- Corriger un bug sans casser un flux voisin.
|
||||
|
||||
Prompt minimal:
|
||||
```text
|
||||
Planifie puis implemente un fix de <symptome> dans <projet>.
|
||||
Ajoute/ajuste un test NonRegression rouge puis vert.
|
||||
```
|
||||
|
||||
Preuves attendues:
|
||||
- Explication cause racine.
|
||||
- Test non-regression associe.
|
||||
- Commande d'execution et resultat attendu.
|
||||
|
||||
## 4) Tracer un flux PostIt/OIDC
|
||||
|
||||
Intention:
|
||||
- Localiser une cassure d'authentification entre client et serveur.
|
||||
|
||||
Prompt minimal:
|
||||
```text
|
||||
Cartographie le flux OIDC PostIt: entrypoints, callback, stockage token,
|
||||
refresh. Donne points de rupture probables et tests/verification proposes.
|
||||
```
|
||||
|
||||
Preuves attendues:
|
||||
- Liste ordonnee des etapes du flux.
|
||||
- Fichiers critiques.
|
||||
- Hypotheses testables.
|
||||
|
||||
## Anti-patterns a eviter
|
||||
|
||||
- Prompt sans objectif verifiable.
|
||||
- Demande trop large sans perimetre de fichiers.
|
||||
- Validation basee uniquement sur "ca semble correct".
|
||||
- Pas de lien entre changement et niveau de test.
|
||||
|
||||
## Gate PR minimale (agent-assiste)
|
||||
|
||||
Avant validation:
|
||||
- Impact architecture explicite.
|
||||
- Rationale de choix agent explicite.
|
||||
- Test(s) executes et justifies.
|
||||
- Risques residuels documentes.
|
||||
73
doc/onboarding-agents.md
Normal file
73
doc/onboarding-agents.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Onboarding guide: agents IA + architecture + tests
|
||||
|
||||
Ce guide est optimise pour accelerer la prise en main des agents IA
|
||||
(Copilot, Plan, Explore) dans Yavsc, avec une verification rapide
|
||||
par les tests.
|
||||
|
||||
## Resultat attendu
|
||||
|
||||
A la fin du parcours, un contributeur doit pouvoir:
|
||||
- Identifier les projets impactes par une modification.
|
||||
- Choisir l'agent adapte a l'intention de travail.
|
||||
- Produire une proposition de changement verifiable par les tests.
|
||||
|
||||
## Parcours en 3 modules
|
||||
|
||||
## Module A - Comprendre le terrain (30-45 min)
|
||||
|
||||
Objectif: acquerir une lecture fiable de l'architecture.
|
||||
|
||||
1. Lire [README.md](../README.md) puis [Architecture.md](Architecture.md).
|
||||
2. Lire [architecture/decoupage-organisation.md](architecture/decoupage-organisation.md).
|
||||
3. Selon le domaine:
|
||||
- Backend/API: [architecture/workflow-multi-parties.md](architecture/workflow-multi-parties.md)
|
||||
- PostIt: [architecture/postit.md](architecture/postit.md) puis [architecture/postit-oidc.md](architecture/postit-oidc.md)
|
||||
|
||||
Definition of done:
|
||||
- Expliquer en 5 phrases quelles couches sont touchees.
|
||||
- Citer le ou les points d'entree applicatifs a verifier.
|
||||
|
||||
## Module B - Boucle tests rapide (20-30 min)
|
||||
|
||||
Objectif: verifier rapidement sans lancer toute la suite.
|
||||
|
||||
1. Lire [testing.md](testing.md).
|
||||
2. Lancer les smoke tests d'abord, puis mandatory selon le projet.
|
||||
3. N'elargir au test complet que si le scope depasse le BC touche.
|
||||
|
||||
Definition of done:
|
||||
- Fournir la commande test executee.
|
||||
- Expliquer pourquoi ce niveau de test est suffisant.
|
||||
|
||||
## Module C - Usage agentique en production (30-40 min)
|
||||
|
||||
Objectif: utiliser les agents comme accelerateurs, pas comme boites noires.
|
||||
|
||||
1. Plan: decomposer la tache en etapes verifiables.
|
||||
2. Explore: collecter le contexte code/doc precise.
|
||||
3. Copilot: implementer localement et verifier.
|
||||
|
||||
Regles:
|
||||
- Toujours donner un contexte explicite (fichier, but, contrainte).
|
||||
- Demander des preuves observables (fichiers modifies, tests, risques).
|
||||
- Refuser toute sortie non verifiable.
|
||||
|
||||
Definition of done:
|
||||
- Une tache simple est livree avec:
|
||||
- Plan
|
||||
- Changement local
|
||||
- Preuve par test
|
||||
|
||||
## Routine continue (sans echeance fixe)
|
||||
|
||||
Rituels recommandes:
|
||||
- Hebdo: revue des prompts qui ont bien fonctionne.
|
||||
- Mensuel: mise a jour du present guide et du playbook.
|
||||
- A chaque incident: ajouter un anti-pattern dans le playbook.
|
||||
|
||||
## Check-list de validation
|
||||
|
||||
- Le changement indique son impact architecture.
|
||||
- Le choix de l'agent est justifie.
|
||||
- La preuve test est incluse.
|
||||
- Les risques residuels sont explicitement listes.
|
||||
|
|
@ -72,9 +72,9 @@ public class ActivitiesPageViewModelTests
|
|||
{
|
||||
Paths.Add(path);
|
||||
|
||||
if (typeof(T) == typeof(List<ActivityBrowseItemDto>))
|
||||
if (typeof(T) == typeof(List<ActivityInfo>))
|
||||
{
|
||||
var activities = new List<ActivityBrowseItemDto>
|
||||
var activities = new List<ActivityInfo>
|
||||
{
|
||||
new()
|
||||
{
|
||||
|
|
@ -82,11 +82,11 @@ public class ActivitiesPageViewModelTests
|
|||
Name = "Brush",
|
||||
Description = "Coiffure à domicile",
|
||||
PerformerCount = 1,
|
||||
Forms = new List<CommandFormSummaryDto>
|
||||
Forms = new List<CommandFormSummary>
|
||||
{
|
||||
new() { Id = 1, ActionName = "Rdv", Title = "Rendez-vous" }
|
||||
},
|
||||
Children = new List<ActivityBrowseItemDto>
|
||||
Children = new List<ActivityInfo>
|
||||
{
|
||||
new()
|
||||
{
|
||||
|
|
@ -95,7 +95,7 @@ public class ActivitiesPageViewModelTests
|
|||
Description = "Spécialisation premium",
|
||||
ParentCode = "brush",
|
||||
PerformerCount = 1,
|
||||
Forms = new List<CommandFormSummaryDto>
|
||||
Forms = new List<CommandFormSummary>
|
||||
{
|
||||
new() { Id = 2, ActionName = "Rdv", Title = "Rendez-vous premium" }
|
||||
}
|
||||
|
|
@ -106,14 +106,14 @@ public class ActivitiesPageViewModelTests
|
|||
return Task.FromResult((T)(object)activities);
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(List<ActivityPerformerDto>))
|
||||
if (typeof(T) == typeof(List<PerformerActivity>))
|
||||
{
|
||||
var performers = path.EndsWith("brush-pro/users", StringComparison.Ordinal)
|
||||
? new List<ActivityPerformerDto>
|
||||
? new List<PerformerActivity>
|
||||
{
|
||||
new() { PerformerId = "pro-2", HasPerformerProfile = true, Active = false, UserName = "Bob", ActivityCode = "brush-pro", ActivityName = "Brush Pro", ExtraActivityCount = 2 }
|
||||
}
|
||||
: new List<ActivityPerformerDto>
|
||||
: new List<PerformerActivity>
|
||||
{
|
||||
new() { PerformerId = "pro-1", HasPerformerProfile = true, Active = true, UserName = "Alice", ActivityCode = "brush", ActivityName = "Brush", ExtraActivityCount = 0 }
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
new ActivityBrowseItemDto { Code = "dev", Name = "Développement" },
|
||||
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 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,
|
||||
};
|
||||
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,16 +48,15 @@ public class BillingCommandPageViewModelTests
|
|||
{
|
||||
var api = new RecordingApi();
|
||||
var client = new BillingApiClient(api, "https://business.example/api/v1/");
|
||||
var vm = new BillingCommandPageViewModel(
|
||||
new ActivityBrowseItemDto { Code = "book", Name = "Book" },
|
||||
|
||||
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 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);
|
||||
Assert.Null(vm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -64,19 +64,19 @@ public class BillingCommandPageViewModelTests
|
|||
{
|
||||
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" },
|
||||
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 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 = 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 = null;
|
||||
vm!.Longitude = null;
|
||||
vm!.Consent = true;
|
||||
|
||||
await vm.SubmitCommand.ExecuteAsync(null);
|
||||
|
||||
|
|
@ -97,16 +97,17 @@ public class BillingCommandPageViewModelTests
|
|||
|
||||
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" },
|
||||
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 CommandFormSummaryDto { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" },
|
||||
client);
|
||||
client) as RdvViewModel;
|
||||
|
||||
await vm.UseCurrentLocationCommand.ExecuteAsync(null);
|
||||
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 +127,18 @@ public class BillingCommandPageViewModelTests
|
|||
}
|
||||
};
|
||||
var client = new BillingApiClient(api, "https://business.example/api/v1/");
|
||||
var vm = new BillingCommandPageViewModel(
|
||||
new ActivityBrowseItemDto { Code = "brush", Name = "Brush" },
|
||||
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 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",
|
||||
};
|
||||
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,23 +162,22 @@ public class BillingCommandPageViewModelTests
|
|||
}
|
||||
};
|
||||
var client = new BillingApiClient(api, "https://business.example/api/v1/");
|
||||
var vm = new BillingCommandPageViewModel(
|
||||
new ActivityBrowseItemDto { Code = "mbrush", Name = "MBrush" },
|
||||
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 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,
|
||||
};
|
||||
client) as MBrushViewModel;
|
||||
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;
|
||||
vm.MultiPrestations[1].IsSelected = true;
|
||||
await vm.SubmitCommand.ExecuteAsync(null);
|
||||
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));
|
||||
|
|
@ -200,13 +199,13 @@ public class BillingCommandPageViewModelTests
|
|||
}
|
||||
};
|
||||
var client = new BillingApiClient(api, "https://business.example/api/v1/");
|
||||
var vm = new BillingCommandPageViewModel(
|
||||
new ActivityBrowseItemDto { Code = "brush", Name = "Brush" },
|
||||
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 CommandFormSummaryDto { Id = 13, ActionName = "Brush", Title = "Coupe" },
|
||||
client);
|
||||
|
||||
await vm.InitializeAsync(new BillingQueryDetailsDto
|
||||
client) as BrushViewModel;
|
||||
await vm!.InitializeAsync(new BillingQueryDetailsDto
|
||||
{
|
||||
Id = 77,
|
||||
BillingCode = "Brush",
|
||||
|
|
@ -226,9 +225,9 @@ public class BillingCommandPageViewModelTests
|
|||
}
|
||||
});
|
||||
|
||||
vm.SelectedPrestation = vm.AvailablePrestations[1];
|
||||
vm.AdditionalInfo = "Note mise à jour";
|
||||
await vm.SubmitCommand.ExecuteAsync(null);
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ public class BillingQueriesPageViewModelTests
|
|||
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 ActivityInfo { Code = "dev", Name = "Développement" },
|
||||
new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" },
|
||||
new CommandFormSummaryDto { Id = 1, ActionName = "Rdv", Title = "Rendez-vous" },
|
||||
new CommandFormSummary { Id = 1, ActionName = "Rdv", Title = "Rendez-vous" },
|
||||
client);
|
||||
|
||||
await vm.InitializeAsync();
|
||||
|
|
@ -33,9 +33,9 @@ public class BillingQueriesPageViewModelTests
|
|||
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 ActivityInfo { Code = "dev", Name = "Développement" },
|
||||
new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" },
|
||||
new CommandFormSummaryDto { Id = 1, ActionName = "Rdv", Title = "Rendez-vous" },
|
||||
new CommandFormSummary { Id = 1, ActionName = "Rdv", Title = "Rendez-vous" },
|
||||
client,
|
||||
isReadOnly: true,
|
||||
ongoingOnly: true);
|
||||
|
|
@ -131,4 +131,4 @@ public class BillingQueriesPageViewModelTests
|
|||
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
50
src/PostIt/PostIt/Helpers/FormHelpers.cs
Normal file
50
src/PostIt/PostIt/Helpers/FormHelpers.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
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 namespacePrefix = typeof(PostIt.ViewModels.Commands.RdvViewModel).Namespace + ".";
|
||||
|
||||
string formVMName = form.ActionName + "ViewModel";
|
||||
|
||||
string formOnActivityVMName = activity.Code + formVMName + "ViewModel";
|
||||
|
||||
var vmType = Type.GetType(namespacePrefix +formOnActivityVMName);
|
||||
if (vmType == null)
|
||||
{
|
||||
vmType = Type.GetType(namespacePrefix + formVMName);
|
||||
}
|
||||
if (vmType == null)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"! Cannot find type '{formOnActivityVMName}' or '{formVMName}'");
|
||||
return null;
|
||||
}
|
||||
if (!typeof(BillingCommandPageViewModel).IsAssignableFrom(vmType))
|
||||
{
|
||||
Console.Error.WriteLine($"! The type '{formOnActivityVMName}' or '{formVMName}' is not a BillingCommandPageViewModel");
|
||||
return null;
|
||||
}
|
||||
|
||||
var vm = Activator.CreateInstance(vmType, activity, performer, form, billingClient);
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,8 @@ public class ViewLocator : IDataTemplate
|
|||
HomePageViewModel => services.GetRequiredService<HomePage>(),
|
||||
ActivitiesPageViewModel => services.GetRequiredService<ActivitiesPage>(),
|
||||
CommandFormsPageViewModel => services.GetRequiredService<CommandFormsPage>(),
|
||||
BillingCommandPageViewModel => services.GetRequiredService<BillingCommandPage>(),
|
||||
BillingQueriesPageViewModel => services.GetRequiredService<BillingQueriesPage>(),
|
||||
BrushViewModel => services.GetRequiredService<BrushPage>(),
|
||||
RdvViewModel => services.GetRequiredService<RdvPage>(),
|
||||
SignaturePageViewModel => services.GetRequiredService<SignaturePage>(),
|
||||
AddCircleMemberDialogViewModel => services.GetRequiredService<AddCircleMemberDialog>(),
|
||||
CirclesPageViewModel => services.GetRequiredService<CirclesPage>(),
|
||||
|
|
|
|||
|
|
@ -20,16 +20,16 @@ public partial class ActivitiesPageViewModel : ViewModelBase
|
|||
private bool _syncingSelection;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<ActivityBrowseItemDto> Activities { get; set; } = new();
|
||||
public partial ObservableCollection<ActivityInfo> Activities { get; set; } = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ActivityBrowseItemDto? SelectedActivity { get; set; }
|
||||
public partial ActivityInfo? SelectedActivity { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<ActivityBrowseItemDto> Specializations { get; set; } = new();
|
||||
public partial ObservableCollection<ActivityInfo> Specializations { get; set; } = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ActivityBrowseItemDto? SelectedSpecialization { get; set; }
|
||||
public partial ActivityInfo? SelectedSpecialization { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<ActivityUserDisplayItem> Performers { get; set; } = new();
|
||||
|
|
@ -43,7 +43,7 @@ public partial class ActivitiesPageViewModel : ViewModelBase
|
|||
[ObservableProperty]
|
||||
public partial string StatusMessage { get; set; } = "Choisissez une activité.";
|
||||
|
||||
public ActivityBrowseItemDto? CurrentActivity => SelectedSpecialization ?? SelectedActivity;
|
||||
public ActivityInfo? CurrentActivity => SelectedSpecialization ?? SelectedActivity;
|
||||
public string SelectedActivityLabel => SelectedActivity?.Name ?? "(aucune activité)";
|
||||
public string CurrentActivityLabel => CurrentActivity?.Name ?? "(aucune)";
|
||||
public int CurrentFormCount => CurrentActivity?.Forms?.Count ?? 0;
|
||||
|
|
@ -66,19 +66,19 @@ public partial class ActivitiesPageViewModel : ViewModelBase
|
|||
_billingClient = billingClient ?? throw new ArgumentNullException(nameof(billingClient));
|
||||
}
|
||||
|
||||
partial void OnSelectedActivityChanged(ActivityBrowseItemDto? value)
|
||||
partial void OnSelectedActivityChanged(ActivityInfo? value)
|
||||
{
|
||||
if (_syncingSelection) return;
|
||||
_ = ShowActivitySafeAsync(value);
|
||||
}
|
||||
|
||||
partial void OnSelectedSpecializationChanged(ActivityBrowseItemDto? value)
|
||||
partial void OnSelectedSpecializationChanged(ActivityInfo? value)
|
||||
{
|
||||
if (_syncingSelection) return;
|
||||
_ = ShowSpecializationSafeAsync(value);
|
||||
}
|
||||
|
||||
private async Task ShowActivitySafeAsync(ActivityBrowseItemDto? value)
|
||||
private async Task ShowActivitySafeAsync(ActivityInfo? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -94,7 +94,7 @@ public partial class ActivitiesPageViewModel : ViewModelBase
|
|||
}
|
||||
}
|
||||
|
||||
private async Task ShowSpecializationSafeAsync(ActivityBrowseItemDto? value)
|
||||
private async Task ShowSpecializationSafeAsync(ActivityInfo? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -117,7 +117,7 @@ public partial class ActivitiesPageViewModel : ViewModelBase
|
|||
try
|
||||
{
|
||||
var list = await _client.GetCatalogAsync();
|
||||
Activities = new ObservableCollection<ActivityBrowseItemDto>(list ?? new());
|
||||
Activities = new ObservableCollection<ActivityInfo>(list ?? new());
|
||||
|
||||
var first = Activities.FirstOrDefault();
|
||||
await ShowActivityAsync(first);
|
||||
|
|
@ -128,15 +128,15 @@ public partial class ActivitiesPageViewModel : ViewModelBase
|
|||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden)
|
||||
{
|
||||
Activities = new ObservableCollection<ActivityBrowseItemDto>();
|
||||
Specializations = new ObservableCollection<ActivityBrowseItemDto>();
|
||||
Activities = new ObservableCollection<ActivityInfo>();
|
||||
Specializations = new ObservableCollection<ActivityInfo>();
|
||||
Performers = new ObservableCollection<ActivityUserDisplayItem>();
|
||||
StatusMessage = "Accès refusé pour les activités (scope 'api'). Déconnectez puis reconnectez-vous.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Activities = new ObservableCollection<ActivityBrowseItemDto>();
|
||||
Specializations = new ObservableCollection<ActivityBrowseItemDto>();
|
||||
Activities = new ObservableCollection<ActivityInfo>();
|
||||
Specializations = new ObservableCollection<ActivityInfo>();
|
||||
Performers = new ObservableCollection<ActivityUserDisplayItem>();
|
||||
StatusMessage = $"Erreur: {ex.Message}";
|
||||
}
|
||||
|
|
@ -146,7 +146,7 @@ public partial class ActivitiesPageViewModel : ViewModelBase
|
|||
}
|
||||
}
|
||||
|
||||
public async Task ShowActivityAsync(ActivityBrowseItemDto? activity)
|
||||
public async Task ShowActivityAsync(ActivityInfo? activity)
|
||||
{
|
||||
_syncingSelection = true;
|
||||
try
|
||||
|
|
@ -163,7 +163,7 @@ public partial class ActivitiesPageViewModel : ViewModelBase
|
|||
OnPropertyChanged(nameof(SelectedActivityLabel));
|
||||
OnPropertyChanged(nameof(CurrentActivityLabel));
|
||||
OnPropertyChanged(nameof(CurrentFormCount));
|
||||
Specializations = new ObservableCollection<ActivityBrowseItemDto>(activity?.Children ?? new());
|
||||
Specializations = new ObservableCollection<ActivityInfo>(activity?.Children ?? new());
|
||||
|
||||
if (activity is null)
|
||||
{
|
||||
|
|
@ -175,7 +175,7 @@ public partial class ActivitiesPageViewModel : ViewModelBase
|
|||
await LoadPerformersAsync(activity);
|
||||
}
|
||||
|
||||
public async Task ShowSpecializationAsync(ActivityBrowseItemDto? specialization)
|
||||
public async Task ShowSpecializationAsync(ActivityInfo? specialization)
|
||||
{
|
||||
_syncingSelection = true;
|
||||
try
|
||||
|
|
@ -203,7 +203,7 @@ public partial class ActivitiesPageViewModel : ViewModelBase
|
|||
await LoadPerformersAsync(specialization);
|
||||
}
|
||||
|
||||
private async Task LoadPerformersAsync(ActivityBrowseItemDto activity)
|
||||
private async Task LoadPerformersAsync(ActivityInfo activity)
|
||||
{
|
||||
IsBusy = true;
|
||||
try
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ public sealed class ActivityUserDisplayItem
|
|||
public int ExtraActivityCount { get; init; }
|
||||
public string ExtraActivityLabel { get; init; } = "Pas d'autre activité";
|
||||
|
||||
public static ActivityUserDisplayItem FromDto(ActivityPerformerDto dto)
|
||||
public static ActivityUserDisplayItem FromDto(PerformerActivity dto)
|
||||
{
|
||||
return new ActivityUserDisplayItem
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 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 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(
|
||||
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));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -18,9 +18,9 @@ public partial class BillingQueriesPageViewModel : ViewModelBase
|
|||
{
|
||||
private readonly BillingApiClient _billingClient;
|
||||
|
||||
public ActivityBrowseItemDto Activity { get; }
|
||||
public ActivityInfo Activity { get; }
|
||||
public ActivityUserDisplayItem Performer { get; }
|
||||
public CommandFormSummaryDto Form { get; }
|
||||
public CommandFormSummary Form { get; }
|
||||
public bool IsReadOnly { get; }
|
||||
public bool OngoingOnly { get; }
|
||||
|
||||
|
|
@ -55,9 +55,9 @@ public partial class BillingQueriesPageViewModel : ViewModelBase
|
|||
}
|
||||
|
||||
public BillingQueriesPageViewModel(
|
||||
ActivityBrowseItemDto activity,
|
||||
ActivityInfo activity,
|
||||
ActivityUserDisplayItem performer,
|
||||
CommandFormSummaryDto form,
|
||||
CommandFormSummary form,
|
||||
BillingApiClient billingClient,
|
||||
bool isReadOnly = false,
|
||||
bool ongoingOnly = false)
|
||||
|
|
@ -133,8 +133,8 @@ 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);
|
||||
await vm.InitializeAsync(details).ConfigureAwait(true);
|
||||
var vm = Form.CreateCommandPageViewModel(Activity, Performer, _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)
|
||||
|
|
@ -170,4 +170,4 @@ public partial class BillingQueriesPageViewModel : ViewModelBase
|
|||
|
||||
private static bool IsOngoingStatus(QueryStatus status)
|
||||
=> status is QueryStatus.Inserted or QueryStatus.Accepted or QueryStatus.InProgress;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,14 +15,14 @@ public partial class CommandFormsPageViewModel : ViewModelBase
|
|||
{
|
||||
private readonly BillingApiClient _billingClient;
|
||||
|
||||
public ActivityBrowseItemDto Activity { get; }
|
||||
public ActivityInfo Activity { get; }
|
||||
public ActivityUserDisplayItem Performer { get; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<CommandFormSummaryDto> Forms { get; set; }
|
||||
public partial ObservableCollection<CommandFormSummary> Forms { get; set; }
|
||||
|
||||
[ObservableProperty, NotifyCanExecuteChangedFor(nameof(OpenSelectedFormCommand)), NotifyCanExecuteChangedFor(nameof(OpenQueriesCommand)), NotifyCanExecuteChangedFor(nameof(OpenOngoingQueriesCommand))]
|
||||
public partial CommandFormSummaryDto? SelectedForm { get; set; }
|
||||
public partial CommandFormSummary? SelectedForm { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string StatusMessage { get; set; }
|
||||
|
|
@ -43,7 +43,7 @@ public partial class CommandFormsPageViewModel : ViewModelBase
|
|||
}
|
||||
|
||||
public CommandFormsPageViewModel(
|
||||
ActivityBrowseItemDto activity,
|
||||
ActivityInfo activity,
|
||||
ActivityUserDisplayItem performer,
|
||||
BillingApiClient billingClient)
|
||||
{
|
||||
|
|
@ -51,7 +51,7 @@ public partial class CommandFormsPageViewModel : ViewModelBase
|
|||
Performer = performer ?? throw new ArgumentNullException(nameof(performer));
|
||||
_billingClient = billingClient ?? throw new ArgumentNullException(nameof(billingClient));
|
||||
|
||||
Forms = new ObservableCollection<CommandFormSummaryDto>((activity.Forms ?? new())
|
||||
Forms = new ObservableCollection<CommandFormSummary>((activity.Forms ?? new())
|
||||
.OrderBy(f => f.Title)
|
||||
.ThenBy(f => f.ActionName));
|
||||
SelectedForm = Forms.FirstOrDefault();
|
||||
|
|
@ -81,8 +81,9 @@ public partial class CommandFormsPageViewModel : ViewModelBase
|
|||
throw new InvalidOperationException("Application PostIt indisponible.");
|
||||
}
|
||||
|
||||
var vm = new BillingCommandPageViewModel(Activity, Performer, SelectedForm, _billingClient);
|
||||
await vm.InitializeAsync();
|
||||
var vm = SelectedForm.CreateCommandPageViewModel(
|
||||
Activity, Performer, _billingClient);
|
||||
await vm!.InitializeAsync();
|
||||
await app.PushPageAsync(vm);
|
||||
}
|
||||
|
||||
|
|
@ -131,4 +132,4 @@ public partial class CommandFormsPageViewModel : ViewModelBase
|
|||
await vm.InitializeAsync();
|
||||
await app.PushPageAsync(vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
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 : RemoteViewModelBase
|
||||
{
|
||||
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 virtual bool IsSupported => true;
|
||||
public string BillingRoute => $"/billing/{Form.ActionName}";
|
||||
public bool IsEditingExisting => ExistingQueryId.HasValue;
|
||||
public string SubmitLabel => IsEditingExisting ? "Mettre à jour la commande" : "Poster la commande";
|
||||
public virtual string SupportMessage => $"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)
|
||||
{
|
||||
await LoadAsync();
|
||||
if (existingQuery is not null)
|
||||
{
|
||||
ApplyExistingQuery(existingQuery);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void ApplyExistingQuery(BillingQueryDetailsDto existingQuery);
|
||||
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
protected abstract Task SubmitAsync();
|
||||
}
|
||||
164
src/PostIt/PostIt/ViewModels/Commands/BrushViewModel.cs
Normal file
164
src/PostIt/PostIt/ViewModels/Commands/BrushViewModel.cs
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
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
|
||||
{
|
||||
public override string SupportMessage => "Choisissez une prestation coiffure puis postez la commande.";
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<HairPrestationDto> AvailablePrestations { get; set; } = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial HairPrestationDto? SelectedPrestation { get; set; }
|
||||
|
||||
|
||||
public BrushViewModel(ActivityInfo activity, ActivityUserDisplayItem performer, CommandFormSummary form, BillingApiClient billingClient)
|
||||
: base(activity, performer, form, billingClient)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task LoadAsync()
|
||||
{
|
||||
var prestations = await _billingClient.GetHairPrestationsAsync(Form.ActionName);
|
||||
|
||||
AvailablePrestations = new ObservableCollection<HairPrestationDto>
|
||||
(prestations ?? new List<HairPrestationDto>());
|
||||
|
||||
if (SelectedPrestation is null)
|
||||
{
|
||||
SelectedPrestation = AvailablePrestations.FirstOrDefault();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected override void ApplyExistingQuery(BillingQueryDetailsDto existingQuery)
|
||||
{
|
||||
base.ApplyExistingQuery(existingQuery);
|
||||
|
||||
if (existingQuery.PrestationId is not null)
|
||||
{
|
||||
SelectedPrestation = AvailablePrestations.FirstOrDefault(x => x.Id == existingQuery.PrestationId.Value);
|
||||
}
|
||||
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
if (SelectedPrestation is null)
|
||||
{
|
||||
SelectedPrestation = AvailablePrestations.FirstOrDefault();
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
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 (SelectedPrestation is null)
|
||||
{
|
||||
StatusMessage = "Sélectionnez une prestation coiffure.";
|
||||
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,
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 de la commande: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
130
src/PostIt/PostIt/ViewModels/Commands/MBrushViewModel.cs
Normal file
130
src/PostIt/PostIt/ViewModels/Commands/MBrushViewModel.cs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
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.Billing;
|
||||
|
||||
namespace PostIt.ViewModels.Commands;
|
||||
|
||||
public partial class MBrushViewModel : BrushViewModel
|
||||
{
|
||||
public override string SupportMessage => "Choisissez une ou plusieurs prestations coiffure puis postez la commande.";
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<SelectableHairPrestationItem> MultiPrestations { get; set; } = new();
|
||||
|
||||
public MBrushViewModel(ActivityInfo activity, ActivityUserDisplayItem performer, CommandFormSummary form, BillingApiClient billingClient)
|
||||
: base(activity, performer, form, billingClient)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task LoadAsync()
|
||||
{
|
||||
await base.LoadAsync().ConfigureAwait(true);
|
||||
MultiPrestations = new ObservableCollection<SelectableHairPrestationItem>(
|
||||
AvailablePrestations.Select(SelectableHairPrestationItem.FromDto));
|
||||
}
|
||||
|
||||
protected override void ApplyExistingQuery(BillingQueryDetailsDto existingQuery)
|
||||
{
|
||||
base.ApplyExistingQuery(existingQuery);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task SubmitAsync()
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
var selectedPrestations = MultiPrestations.Where(x => x.IsSelected).ToList();
|
||||
if (selectedPrestations.Count == 0)
|
||||
{
|
||||
StatusMessage = "Sélectionnez au moins une prestation coiffure.";
|
||||
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(),
|
||||
Location = new BillingLocationDto
|
||||
{
|
||||
Address = address,
|
||||
Latitude = Latitude,
|
||||
Longitude = Longitude,
|
||||
},
|
||||
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 de la commande: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
211
src/PostIt/PostIt/ViewModels/Commands/RdvViewModel.cs
Normal file
211
src/PostIt/PostIt/ViewModels/Commands/RdvViewModel.cs
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
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
|
||||
{
|
||||
public override string SupportMessage => "Complétez les informations du rendez-vous puis postez la commande.";
|
||||
|
||||
[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;
|
||||
Longitude = existingQuery.Location.Longitude;
|
||||
}
|
||||
|
||||
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 (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 (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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public override Task LoadAsync()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
10
src/PostIt/PostIt/ViewModels/RemoteViewModelBase.cs
Normal file
10
src/PostIt/PostIt/ViewModels/RemoteViewModelBase.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace PostIt.ViewModels;
|
||||
|
||||
public abstract class RemoteViewModelBase : ViewModelBase
|
||||
{
|
||||
public abstract Task LoadAsync();
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
142
src/PostIt/PostIt/Views/Commands/BrushPage.axaml
Normal file
142
src/PostIt/PostIt/Views/Commands/BrushPage.axaml
Normal 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>
|
||||
13
src/PostIt/PostIt/Views/Commands/BrushPage.axaml.cs
Normal file
13
src/PostIt/PostIt/Views/Commands/BrushPage.axaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
95
src/PostIt/PostIt/Views/Commands/RdvPage.axaml
Normal file
95
src/PostIt/PostIt/Views/Commands/RdvPage.axaml
Normal 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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ namespace Yavsc.Abstract.Workflow;
|
|||
/// <summary>
|
||||
/// Activity node returned by the browsing API.
|
||||
/// </summary>
|
||||
public sealed class ActivityBrowseItemDto
|
||||
public sealed class ActivityInfo
|
||||
{
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
|
@ -12,6 +12,6 @@ public sealed class ActivityBrowseItemDto
|
|||
public string Photo { get; set; } = string.Empty;
|
||||
public int Rate { get; set; }
|
||||
public int PerformerCount { get; set; }
|
||||
public List<CommandFormSummaryDto> Forms { get; set; } = new();
|
||||
public List<ActivityBrowseItemDto> Children { get; set; } = new();
|
||||
public List<CommandFormSummary> Forms { get; set; } = new();
|
||||
public List<ActivityInfo> Children { get; set; } = new();
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ namespace Yavsc.Abstract.Workflow;
|
|||
/// <summary>
|
||||
/// Lightweight command-form description exposed to API clients.
|
||||
/// </summary>
|
||||
public sealed class CommandFormSummaryDto
|
||||
public sealed class CommandFormSummary
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string ActionName { get; set; } = string.Empty;
|
||||
|
|
@ -3,7 +3,7 @@ namespace Yavsc.Abstract.Workflow;
|
|||
/// <summary>
|
||||
/// Lightweight performer description returned for one activity.
|
||||
/// </summary>
|
||||
public sealed class ActivityPerformerDto
|
||||
public sealed class PerformerActivity
|
||||
{
|
||||
public string PerformerId { get; set; } = string.Empty;
|
||||
public bool HasPerformerProfile { get; set; }
|
||||
|
|
@ -30,7 +30,7 @@ public sealed class ActivityApiClient
|
|||
_baseAddress = new Uri(businessBaseAddress, UriKind.Absolute);
|
||||
}
|
||||
|
||||
public Task<List<ActivityBrowseItemDto>> GetCatalogAsync(
|
||||
public Task<List<ActivityInfo>> GetCatalogAsync(
|
||||
string? parentCode = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
|
|
@ -38,23 +38,23 @@ public sealed class ActivityApiClient
|
|||
? $"{PathPrefix}/catalog"
|
||||
: $"{PathPrefix}/catalog?parentCode={Uri.EscapeDataString(parentCode)}";
|
||||
|
||||
return _api.CallAsync<List<ActivityBrowseItemDto>>(HttpMethod.Get, Absolute(path), ct: ct);
|
||||
return _api.CallAsync<List<ActivityInfo>>(HttpMethod.Get, Absolute(path), ct: ct);
|
||||
}
|
||||
|
||||
public Task<List<ActivityPerformerDto>> GetUsersAsync(
|
||||
public Task<List<PerformerActivity>> GetUsersAsync(
|
||||
string activityCode,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(activityCode))
|
||||
throw new ArgumentException("Activity code is required.", nameof(activityCode));
|
||||
|
||||
return _api.CallAsync<List<ActivityPerformerDto>>(
|
||||
return _api.CallAsync<List<PerformerActivity>>(
|
||||
HttpMethod.Get,
|
||||
Absolute($"{PathPrefix}/{Uri.EscapeDataString(activityCode)}/users"),
|
||||
ct: ct);
|
||||
}
|
||||
|
||||
public Task<List<ActivityPerformerDto>> GetPerformersAsync(
|
||||
public Task<List<PerformerActivity>> GetPerformersAsync(
|
||||
string activityCode,
|
||||
CancellationToken ct = default)
|
||||
=> GetUsersAsync(activityCode, ct);
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ public sealed class ActivityApiControllerTests : IClassFixture<ApiWebServerFixtu
|
|||
var response = await http.GetAsync("/api/v1/activity/dev/users", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var payload = await response.Content.ReadFromJsonAsync<List<ActivityPerformerDto>>(TestContext.Current.CancellationToken);
|
||||
var payload = await response.Content.ReadFromJsonAsync<List<PerformerActivity>>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(payload);
|
||||
Assert.Single(payload!);
|
||||
Assert.Equal("alice", payload[0].PerformerId);
|
||||
|
|
@ -68,7 +68,7 @@ public sealed class ActivityApiControllerTests : IClassFixture<ApiWebServerFixtu
|
|||
var response = await http.GetAsync("/api/v1/activity/dev/users", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var payload = await response.Content.ReadFromJsonAsync<List<ActivityPerformerDto>>(TestContext.Current.CancellationToken);
|
||||
var payload = await response.Content.ReadFromJsonAsync<List<PerformerActivity>>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(payload);
|
||||
Assert.Single(payload!);
|
||||
Assert.Equal("alice", payload[0].PerformerId);
|
||||
|
|
@ -84,7 +84,7 @@ public sealed class ActivityApiControllerTests : IClassFixture<ApiWebServerFixtu
|
|||
var catalogResponse = await http.GetAsync("/api/v1/activity/catalog", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, catalogResponse.StatusCode);
|
||||
|
||||
var catalog = await catalogResponse.Content.ReadFromJsonAsync<List<ActivityBrowseItemDto>>(TestContext.Current.CancellationToken);
|
||||
var catalog = await catalogResponse.Content.ReadFromJsonAsync<List<ActivityInfo>>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(catalog);
|
||||
|
||||
var dev = catalog!.Single(a => a.Code == "dev");
|
||||
|
|
@ -93,7 +93,7 @@ public sealed class ActivityApiControllerTests : IClassFixture<ApiWebServerFixtu
|
|||
var performersResponse = await http.GetAsync("/api/v1/activity/dev/users", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, performersResponse.StatusCode);
|
||||
|
||||
var performers = await performersResponse.Content.ReadFromJsonAsync<List<ActivityPerformerDto>>(TestContext.Current.CancellationToken);
|
||||
var performers = await performersResponse.Content.ReadFromJsonAsync<List<PerformerActivity>>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(performers);
|
||||
Assert.Equal(dev.PerformerCount, performers!.Count);
|
||||
Assert.Contains(performers, p => p.PerformerId == "alice");
|
||||
|
|
@ -108,7 +108,7 @@ public sealed class ActivityApiControllerTests : IClassFixture<ApiWebServerFixtu
|
|||
var catalogResponse = await http.GetAsync("/api/v1/activity/catalog", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, catalogResponse.StatusCode);
|
||||
|
||||
var catalog = await catalogResponse.Content.ReadFromJsonAsync<List<ActivityBrowseItemDto>>(TestContext.Current.CancellationToken);
|
||||
var catalog = await catalogResponse.Content.ReadFromJsonAsync<List<ActivityInfo>>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(catalog);
|
||||
Assert.DoesNotContain(catalog!, a => a.Code == "ghost");
|
||||
Assert.Contains(catalog!, a => a.Code == "dev");
|
||||
|
|
@ -123,13 +123,13 @@ public sealed class ActivityApiControllerTests : IClassFixture<ApiWebServerFixtu
|
|||
var catalogResponse = await http.GetAsync("/api/v1/activity/catalog", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, catalogResponse.StatusCode);
|
||||
|
||||
var catalog = await catalogResponse.Content.ReadFromJsonAsync<List<ActivityBrowseItemDto>>(TestContext.Current.CancellationToken);
|
||||
var catalog = await catalogResponse.Content.ReadFromJsonAsync<List<ActivityInfo>>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(catalog);
|
||||
Assert.Contains(catalog!, a => a.Code == "declared-only");
|
||||
|
||||
var response = await http.GetAsync("/api/v1/activity/declared-only/users", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var payload = await response.Content.ReadFromJsonAsync<List<ActivityPerformerDto>>(TestContext.Current.CancellationToken);
|
||||
var payload = await response.Content.ReadFromJsonAsync<List<PerformerActivity>>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(payload);
|
||||
Assert.Single(payload!);
|
||||
Assert.Equal("bob", payload[0].PerformerId);
|
||||
|
|
@ -149,8 +149,8 @@ public sealed class ActivityApiControllerTests : IClassFixture<ApiWebServerFixtu
|
|||
Assert.Equal(HttpStatusCode.OK, usersResponse.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, performersResponse.StatusCode);
|
||||
|
||||
var usersPayload = await usersResponse.Content.ReadFromJsonAsync<List<ActivityPerformerDto>>(TestContext.Current.CancellationToken);
|
||||
var performersPayload = await performersResponse.Content.ReadFromJsonAsync<List<ActivityPerformerDto>>(TestContext.Current.CancellationToken);
|
||||
var usersPayload = await usersResponse.Content.ReadFromJsonAsync<List<PerformerActivity>>(TestContext.Current.CancellationToken);
|
||||
var performersPayload = await performersResponse.Content.ReadFromJsonAsync<List<PerformerActivity>>(TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.NotNull(usersPayload);
|
||||
Assert.NotNull(performersPayload);
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ namespace Yavsc.Controllers
|
|||
}
|
||||
|
||||
[HttpGet("catalog")]
|
||||
public async Task<ActionResult<IEnumerable<ActivityBrowseItemDto>>> GetCatalog(
|
||||
public async Task<ActionResult<IEnumerable<ActivityInfo>>> GetCatalog(
|
||||
CancellationToken cancellationToken,
|
||||
[FromQuery] string parentCode = null)
|
||||
{
|
||||
|
|
@ -74,7 +74,7 @@ namespace Yavsc.Controllers
|
|||
}
|
||||
|
||||
[HttpGet("{id}/users")]
|
||||
public async Task<ActionResult<IEnumerable<ActivityPerformerDto>>> GetUsers(
|
||||
public async Task<ActionResult<IEnumerable<PerformerActivity>>> GetUsers(
|
||||
[FromRoute] string id,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
|
|
@ -97,7 +97,7 @@ namespace Yavsc.Controllers
|
|||
}
|
||||
|
||||
[HttpGet("{id}/performers")]
|
||||
public Task<ActionResult<IEnumerable<ActivityPerformerDto>>> GetPerformers(
|
||||
public Task<ActionResult<IEnumerable<PerformerActivity>>> GetPerformers(
|
||||
[FromRoute] string id,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
|
|
@ -105,7 +105,7 @@ namespace Yavsc.Controllers
|
|||
return GetUsers(id, cancellationToken);
|
||||
}
|
||||
|
||||
private Task<List<ActivityPerformerDto>> QueryDeclaredUsersAsync(
|
||||
private Task<List<PerformerActivity>> QueryDeclaredUsersAsync(
|
||||
string activityCode,
|
||||
string activityName,
|
||||
CancellationToken cancellationToken)
|
||||
|
|
@ -118,7 +118,7 @@ namespace Yavsc.Controllers
|
|||
from performer in performerProfiles.DefaultIfEmpty()
|
||||
where ua.DoesCode == activityCode
|
||||
orderby user != null ? user.UserName : ua.UserId
|
||||
select new ActivityPerformerDto
|
||||
select new PerformerActivity
|
||||
{
|
||||
PerformerId = ua.UserId,
|
||||
HasPerformerProfile = performer != null,
|
||||
|
|
@ -260,11 +260,11 @@ namespace Yavsc.Controllers
|
|||
return _context.Activities.Count(e => e.Code == id) > 0;
|
||||
}
|
||||
|
||||
private static ActivityBrowseItemDto ToBrowseItem(
|
||||
private static ActivityInfo ToBrowseItem(
|
||||
Activity activity,
|
||||
IReadOnlyDictionary<string, int> performerCounts)
|
||||
{
|
||||
return new ActivityBrowseItemDto
|
||||
return new ActivityInfo
|
||||
{
|
||||
Code = activity.Code,
|
||||
Name = activity.Name,
|
||||
|
|
@ -274,7 +274,7 @@ namespace Yavsc.Controllers
|
|||
Rate = activity.Rate,
|
||||
PerformerCount = performerCounts.TryGetValue(activity.Code, out var count) ? count : 0,
|
||||
Forms = (activity.Forms ?? Enumerable.Empty<CommandForm>())
|
||||
.Select(f => new CommandFormSummaryDto
|
||||
.Select(f => new CommandFormSummary
|
||||
{
|
||||
Id = f.Id,
|
||||
ActionName = f.ActionName,
|
||||
|
|
@ -285,7 +285,7 @@ namespace Yavsc.Controllers
|
|||
.Where(c => !c.Hidden)
|
||||
.Where(c => performerCounts.TryGetValue(c.Code, out var childCount) && childCount > 0)
|
||||
.OrderByDescending(c => c.Rate)
|
||||
.Select(c => new ActivityBrowseItemDto
|
||||
.Select(c => new ActivityInfo
|
||||
{
|
||||
Code = c.Code,
|
||||
Name = c.Name,
|
||||
|
|
@ -295,7 +295,7 @@ namespace Yavsc.Controllers
|
|||
Rate = c.Rate,
|
||||
PerformerCount = performerCounts.TryGetValue(c.Code, out var childCount) ? childCount : 0,
|
||||
Forms = (c.Forms ?? Enumerable.Empty<CommandForm>())
|
||||
.Select(f => new CommandFormSummaryDto
|
||||
.Select(f => new CommandFormSummary
|
||||
{
|
||||
Id = f.Id,
|
||||
ActionName = f.ActionName,
|
||||
|
|
|
|||
|
|
@ -13,15 +13,15 @@ namespace Yavsc.Models.Messaging
|
|||
{
|
||||
Topic = "Estimation";
|
||||
Estimation = estimate;
|
||||
perfer = estimate.Owner;
|
||||
performer = estimate.Owner;
|
||||
|
||||
ProviderInfo = new ProviderClientInfo {
|
||||
Rate = perfer.Rate,
|
||||
UserName = perfer.Performer.UserName,
|
||||
Avatar = perfer.Performer.Avatar,
|
||||
UserId = perfer.PerformerId
|
||||
Rate = performer.Rate,
|
||||
UserName = performer.Performer.UserName,
|
||||
Avatar = performer.Performer.Avatar,
|
||||
UserId = performer.PerformerId
|
||||
};
|
||||
Sender = perfer.Performer.UserName;
|
||||
Sender = performer.Performer.UserName;
|
||||
_localizer = SR;
|
||||
}
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ namespace Yavsc.Models.Messaging
|
|||
ProviderClientInfo ProviderInfo { get; set; }
|
||||
Estimate Estimation { get; set; }
|
||||
|
||||
private readonly PerformerProfile perfer;
|
||||
private readonly PerformerProfile performer;
|
||||
|
||||
public string Topic
|
||||
{
|
||||
|
|
@ -47,7 +47,7 @@ namespace Yavsc.Models.Messaging
|
|||
|
||||
public string CreateBody()
|
||||
{
|
||||
return string.Format(_localizer["EstimationMessageToClient"], perfer.Performer.UserName, this.Estimation.Bill.Addition());
|
||||
return string.Format(_localizer["EstimationMessageToClient"], performer.Performer.UserName, this.Estimation.Bill.Addition());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue