diff --git a/.vscode/tasks.json b/.vscode/tasks.json index a30c9c06..fd46437a 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -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, diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bef40e3..a7c3209c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,42 +1,5 @@ # Changelog -## [1.0.8-rc12] - unstable - -### Added - -* [PostIt] Nouveau helper d'image `ImageHelper` pour charger des bitmaps depuis les ressources et depuis le web. -* [PostIt] Affichage de l'avatar XS dans la liste des performers d'activites, avec fallback visuel (initiale utilisateur). -* [PostIt.Tests] Nouveaux tests autour des URLs avatar et de la source d'autorite. -* [contrib] Ajout d'un `README.md` utilitaire pour les symboles/icones. - -### Changed - -* [PostIt] Les avatars ne sont plus relies en string sur `Image.Source`: ils sont telecharges et lies en `Bitmap`. -* [Yavsc.Api.Client] `ActivityApiClient` accepte une base d'avatar dediee et construit les URLs avatar depuis l'autorite d'identification. -* [PostIt] Le header de `MainPage` n'utilise plus `ScrollViewer`; remplacement par une barre de commandes basee sur `WrapPanel`. -* [PostIt] Alignement de la navigation blogs: renommage `PushMainPageAsync` -> `PushBlogsPageAsync` et ajustement de `HomePageViewModel`. - -### Fixed - -néant - -## [1.0.8-rc11] - unstable - -### Added - -nothing - -### Changed - -* [Yavsc.Api.Test] Mise a jour de `Microsoft.EntityFrameworkCore.Sqlite` vers `10.0.11` afin de supprimer l'alerte NU1903 liee a `SQLitePCLRaw.lib.e_sqlite3` 2.1.11. -* [Yavsc.Org] Nettoyage de la configuration NuGet pour le restore: suppression du fichier local `Directory.Packages.props` au profit du fichier racine centralise. -* [Yavsc.Org] Suppression de references de packages redondantes dans le projet, sans impact fonctionnel attendu. - -### Fixed - -* [Yavsc.Api.Test] Le restore n'emet plus le warning de vulnerabilite `NU1903` sur `SQLitePCLRaw.lib.e_sqlite3`. -* [Yavsc.Org] Suppression d'une vulnerabilite de severite elevee sur AutoMapper apres publication et consommation de la nouvelle version candidate de `HigginsSoft.IdentityServer8`. - ## [1.0.8-rc10] - unstable ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8aa0567d..fd408c5c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,19 +49,6 @@ 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 @@ -74,8 +61,8 @@ et ce projet adhère au [Semantic Versioning](https://semver.org/spec/v2.0.0.htm À noter : la **parité du numéro de patch** porte une signification de canal : -- **patch pair** (ex. `1.0.0`, `1.0.2`) → **preview** -- **patch impair** (ex. `1.0.1`, `1.0.3`) → **stable** +- **patch pair** (ex. `1.0.0`, `1.0.2`) → **stable** +- **patch impair** (ex. `1.0.1`, `1.0.3`) → **preview** - **suffixe** (ex. `1.0.0-rc1`, `1.0.0-alpha`) → **instable** Cette convention est partagée avec le dépôt diff --git a/Directory.Packages.props b/Directory.Packages.props index f1be93f9..7505c851 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,31 +1,15 @@ true - 8.1.0-pazofrc007 - - - - - - - - - - - - - - - - - - - - - - + + + + + + + @@ -33,24 +17,14 @@ - + - - - - - - - - - - - + \ No newline at end of file diff --git a/README.md b/README.md index 549348f4..b132f3f2 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,6 @@ 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 diff --git a/contrib/README.md b/contrib/README.md deleted file mode 100644 index 01156156..00000000 --- a/contrib/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Read me - -## Note aux icones - -㝉®🅬⛒⛑🩎🩺🞫🞮🞕🞖🞆🔴🔵🔲🖂🔧🔩🔐🔌💾💼💬💭👿👾🏷🎯🏹🌍🎎💩 diff --git a/doc/README.md b/doc/README.md index fb9bea94..912a2e56 100644 --- a/doc/README.md +++ b/doc/README.md @@ -18,9 +18,6 @@ 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 diff --git a/doc/agent-intent-matrix.md b/doc/agent-intent-matrix.md deleted file mode 100644 index 975cd7b7..00000000 --- a/doc/agent-intent-matrix.md +++ /dev/null @@ -1,20 +0,0 @@ -# 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. diff --git a/doc/agent-playbook.md b/doc/agent-playbook.md deleted file mode 100644 index ecc14f1d..00000000 --- a/doc/agent-playbook.md +++ /dev/null @@ -1,101 +0,0 @@ -# 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: -Objectif: -Contraintes: -Verification: -Sortie attendue: -``` - -## 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 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 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 dans . -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. diff --git a/doc/onboarding-agents.md b/doc/onboarding-agents.md deleted file mode 100644 index 14a727a1..00000000 --- a/doc/onboarding-agents.md +++ /dev/null @@ -1,73 +0,0 @@ -# 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. diff --git a/src/PostIt/PostIt.Android/Application.cs b/src/PostIt/PostIt.Android/Application.cs index 040b01ca..f5a7908d 100644 --- a/src/PostIt/PostIt.Android/Application.cs +++ b/src/PostIt/PostIt.Android/Application.cs @@ -1,5 +1,4 @@ using Android.App; -using Android; using Android.Runtime; using Avalonia; using Avalonia.Android; @@ -10,9 +9,6 @@ using Avalonia.Controls; using Avalonia.Styling; using Yavsc.Api.Client; -[assembly: UsesPermission(Manifest.Permission.AccessFineLocation)] -[assembly: UsesPermission(Manifest.Permission.AccessCoarseLocation)] - namespace PostIt.Android { [Application] diff --git a/src/PostIt/PostIt.Android/MainActivity.cs b/src/PostIt/PostIt.Android/MainActivity.cs index 54910080..ad8455ef 100644 --- a/src/PostIt/PostIt.Android/MainActivity.cs +++ b/src/PostIt/PostIt.Android/MainActivity.cs @@ -57,17 +57,6 @@ public class MainActivity : AvaloniaMainActivity } - public override void OnRequestPermissionsResult(int requestCode, string[]? permissions, Permission[]? grantResults) - { - if (PostIt.Android.Services.AndroidCurrentLocationProvider - .HandlePermissionResult(requestCode, grantResults)) - { - return; - } - - base.OnRequestPermissionsResult(requestCode, permissions, grantResults); - } - internal static class AndroidOidcCallbackSink { private static System.Threading.Tasks.TaskCompletionSource? _pending; diff --git a/src/PostIt/PostIt.Android/PlatformBootstrap.cs b/src/PostIt/PostIt.Android/PlatformBootstrap.cs index 5b90267f..f208f9ce 100644 --- a/src/PostIt/PostIt.Android/PlatformBootstrap.cs +++ b/src/PostIt/PostIt.Android/PlatformBootstrap.cs @@ -14,12 +14,11 @@ internal static class PlatformBootstrap { internal static void InitPlatform() { + Platform.CreateBrowser = () => { var activity = MainActivity.Current; return activity is null ? null : new AndroidSystemBrowser(activity); }; - - Platform.TryGetCurrentLocationAsync = AndroidCurrentLocationProvider.TryGetCurrentLocationAsync; } } diff --git a/src/PostIt/PostIt.Android/Services/AndroidCurrentLocationProvider.cs b/src/PostIt/PostIt.Android/Services/AndroidCurrentLocationProvider.cs deleted file mode 100644 index f710cacc..00000000 --- a/src/PostIt/PostIt.Android/Services/AndroidCurrentLocationProvider.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Android; -using Android.App; -using Android.Content.PM; -using Android.Locations; -using AndroidX.Core.App; -using AndroidX.Core.Content; -using PostIt.Services; - -namespace PostIt.Android.Services; - -internal static class AndroidCurrentLocationProvider -{ - public static async Task TryGetCurrentLocationAsync(CancellationToken cancellationToken) - { - var activity = MainActivity.Current; - if (activity is null) - { - return CurrentLocationResult.Unavailable("L'activité Android n'est pas encore prête."); - } - - var permissionGranted = await LocationPermissionBroker.EnsureGrantedAsync(activity, cancellationToken).ConfigureAwait(false); - if (!permissionGranted) - { - return CurrentLocationResult.PermissionDenied(); - } - - var locationManager = activity.GetSystemService(global::Android.Content.Context.LocationService) as LocationManager; - if (locationManager is null) - { - return CurrentLocationResult.Unavailable("Le service de localisation Android est indisponible."); - } - - var location = locationManager.GetProviders(enabledOnly: true)? - .Select(provider => locationManager.GetLastKnownLocation(provider)) - .Where(candidate => candidate is not null) - .OrderByDescending(candidate => candidate!.Time) - .ThenBy(candidate => candidate!.Accuracy) - .FirstOrDefault(); - - if (location is null) - { - return CurrentLocationResult.Unavailable("Aucune position n'est disponible. Activez la localisation du système puis réessayez."); - } - - return CurrentLocationResult.Success(location.Latitude, location.Longitude); - } - - public static bool HandlePermissionResult(int requestCode, Permission[]? grantResults) - => LocationPermissionBroker.HandleResult(requestCode, grantResults); - - private static class LocationPermissionBroker - { - private const int RequestCode = 4042; - private static readonly string[] RequestedPermissions = - { - Manifest.Permission.AccessFineLocation, - Manifest.Permission.AccessCoarseLocation, - }; - - private static readonly object SyncRoot = new(); - private static TaskCompletionSource? _pendingRequest; - - public static Task EnsureGrantedAsync(Activity activity, CancellationToken cancellationToken) - { - if (HasLocationPermission(activity)) - { - return Task.FromResult(true); - } - - lock (SyncRoot) - { - if (_pendingRequest is null) - { - _pendingRequest = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - ActivityCompat.RequestPermissions(activity, RequestedPermissions, RequestCode); - } - - if (!cancellationToken.CanBeCanceled) - { - return _pendingRequest.Task; - } - - return WaitAsync(_pendingRequest.Task, cancellationToken); - } - } - - public static bool HandleResult(int requestCode, Permission[]? grantResults) - { - if (requestCode != RequestCode) - { - return false; - } - - var granted = grantResults is { Length: > 0 } && grantResults.All(result => result == Permission.Granted); - TaskCompletionSource? pendingRequest; - lock (SyncRoot) - { - pendingRequest = _pendingRequest; - _pendingRequest = null; - } - - pendingRequest?.TrySetResult(granted); - return true; - } - - private static bool HasLocationPermission(Activity activity) - { - return ContextCompat.CheckSelfPermission(activity, Manifest.Permission.AccessFineLocation) == Permission.Granted - || ContextCompat.CheckSelfPermission(activity, Manifest.Permission.AccessCoarseLocation) == Permission.Granted; - } - - private static async Task WaitAsync(Task task, CancellationToken cancellationToken) - { - using var registration = cancellationToken.Register(() => - { - lock (SyncRoot) - { - _pendingRequest?.TrySetCanceled(cancellationToken); - _pendingRequest = null; - } - }); - - return await task.ConfigureAwait(false); - } - } -} diff --git a/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs b/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs index 2e8d3e54..c2fab4c3 100644 --- a/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs @@ -7,20 +7,6 @@ namespace PostIt.Tests; public class ActivitiesPageViewModelTests { - [Fact] - public void ActivityApiClient_uses_avatar_authority_when_provided() - { - var api = new StubActivityApi(); - var client = new ActivityApiClient( - api, - "https://api.pschneider.fr/api/v1/", - "https://yavsc.pschneider.fr/"); - - var url = client.BuildAvatarXsUrl("paul"); - - Assert.Equal("https://yavsc.pschneider.fr/avatars/paul.xs.png", url); - } - [Fact] public async Task ActivityApiClient_uses_business_absolute_paths() { @@ -54,7 +40,6 @@ public class ActivitiesPageViewModelTests Assert.Equal("brush", vm.CurrentActivity?.Code); Assert.Single(vm.Performers); Assert.Equal("Alice", vm.Performers[0].UserName); - Assert.Equal("https://business.example/avatars/Alice.xs.png", vm.Performers[0].AvatarXsUrl); Assert.True(vm.Performers[0].HasPerformerProfile); Assert.True(vm.Performers[0].IsPerformerActive); Assert.Equal("Actif", vm.Performers[0].PerformerStatusBadgeLabel); @@ -65,7 +50,6 @@ public class ActivitiesPageViewModelTests Assert.Equal("brush-pro", vm.CurrentActivity?.Code); Assert.Single(vm.Performers); Assert.Equal("Bob", vm.Performers[0].UserName); - Assert.Equal("https://business.example/avatars/Bob.xs.png", vm.Performers[0].AvatarXsUrl); Assert.True(vm.Performers[0].HasPerformerProfile); Assert.False(vm.Performers[0].IsPerformerActive); Assert.Equal("Inactif", vm.Performers[0].PerformerStatusBadgeLabel); @@ -88,9 +72,9 @@ public class ActivitiesPageViewModelTests { Paths.Add(path); - if (typeof(T) == typeof(List)) + if (typeof(T) == typeof(List)) { - var activities = new List + var activities = new List { new() { @@ -98,11 +82,11 @@ public class ActivitiesPageViewModelTests Name = "Brush", Description = "Coiffure à domicile", PerformerCount = 1, - Forms = new List + Forms = new List { new() { Id = 1, ActionName = "Rdv", Title = "Rendez-vous" } }, - Children = new List + Children = new List { new() { @@ -111,7 +95,7 @@ public class ActivitiesPageViewModelTests Description = "Spécialisation premium", ParentCode = "brush", PerformerCount = 1, - Forms = new List + Forms = new List { new() { Id = 2, ActionName = "Rdv", Title = "Rendez-vous premium" } } @@ -122,14 +106,14 @@ public class ActivitiesPageViewModelTests return Task.FromResult((T)(object)activities); } - if (typeof(T) == typeof(List)) + if (typeof(T) == typeof(List)) { var performers = path.EndsWith("brush-pro/users", StringComparison.Ordinal) - ? new List + ? new List { new() { PerformerId = "pro-2", HasPerformerProfile = true, Active = false, UserName = "Bob", ActivityCode = "brush-pro", ActivityName = "Brush Pro", ExtraActivityCount = 2 } } - : new List + : new List { new() { PerformerId = "pro-1", HasPerformerProfile = true, Active = true, UserName = "Alice", ActivityCode = "brush", ActivityName = "Brush", ExtraActivityCount = 0 } }; diff --git a/src/PostIt/PostIt.Tests/BearerScopeTests.cs b/src/PostIt/PostIt.Tests/BearerScopeTests.cs index 984483fc..c6bf7d56 100644 --- a/src/PostIt/PostIt.Tests/BearerScopeTests.cs +++ b/src/PostIt/PostIt.Tests/BearerScopeTests.cs @@ -67,7 +67,7 @@ public class BearerScopeTests Scopes = userScopes, RedirectUri = "postit://callback", }, - ApiUrl = "https://example.invalid/api/v1/", + BusinessApiUrl = "https://example.invalid/api/v1/", }; var tokensPath = Path.Combine( @@ -266,7 +266,7 @@ public class BearerScopeTests // private HttpClient is independent, so we resolve the // absolute URI ourselves from Settings.BusinessApiUrl — // the same URL BlogApiClient would have set as BaseAddress. - var absolute = new Uri(new Uri(Settings.ApiUrl), path); + var absolute = new Uri(new Uri(Settings.BusinessApiUrl), path); using var req = new HttpRequestMessage(method, absolute); req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _accessToken); diff --git a/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs b/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs index c7b5b926..eef3fe21 100644 --- a/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs @@ -1,8 +1,6 @@ +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; @@ -17,19 +15,19 @@ public class BillingCommandPageViewModelTests { var api = new RecordingApi(); var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = - new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" } - .CreateCommandPageViewModel( - new ActivityInfo { Code = "dev", Name = "Développement" }, + var vm = new BillingCommandPageViewModel( + new ActivityBrowseItemDto { Code = "dev", Name = "Développement" }, new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" }, - 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; + new CommandFormSummaryDto { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" }, + client) + { + EventDateText = "2026-09-02 14:30", + Reason = "Point de cadrage", + Address = "1 rue du Test", + LatitudeText = "48.8566", + LongitudeText = "2.3522", + Consent = true, + }; await vm.SubmitCommand.ExecuteAsync(null); @@ -48,71 +46,16 @@ public class BillingCommandPageViewModelTests { var api = new RecordingApi(); var client = new BillingApiClient(api, "https://business.example/api/v1/"); - - var vm = - new CommandFormSummary { Id = 13, ActionName = "Book", Title = "Réservation" } - .CreateCommandPageViewModel( - new ActivityInfo { Code = "book", Name = "Book" }, + var vm = new BillingCommandPageViewModel( + new ActivityBrowseItemDto { Code = "book", Name = "Book" }, new ActivityUserDisplayItem { PerformerId = "perf-2", UserName = "Bob" }, + new CommandFormSummaryDto { Id = 13, ActionName = "Book", Title = "Réservation" }, client); - Assert.Null(vm); - } - - [Fact] - public async Task SubmitAsync_allows_missing_coordinates_and_omits_them_from_payload() - { - var api = new RecordingApi(); - var client = new BillingApiClient(api, "https://business.example/api/v1/"); - 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" }, - 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); - using var json = JsonDocument.Parse(JsonSerializer.Serialize(api.LastBody)); - var location = json.RootElement.GetProperty("Location"); - Assert.Equal("1 rue du Test", location.GetProperty("Address").GetString()); - Assert.False(location.TryGetProperty("Latitude", out _)); - Assert.False(location.TryGetProperty("Longitude", out _)); - } - - [Fact] - public async Task UseCurrentLocationAsync_prefills_coordinates_from_platform_provider() - { - var original = Platform.TryGetCurrentLocationAsync; - try - { - Platform.TryGetCurrentLocationAsync = _ => Task.FromResult(CurrentLocationResult.Success(48.8566, 2.3522)); - - var api = new RecordingApi(); - var client = new BillingApiClient(api, "https://business.example/api/v1/"); - 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" }, - client) as RdvViewModel; - - await vm!.UseCurrentLocationCommand.ExecuteAsync(null); - - Assert.Equal(48.8566, vm!.Latitude); - Assert.Equal(2.3522, vm!.Longitude); - } - finally - { - Platform.TryGetCurrentLocationAsync = original; - } + Assert.Null(api.LastPath); + Assert.Contains("n'est pas encore pris en charge", vm.StatusMessage, StringComparison.OrdinalIgnoreCase); } [Fact] @@ -127,18 +70,19 @@ public class BillingCommandPageViewModelTests } }; var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = - new CommandFormSummary { Id = 13, ActionName = "Brush", Title = "Coupe" } - .CreateCommandPageViewModel( - new ActivityInfo { Code = "brush", Name = "Brush" }, + var vm = new BillingCommandPageViewModel( + new ActivityBrowseItemDto { Code = "brush", Name = "Brush" }, new ActivityUserDisplayItem { PerformerId = "perf-2", UserName = "Bob" }, - 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"; + new CommandFormSummaryDto { Id = 13, ActionName = "Brush", Title = "Coupe" }, + client) + { + EventDateText = "2026-09-02 14:30", + Address = "1 rue du Test", + LatitudeText = "48.8566", + LongitudeText = "2.3522", + Consent = true, + AdditionalInfo = "Prévoir shampoing", + }; await vm.InitializeAsync(); vm.SelectedPrestation = vm.AvailablePrestations[1]; @@ -162,22 +106,23 @@ public class BillingCommandPageViewModelTests } }; var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = - new CommandFormSummary { Id = 14, ActionName = "MBrush", Title = "Coupe groupée" } - .CreateCommandPageViewModel( - new ActivityInfo { Code = "mbrush", Name = "MBrush" }, + var vm = new BillingCommandPageViewModel( + new ActivityBrowseItemDto { Code = "mbrush", Name = "MBrush" }, new ActivityUserDisplayItem { PerformerId = "perf-3", UserName = "Cara" }, - 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; + new CommandFormSummaryDto { Id = 14, ActionName = "MBrush", Title = "Coupe groupée" }, + client) + { + EventDateText = "2026-09-03 10:00", + Address = "2 rue du Test", + LatitudeText = "48.8567", + LongitudeText = "2.3523", + Consent = true, + }; await vm.InitializeAsync(); - vm!.MultiPrestations[0].IsSelected = true; - vm!.MultiPrestations[1].IsSelected = true; - await vm!.SubmitCommand.ExecuteAsync(null); + 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)); @@ -199,13 +144,13 @@ public class BillingCommandPageViewModelTests } }; var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = - new CommandFormSummary { Id = 13, ActionName = "Brush", Title = "Coupe" } - .CreateCommandPageViewModel( - new ActivityInfo { Code = "brush", Name = "Brush" }, + var vm = new BillingCommandPageViewModel( + new ActivityBrowseItemDto { Code = "brush", Name = "Brush" }, new ActivityUserDisplayItem { PerformerId = "perf-2", UserName = "Bob" }, - client) as BrushViewModel; - await vm!.InitializeAsync(new BillingQueryDetailsDto + new CommandFormSummaryDto { Id = 13, ActionName = "Brush", Title = "Coupe" }, + client); + + await vm.InitializeAsync(new BillingQueryDetailsDto { Id = 77, BillingCode = "Brush", @@ -225,9 +170,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); @@ -271,4 +216,4 @@ public class BillingCommandPageViewModelTests public ValueTask DisposeAsync() => ValueTask.CompletedTask; } -} +} \ No newline at end of file diff --git a/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs b/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs index a7f2b084..a5d37532 100644 --- a/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs @@ -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 ActivityInfo { Code = "dev", Name = "Développement" }, + new ActivityBrowseItemDto { Code = "dev", Name = "Développement" }, new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" }, - new CommandFormSummary { Id = 1, ActionName = "Rdv", Title = "Rendez-vous" }, + new CommandFormSummaryDto { 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 ActivityInfo { Code = "dev", Name = "Développement" }, + new ActivityBrowseItemDto { Code = "dev", Name = "Développement" }, new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" }, - new CommandFormSummary { Id = 1, ActionName = "Rdv", Title = "Rendez-vous" }, + new CommandFormSummaryDto { Id = 1, ActionName = "Rdv", Title = "Rendez-vous" }, client, isReadOnly: true, ongoingOnly: true); @@ -131,4 +131,4 @@ public class BillingQueriesPageViewModelTests public ValueTask DisposeAsync() => ValueTask.CompletedTask; } -} +} \ No newline at end of file diff --git a/src/PostIt/PostIt.Tests/PostAclDialogTests.cs b/src/PostIt/PostIt.Tests/PostAclDialogTests.cs index e7175203..dd277629 100644 --- a/src/PostIt/PostIt.Tests/PostAclDialogTests.cs +++ b/src/PostIt/PostIt.Tests/PostAclDialogTests.cs @@ -95,7 +95,7 @@ public class PostAclDialogTests HttpMethod method, string path, object? body = null, CancellationToken ct = default) { - var absolute = new Uri(new Uri(Settings.ApiUrl), path); + var absolute = new Uri(new Uri(Settings.BusinessApiUrl), path); using var req = new HttpRequestMessage(method, absolute); using var resp = _http.SendAsync(req, ct).GetAwaiter().GetResult(); resp.EnsureSuccessStatusCode(); @@ -123,8 +123,8 @@ public class PostAclDialogTests var handler = new CountingHttpHandler(); var settings = new Settings(); var api = new TestableYavscApiClient(settings, new TokenStore(System.IO.Path.GetTempFileName()), handler); - var aclClient = new BlogAclApiClient(api, settings.ApiUrl); - var circleClient = new CircleApiClient(api, settings.ApiUrl); + var aclClient = new BlogAclApiClient(api, settings.BusinessApiUrl); + var circleClient = new CircleApiClient(api, settings.BusinessApiUrl); var services = new ServiceCollection(); services.AddSingleton(settings); diff --git a/src/PostIt/PostIt.Tests/SettingsLoadTests.cs b/src/PostIt/PostIt.Tests/SettingsLoadTests.cs index 3f8dc02b..3f350240 100644 --- a/src/PostIt/PostIt.Tests/SettingsLoadTests.cs +++ b/src/PostIt/PostIt.Tests/SettingsLoadTests.cs @@ -89,7 +89,7 @@ public class SettingsLoadTests settings.Authentication.RedirectUri = global::AuthenticationSettings.DesktopRedirectUri; - settings.ApiUrl = flip + settings.BusinessApiUrl = flip ? "https://a.example.test/api/v1/" : "https://b.example.test/api/v1/"; diff --git a/src/PostIt/PostIt.Tests/YavscApiClientTests.cs b/src/PostIt/PostIt.Tests/YavscApiClientTests.cs index b074de66..21c0dd00 100644 --- a/src/PostIt/PostIt.Tests/YavscApiClientTests.cs +++ b/src/PostIt/PostIt.Tests/YavscApiClientTests.cs @@ -58,7 +58,7 @@ public class YavscApiClientTests // calls CallAsync("posts", ...) directly (bypassing // BlogApiClient, which is the only thing that would set // it in production). Mirror prod here. - reloaded.Http.BaseAddress = new Uri(settings.ApiUrl); + reloaded.Http.BaseAddress = new Uri(settings.BusinessApiUrl); var posts = await reloaded.CallAsync>( HttpMethod.Get, "posts", TestContext.Current.CancellationToken); @@ -118,7 +118,7 @@ public class YavscApiClientTests RedirectUri = "postit://callback", Scopes = new[] { "openid" }, }, - ApiUrl = "https://127.0.0.1:5003/api/v1", + BusinessApiUrl = "https://127.0.0.1:5003/api/v1", }; var client = new YavscApiClient(settings, new TokenStore(Path.Combine( Path.GetTempPath(), $"postit-tests-noop-{Guid.NewGuid():N}.json"))); @@ -162,7 +162,7 @@ public class YavscApiClientTests RedirectUri = authority.LoopbackRedirectUri, Scopes = new[] { "openid", "profile", "blog" } }, - ApiUrl = apiBaseUrl + BusinessApiUrl = apiBaseUrl }; private static async Task LoginAndPersistAsync( @@ -175,7 +175,7 @@ public class YavscApiClientTests // directly (bypassing BlogApiClient) rely on the same // BaseAddress the production chain sets in BlogApiClient's // ctor. Mirror that here so "posts" resolves to the stub. - client.Http.BaseAddress = new Uri(settings.ApiUrl); + client.Http.BaseAddress = new Uri(settings.BusinessApiUrl); // Force the API client to use the test browser by routing the // LoginInteractiveAsync call through a small wrapper. diff --git a/src/PostIt/PostIt/App.axaml.cs b/src/PostIt/PostIt/App.axaml.cs index 59d5e412..d3fbf651 100644 --- a/src/PostIt/PostIt/App.axaml.cs +++ b/src/PostIt/PostIt/App.axaml.cs @@ -135,6 +135,9 @@ private void ConfigureRootView(MainView rootView) var homePage = provider.GetRequiredService(); var app = (App)Current!; await app.PushPageAsync(homePage); + if (!refreshed) return; + + await PushMainPageAsync().ConfigureAwait(true); } /// @@ -145,7 +148,7 @@ private void ConfigureRootView(MainView rootView) /// (interactive login from the banner). Pulled out as a helper so /// the two callers can't drift apart. /// - public static async Task PushBlogsPageAsync() + public static async Task PushMainPageAsync() { var app = (App)Current!; var mainVm = app.ServiceProvider!.GetRequiredService(); diff --git a/src/PostIt/PostIt/Helpers/FormHelpers.cs b/src/PostIt/PostIt/Helpers/FormHelpers.cs deleted file mode 100644 index 18cc8fd0..00000000 --- a/src/PostIt/PostIt/Helpers/FormHelpers.cs +++ /dev/null @@ -1,50 +0,0 @@ -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"); - } -} diff --git a/src/PostIt/PostIt/Helpers/ImageHelper.cs b/src/PostIt/PostIt/Helpers/ImageHelper.cs deleted file mode 100644 index 30e7e34e..00000000 --- a/src/PostIt/PostIt/Helpers/ImageHelper.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.IO; -using System.Net.Http; -using System.Threading.Tasks; -using Avalonia.Media.Imaging; -using Avalonia.Platform; - -namespace PostIt.Helpers; - -public static class ImageHelper -{ - private static readonly HttpClient HttpClient = new(); - - public static Bitmap LoadFromResource(Uri resourceUri) - { - return new Bitmap(AssetLoader.Open(resourceUri)); - } - - public static async Task LoadFromWeb(Uri url) - { - try - { - var response = await HttpClient.GetAsync(url).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - var data = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); - return new Bitmap(new MemoryStream(data)); - } - catch (HttpRequestException ex) - { - Console.WriteLine($"An error occurred while downloading image '{url}': {ex.Message}"); - return null; - } - } -} \ No newline at end of file diff --git a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs index f95f31e3..e86d1c4c 100644 --- a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs +++ b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.DependencyInjection; using PostIt.Services; using PostIt.ViewModels; using PostIt.Views; -using PostIt.Views.Commands; using Yavsc.Api.Client; namespace PostIt.Helpers; @@ -24,11 +23,8 @@ public static class ServiceCollectionHelpers var circleClient = new CircleApiClient(api, settings.BlogsApiUrl); var blogAclClient = new BlogAclApiClient(api, settings.BlogsApiUrl); var userSearchClient = new UserSearchClient(api, settings.BlogsApiUrl); - var activityClient = new ActivityApiClient( - api, - settings.ApiUrl, - settings.Authentication?.Authority); - var billingClient = new BillingApiClient(api, settings.ApiUrl); + var activityClient = new ActivityApiClient(api, settings.BusinessApiUrl); + var billingClient = new BillingApiClient(api, settings.BusinessApiUrl); var userDirectory = new UserDirectory(userSearchClient); // Vues @@ -53,8 +49,7 @@ public static class ServiceCollectionHelpers services.AddSingleton(); services.AddSingleton(); services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + services.AddTransient(); services.AddTransient(); // ViewModels services.AddSingleton(settings); diff --git a/src/PostIt/PostIt/Services/CurrentLocationResult.cs b/src/PostIt/PostIt/Services/CurrentLocationResult.cs deleted file mode 100644 index ea19da43..00000000 --- a/src/PostIt/PostIt/Services/CurrentLocationResult.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace PostIt.Services; - -public sealed class CurrentLocationResult -{ - private CurrentLocationResult(bool isSuccess, bool isPermissionDenied, double? latitude, double? longitude, string message) - { - IsSuccess = isSuccess; - IsPermissionDenied = isPermissionDenied; - Latitude = latitude; - Longitude = longitude; - Message = message; - } - - public bool IsSuccess { get; } - public bool IsPermissionDenied { get; } - public double? Latitude { get; } - public double? Longitude { get; } - public string Message { get; } - - public static CurrentLocationResult Success(double latitude, double longitude, string? message = null) - => new(true, false, latitude, longitude, message ?? "Position récupérée."); - - public static CurrentLocationResult PermissionDenied(string? message = null) - => new(false, true, null, null, message ?? "La géolocalisation n'est pas autorisée."); - - public static CurrentLocationResult Unavailable(string? message = null) - => new(false, false, null, null, message ?? "La géolocalisation n'est pas disponible sur cette plateforme."); -} diff --git a/src/PostIt/PostIt/Services/Platform.cs b/src/PostIt/PostIt/Services/Platform.cs index 2e5ac76a..8e5f7e25 100644 --- a/src/PostIt/PostIt/Services/Platform.cs +++ b/src/PostIt/PostIt/Services/Platform.cs @@ -1,7 +1,4 @@ -using System; using IdentityModel.OidcClient.Browser; -using System.Threading; -using System.Threading.Tasks; namespace PostIt.Services; @@ -40,12 +37,4 @@ public static class Platform /// public static System.Func? CreateBrowser { get; set; } = () => new CustomSchemeBrowser(CustomScheme); - - /// - /// Optional platform hook used by the shared billing form to request a - /// current device position. Platforms that do not expose a native - /// location provider can leave the default delegate in place. - /// - public static Func> TryGetCurrentLocationAsync { get; set; } = - _ => Task.FromResult(CurrentLocationResult.Unavailable()); } diff --git a/src/PostIt/PostIt/Services/YavscApiClient.cs b/src/PostIt/PostIt/Services/YavscApiClient.cs index 4d748f87..726b3f4f 100644 --- a/src/PostIt/PostIt/Services/YavscApiClient.cs +++ b/src/PostIt/PostIt/Services/YavscApiClient.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -352,77 +351,6 @@ public class YavscApiClient : IYavscApiClient, IAsyncDisposable _store.Save(_tokens); } - /// - /// Upload a user avatar to the Yavsc API. The server expects a - /// single multipart file named file and validates the image - /// content type before persisting it. - /// - public async Task SetAvatarAsync( - Stream imageStream, - string fileName, - string? contentType = null, - CancellationToken ct = default) - { - if (imageStream is null) - throw new ArgumentNullException(nameof(imageStream)); - if (string.IsNullOrWhiteSpace(fileName)) - throw new ArgumentException("A file name is required.", nameof(fileName)); - - var endpoint = new Uri(new Uri(Settings.ApiUrl.TrimEnd('/') + "/", UriKind.Absolute), "account/set-avatar"); - - await EnsureFreshTokenAsync(ct).ConfigureAwait(false); - - var attemptUpload = async () => - { - if (imageStream.CanSeek) - imageStream.Position = 0; - - using var content = new MultipartFormDataContent(); - using var fileContent = new StreamContent(imageStream); - fileContent.Headers.ContentType = new MediaTypeHeaderValue( - string.IsNullOrWhiteSpace(contentType) ? "application/octet-stream" : contentType); - content.Add(fileContent, "file", fileName); - - using var request = new HttpRequestMessage(HttpMethod.Post, endpoint) - { - Content = content, - }; - - return await Http.SendAsync(request, ct).ConfigureAwait(false); - }; - - var response = await attemptUpload().ConfigureAwait(false); - if (response.StatusCode == HttpStatusCode.Unauthorized) - { - response.Dispose(); - await ForceRefreshAsync(ct).ConfigureAwait(false); - response = await attemptUpload().ConfigureAwait(false); - } - - await EnsureSuccessOrThrowAsync(response, ct).ConfigureAwait(false); - - var payload = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false); - if (string.IsNullOrWhiteSpace(payload)) - return "Avatar mis à jour."; - - try - { - using var json = JsonDocument.Parse(payload); - if (json.RootElement.TryGetProperty("message", out var msgEl)) - { - var message = msgEl.GetString(); - if (!string.IsNullOrWhiteSpace(message)) - return message; - } - } - catch (JsonException) - { - // Keep a user-friendly fallback when the API payload is not JSON. - } - - return "Avatar mis à jour."; - } - public async Task LogoutAsync() { _store.Clear(); diff --git a/src/PostIt/PostIt/ViewLocator.cs b/src/PostIt/PostIt/ViewLocator.cs index 0055a9e6..2fd51633 100644 --- a/src/PostIt/PostIt/ViewLocator.cs +++ b/src/PostIt/PostIt/ViewLocator.cs @@ -4,9 +4,7 @@ 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; @@ -43,8 +41,8 @@ public class ViewLocator : IDataTemplate HomePageViewModel => services.GetRequiredService(), ActivitiesPageViewModel => services.GetRequiredService(), CommandFormsPageViewModel => services.GetRequiredService(), - BrushViewModel => services.GetRequiredService(), - RdvViewModel => services.GetRequiredService(), + BillingCommandPageViewModel => services.GetRequiredService(), + BillingQueriesPageViewModel => services.GetRequiredService(), SignaturePageViewModel => services.GetRequiredService(), AddCircleMemberDialogViewModel => services.GetRequiredService(), CirclesPageViewModel => services.GetRequiredService(), diff --git a/src/PostIt/PostIt/ViewModels/ActivitiesPageViewModel.cs b/src/PostIt/PostIt/ViewModels/ActivitiesPageViewModel.cs index 2dc8938f..5268d07a 100644 --- a/src/PostIt/PostIt/ViewModels/ActivitiesPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/ActivitiesPageViewModel.cs @@ -20,16 +20,16 @@ public partial class ActivitiesPageViewModel : ViewModelBase private bool _syncingSelection; [ObservableProperty] - public partial ObservableCollection Activities { get; set; } = new(); + public partial ObservableCollection Activities { get; set; } = new(); [ObservableProperty] - public partial ActivityInfo? SelectedActivity { get; set; } + public partial ActivityBrowseItemDto? SelectedActivity { get; set; } [ObservableProperty] - public partial ObservableCollection Specializations { get; set; } = new(); + public partial ObservableCollection Specializations { get; set; } = new(); [ObservableProperty] - public partial ActivityInfo? SelectedSpecialization { get; set; } + public partial ActivityBrowseItemDto? SelectedSpecialization { get; set; } [ObservableProperty] public partial ObservableCollection Performers { get; set; } = new(); @@ -43,7 +43,7 @@ public partial class ActivitiesPageViewModel : ViewModelBase [ObservableProperty] public partial string StatusMessage { get; set; } = "Choisissez une activité."; - public ActivityInfo? CurrentActivity => SelectedSpecialization ?? SelectedActivity; + public ActivityBrowseItemDto? 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(ActivityInfo? value) + partial void OnSelectedActivityChanged(ActivityBrowseItemDto? value) { if (_syncingSelection) return; _ = ShowActivitySafeAsync(value); } - partial void OnSelectedSpecializationChanged(ActivityInfo? value) + partial void OnSelectedSpecializationChanged(ActivityBrowseItemDto? value) { if (_syncingSelection) return; _ = ShowSpecializationSafeAsync(value); } - private async Task ShowActivitySafeAsync(ActivityInfo? value) + private async Task ShowActivitySafeAsync(ActivityBrowseItemDto? value) { try { @@ -94,7 +94,7 @@ public partial class ActivitiesPageViewModel : ViewModelBase } } - private async Task ShowSpecializationSafeAsync(ActivityInfo? value) + private async Task ShowSpecializationSafeAsync(ActivityBrowseItemDto? value) { try { @@ -117,7 +117,7 @@ public partial class ActivitiesPageViewModel : ViewModelBase try { var list = await _client.GetCatalogAsync(); - Activities = new ObservableCollection(list ?? new()); + Activities = new ObservableCollection(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(); - Specializations = new ObservableCollection(); + Activities = new ObservableCollection(); + Specializations = new ObservableCollection(); Performers = new ObservableCollection(); StatusMessage = "Accès refusé pour les activités (scope 'api'). Déconnectez puis reconnectez-vous."; } catch (Exception ex) { - Activities = new ObservableCollection(); - Specializations = new ObservableCollection(); + Activities = new ObservableCollection(); + Specializations = new ObservableCollection(); Performers = new ObservableCollection(); StatusMessage = $"Erreur: {ex.Message}"; } @@ -146,7 +146,7 @@ public partial class ActivitiesPageViewModel : ViewModelBase } } - public async Task ShowActivityAsync(ActivityInfo? activity) + public async Task ShowActivityAsync(ActivityBrowseItemDto? 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(activity?.Children ?? new()); + Specializations = new ObservableCollection(activity?.Children ?? new()); if (activity is null) { @@ -175,7 +175,7 @@ public partial class ActivitiesPageViewModel : ViewModelBase await LoadPerformersAsync(activity); } - public async Task ShowSpecializationAsync(ActivityInfo? specialization) + public async Task ShowSpecializationAsync(ActivityBrowseItemDto? specialization) { _syncingSelection = true; try @@ -203,32 +203,14 @@ public partial class ActivitiesPageViewModel : ViewModelBase await LoadPerformersAsync(specialization); } - private async Task LoadPerformersAsync(ActivityInfo activity) + private async Task LoadPerformersAsync(ActivityBrowseItemDto activity) { IsBusy = true; try { var list = await _client.GetUsersAsync(activity.Code); - var items = (list ?? new()) - .Select(dto => ActivityUserDisplayItem.FromDto(dto, _client.BuildAvatarXsUrl(dto.UserName))) - .ToList(); - - await Task.WhenAll(items.Select(async item => - { - if (string.IsNullOrWhiteSpace(item.AvatarXsUrl)) - { - return; - } - - if (!Uri.TryCreate(item.AvatarXsUrl, UriKind.Absolute, out var avatarUri)) - { - return; - } - - item.AvatarImage = await ImageHelper.LoadFromWeb(avatarUri); - })); - - Performers = new ObservableCollection(items); + Performers = new ObservableCollection((list ?? new()) + .Select(ActivityUserDisplayItem.FromDto)); SelectedPerformer = null; StatusMessage = $"{activity.Name} · {Performers.Count} utilisateur(s)"; } diff --git a/src/PostIt/PostIt/ViewModels/ActivityUserDisplayItem.cs b/src/PostIt/PostIt/ViewModels/ActivityUserDisplayItem.cs index d89a66e9..ee06b883 100644 --- a/src/PostIt/PostIt/ViewModels/ActivityUserDisplayItem.cs +++ b/src/PostIt/PostIt/ViewModels/ActivityUserDisplayItem.cs @@ -1,13 +1,10 @@ -using Avalonia.Media.Imaging; -using CommunityToolkit.Mvvm.ComponentModel; using Yavsc.Abstract.Workflow; namespace PostIt.ViewModels; -public sealed partial class ActivityUserDisplayItem : ObservableObject +public sealed class ActivityUserDisplayItem { public string PerformerId { get; init; } = string.Empty; - public string AvatarXsUrl { get; init; } = string.Empty; public bool HasPerformerProfile { get; init; } public string PerformerBadgeLabel { get; init; } = "Profil pro"; public bool IsPerformerActive { get; init; } @@ -16,25 +13,17 @@ public sealed partial class ActivityUserDisplayItem : ObservableObject public string PerformerStatusBadgeBorder { get; init; } = "#C62828"; public string PerformerStatusBadgeForeground { get; init; } = "#8E0000"; public string UserName { get; init; } = string.Empty; - public string AvatarFallbackLabel { get; init; } = "?"; public string WebSite { get; init; } = string.Empty; public int ExtraActivityCount { get; init; } public string ExtraActivityLabel { get; init; } = "Pas d'autre activité"; - [ObservableProperty] - public partial Bitmap? AvatarImage { get; set; } - - public static ActivityUserDisplayItem FromDto(PerformerActivity dto, string avatarXsUrl) + public static ActivityUserDisplayItem FromDto(ActivityPerformerDto dto) { return new ActivityUserDisplayItem { PerformerId = dto.PerformerId, - AvatarXsUrl = avatarXsUrl, HasPerformerProfile = dto.HasPerformerProfile, UserName = dto.UserName, - AvatarFallbackLabel = string.IsNullOrWhiteSpace(dto.UserName) - ? "?" - : dto.UserName.Trim()[0].ToString().ToUpperInvariant(), WebSite = dto.WebSite, IsPerformerActive = dto.Active, PerformerStatusBadgeLabel = dto.Active ? "Actif" : "Inactif", diff --git a/src/PostIt/PostIt/ViewModels/BillingCommandPageViewModel.cs b/src/PostIt/PostIt/ViewModels/BillingCommandPageViewModel.cs new file mode 100644 index 00000000..c9050cbf --- /dev/null +++ b/src/PostIt/PostIt/ViewModels/BillingCommandPageViewModel.cs @@ -0,0 +1,399 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Net; +using System.Net.Http; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Yavsc; +using Yavsc.Abstract.Workflow; +using Yavsc.Api.Client; +using Yavsc.Models.Billing; +using Yavsc.Models.Haircut; +using Yavsc.Models.Relationship; + +namespace PostIt.ViewModels; + +public partial class BillingCommandPageViewModel : ViewModelBase +{ + private readonly BillingApiClient _billingClient; + + public ActivityBrowseItemDto Activity { get; } + public ActivityUserDisplayItem Performer { get; } + public CommandFormSummaryDto Form { get; } + + [ObservableProperty] + public partial bool IsBusy { get; set; } + + [ObservableProperty] + public partial string StatusMessage { get; set; } + + [ObservableProperty] + public partial string EventDateText { get; set; } + + [ObservableProperty] + public partial string Reason { get; set; } = string.Empty; + + [ObservableProperty] + public partial string Address { get; set; } = string.Empty; + + [ObservableProperty] + public partial string LatitudeText { get; set; } = string.Empty; + + [ObservableProperty] + public partial string LongitudeText { get; set; } = string.Empty; + + [ObservableProperty] + public partial bool Consent { get; set; } = true; + + [ObservableProperty] + public partial ObservableCollection AvailablePrestations { get; set; } = new(); + + [ObservableProperty] + public partial HairPrestationDto? SelectedPrestation { get; set; } + + [ObservableProperty] + public partial ObservableCollection MultiPrestations { get; set; } = new(); + + [ObservableProperty] + public partial string AdditionalInfo { get; set; } = string.Empty; + + [ObservableProperty] + public partial long? ExistingQueryId { get; set; } + + [ObservableProperty] + public partial QueryStatus CommandStatus { get; set; } = QueryStatus.Inserted; + + public string Title => Form.Title; + public string PerformerLabel => Performer.UserName; + public string ActivityLabel => Activity.Name; + public bool IsSupported => IsRdv || IsBrush || IsMultiBrush; + public bool IsRdv => string.Equals(Form.ActionName, BillingCodes.Rdv, StringComparison.Ordinal); + public bool IsBrush => string.Equals(Form.ActionName, BillingCodes.Brush, StringComparison.Ordinal); + public bool IsMultiBrush => string.Equals(Form.ActionName, BillingCodes.MBrush, StringComparison.Ordinal); + public bool ShowsReason => IsRdv; + public bool ShowsAdditionalInfo => IsBrush; + public bool ShowsSinglePrestation => IsBrush; + public bool ShowsMultiplePrestations => IsMultiBrush; + public string BillingRoute => $"/billing/{Form.ActionName}"; + public bool IsEditingExisting => ExistingQueryId.HasValue; + public string SubmitLabel => IsEditingExisting ? "Mettre à jour la commande" : "Poster la commande"; + public string SupportMessage => IsSupported + ? IsRdv + ? "Complétez les informations du rendez-vous puis postez la commande." + : IsBrush + ? "Choisissez une prestation coiffure puis postez la commande." + : "Choisissez une ou plusieurs prestations coiffure puis postez la commande." + : $"Le formulaire {Form.ActionName} n'est pas encore pris en charge dans PostIt."; + + public override bool CanNavigateNext + { + get => false; + protected set { _ = value; } + } + + public override bool CanNavigatePrevious + { + get => true; + protected set { _ = value; } + } + + public BillingCommandPageViewModel( + ActivityBrowseItemDto activity, + ActivityUserDisplayItem performer, + CommandFormSummaryDto form, + BillingApiClient billingClient) + { + Activity = activity ?? throw new ArgumentNullException(nameof(activity)); + Performer = performer ?? throw new ArgumentNullException(nameof(performer)); + Form = form ?? throw new ArgumentNullException(nameof(form)); + _billingClient = billingClient ?? throw new ArgumentNullException(nameof(billingClient)); + + EventDateText = DateTime.Now.AddDays(1).ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture); + StatusMessage = SupportMessage; + } + + partial void OnExistingQueryIdChanged(long? value) + { + OnPropertyChanged(nameof(IsEditingExisting)); + OnPropertyChanged(nameof(SubmitLabel)); + } + + public async Task InitializeAsync(BillingQueryDetailsDto? existingQuery = null) + { + if (!IsBrush && !IsMultiBrush) + { + if (existingQuery is not null) + { + ApplyExistingQuery(existingQuery); + } + + return; + } + + IsBusy = true; + try + { + var prestations = await _billingClient.GetHairPrestationsAsync(Form.ActionName).ConfigureAwait(true); + AvailablePrestations = new ObservableCollection(prestations ?? new List()); + SelectedPrestation = AvailablePrestations.FirstOrDefault(); + MultiPrestations = new ObservableCollection(AvailablePrestations.Select(SelectableHairPrestationItem.FromDto)); + + StatusMessage = AvailablePrestations.Count == 0 + ? "Aucune prestation coiffure disponible." + : SupportMessage; + } + catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden) + { + StatusMessage = "Accès refusé au catalogue de prestations (scope 'api'). Déconnectez puis reconnectez-vous."; + } + catch (Exception ex) + { + StatusMessage = $"Erreur lors du chargement des prestations: {ex.Message}"; + } + finally + { + IsBusy = false; + } + + if (existingQuery is not null) + { + ApplyExistingQuery(existingQuery); + } + } + + [RelayCommand] + private async Task SubmitAsync() + { + if (!IsSupported) + { + StatusMessage = SupportMessage; + return; + } + + if (!Consent) + { + StatusMessage = "Le consentement est requis pour poster la commande."; + return; + } + + if (!TryParseEventDate(out var eventDate)) + { + StatusMessage = "La date doit être saisie au format yyyy-MM-dd HH:mm."; + return; + } + + if (IsRdv && string.IsNullOrWhiteSpace(Reason)) + { + StatusMessage = "Le motif du rendez-vous est requis."; + return; + } + + if (string.IsNullOrWhiteSpace(Address)) + { + StatusMessage = "L'adresse du rendez-vous est requise."; + return; + } + + if (!TryParseCoordinate(LatitudeText, out var latitude)) + { + StatusMessage = "Latitude invalide."; + return; + } + + if (!TryParseCoordinate(LongitudeText, out var longitude)) + { + StatusMessage = "Longitude invalide."; + return; + } + + IsBusy = true; + try + { + var location = new Location + { + Address = Address.Trim(), + Latitude = latitude, + Longitude = longitude, + }; + + var payload = new BillingQueryDetailsDto + { + Id = ExistingQueryId ?? 0, + BillingCode = Form.ActionName, + ActivityCode = Activity.Code, + PerformerId = Performer.PerformerId, + Consent = Consent, + EventDate = eventDate, + Status = CommandStatus, + Reason = Reason.Trim(), + AdditionalInfo = string.IsNullOrWhiteSpace(AdditionalInfo) ? string.Empty : AdditionalInfo.Trim(), + Location = new BillingLocationDto + { + Address = location.Address, + Latitude = location.Latitude, + Longitude = location.Longitude, + } + }; + + if (IsRdv) + { + if (IsEditingExisting) + { + await _billingClient.UpdateAsync(Form.ActionName, ExistingQueryId!.Value, payload).ConfigureAwait(true); + } + else + { + await _billingClient.CreateAsync(Form.ActionName, new + { + ActivityCode = Activity.Code, + PerformerId = Performer.PerformerId, + Consent, + EventDate = eventDate, + Location = location, + Reason = payload.Reason, + Status = payload.Status, + }).ConfigureAwait(true); + } + } + else if (IsBrush) + { + if (SelectedPrestation is null) + { + StatusMessage = "Sélectionnez une prestation coiffure."; + return; + } + + payload.PrestationId = SelectedPrestation.Id; + + if (IsEditingExisting) + { + await _billingClient.UpdateAsync(Form.ActionName, ExistingQueryId!.Value, payload).ConfigureAwait(true); + } + else + { + await _billingClient.CreateAsync(Form.ActionName, new + { + ActivityCode = Activity.Code, + PerformerId = Performer.PerformerId, + Consent, + EventDate = (DateTime?)eventDate, + Location = location, + PrestationId = SelectedPrestation.Id, + AdditionalInfo = string.IsNullOrWhiteSpace(AdditionalInfo) ? null : AdditionalInfo.Trim(), + Status = payload.Status, + }).ConfigureAwait(true); + } + } + else if (IsMultiBrush) + { + var selectedPrestations = MultiPrestations.Where(x => x.IsSelected).ToList(); + if (selectedPrestations.Count == 0) + { + StatusMessage = "Sélectionnez au moins une prestation coiffure."; + return; + } + + payload.PrestationIds = selectedPrestations.Select(x => x.Id).ToList(); + + if (IsEditingExisting) + { + await _billingClient.UpdateAsync(Form.ActionName, ExistingQueryId!.Value, payload).ConfigureAwait(true); + } + else + { + await _billingClient.CreateAsync(Form.ActionName, new + { + ActivityCode = Activity.Code, + PerformerId = Performer.PerformerId, + Consent, + EventDate = eventDate, + Location = location, + Prestations = selectedPrestations.Select(x => new { PrestationId = x.Id }).ToList(), + Status = payload.Status, + }).ConfigureAwait(true); + } + } + + StatusMessage = IsEditingExisting + ? $"Commande #{ExistingQueryId} mise à jour sur {BillingRoute} pour {Performer.UserName}." + : $"Commande transmise sur {BillingRoute} pour {Performer.UserName}."; + } + catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden) + { + StatusMessage = "Accès refusé au billing (scope 'api'). Déconnectez puis reconnectez-vous."; + } + catch (Exception ex) + { + StatusMessage = $"Erreur lors de l'envoi: {ex.Message}"; + } + finally + { + IsBusy = false; + } + } + + private void ApplyExistingQuery(BillingQueryDetailsDto existingQuery) + { + ExistingQueryId = existingQuery.Id; + CommandStatus = existingQuery.Status; + Consent = existingQuery.Consent; + Reason = existingQuery.Reason ?? string.Empty; + AdditionalInfo = existingQuery.AdditionalInfo ?? string.Empty; + + if (existingQuery.EventDate is not null) + { + EventDateText = existingQuery.EventDate.Value + .ToLocalTime() + .ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture); + } + + if (existingQuery.Location is not null) + { + Address = existingQuery.Location.Address ?? string.Empty; + LatitudeText = existingQuery.Location.Latitude.ToString(CultureInfo.InvariantCulture); + LongitudeText = existingQuery.Location.Longitude.ToString(CultureInfo.InvariantCulture); + } + + if (IsBrush && existingQuery.PrestationId is not null) + { + SelectedPrestation = AvailablePrestations.FirstOrDefault(x => x.Id == existingQuery.PrestationId.Value); + } + + if (IsMultiBrush) + { + var selectedIds = existingQuery.PrestationIds is null + ? new HashSet() + : new HashSet(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); + } +} \ No newline at end of file diff --git a/src/PostIt/PostIt/ViewModels/BillingQueriesPageViewModel.cs b/src/PostIt/PostIt/ViewModels/BillingQueriesPageViewModel.cs index d5aba021..d680ba74 100644 --- a/src/PostIt/PostIt/ViewModels/BillingQueriesPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/BillingQueriesPageViewModel.cs @@ -18,9 +18,9 @@ public partial class BillingQueriesPageViewModel : ViewModelBase { private readonly BillingApiClient _billingClient; - public ActivityInfo Activity { get; } + public ActivityBrowseItemDto Activity { get; } public ActivityUserDisplayItem Performer { get; } - public CommandFormSummary Form { get; } + public CommandFormSummaryDto Form { get; } public bool IsReadOnly { get; } public bool OngoingOnly { get; } @@ -55,9 +55,9 @@ public partial class BillingQueriesPageViewModel : ViewModelBase } public BillingQueriesPageViewModel( - ActivityInfo activity, + ActivityBrowseItemDto activity, ActivityUserDisplayItem performer, - CommandFormSummary form, + CommandFormSummaryDto 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 = Form.CreateCommandPageViewModel(Activity, Performer, _billingClient); - await vm!.InitializeAsync(details).ConfigureAwait(true); + var vm = new BillingCommandPageViewModel(Activity, Performer, Form, _billingClient); + await vm.InitializeAsync(details).ConfigureAwait(true); await app.PushPageAsync(vm).ConfigureAwait(true); } catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden) @@ -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; -} +} \ No newline at end of file diff --git a/src/PostIt/PostIt/ViewModels/CommandFormsPageViewModel.cs b/src/PostIt/PostIt/ViewModels/CommandFormsPageViewModel.cs index 71f5d67a..e1453c11 100644 --- a/src/PostIt/PostIt/ViewModels/CommandFormsPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/CommandFormsPageViewModel.cs @@ -15,14 +15,14 @@ public partial class CommandFormsPageViewModel : ViewModelBase { private readonly BillingApiClient _billingClient; - public ActivityInfo Activity { get; } + public ActivityBrowseItemDto Activity { get; } public ActivityUserDisplayItem Performer { get; } [ObservableProperty] - public partial ObservableCollection Forms { get; set; } + public partial ObservableCollection Forms { get; set; } [ObservableProperty, NotifyCanExecuteChangedFor(nameof(OpenSelectedFormCommand)), NotifyCanExecuteChangedFor(nameof(OpenQueriesCommand)), NotifyCanExecuteChangedFor(nameof(OpenOngoingQueriesCommand))] - public partial CommandFormSummary? SelectedForm { get; set; } + public partial CommandFormSummaryDto? SelectedForm { get; set; } [ObservableProperty] public partial string StatusMessage { get; set; } @@ -43,7 +43,7 @@ public partial class CommandFormsPageViewModel : ViewModelBase } public CommandFormsPageViewModel( - ActivityInfo activity, + ActivityBrowseItemDto 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((activity.Forms ?? new()) + Forms = new ObservableCollection((activity.Forms ?? new()) .OrderBy(f => f.Title) .ThenBy(f => f.ActionName)); SelectedForm = Forms.FirstOrDefault(); @@ -81,9 +81,8 @@ public partial class CommandFormsPageViewModel : ViewModelBase throw new InvalidOperationException("Application PostIt indisponible."); } - var vm = SelectedForm.CreateCommandPageViewModel( - Activity, Performer, _billingClient); - await vm!.InitializeAsync(); + var vm = new BillingCommandPageViewModel(Activity, Performer, SelectedForm, _billingClient); + await vm.InitializeAsync(); await app.PushPageAsync(vm); } @@ -132,4 +131,4 @@ public partial class CommandFormsPageViewModel : ViewModelBase await vm.InitializeAsync(); await app.PushPageAsync(vm); } -} +} \ No newline at end of file diff --git a/src/PostIt/PostIt/ViewModels/Commands/BillingCommandPageViewModel.cs b/src/PostIt/PostIt/ViewModels/Commands/BillingCommandPageViewModel.cs deleted file mode 100644 index 0ab76291..00000000 --- a/src/PostIt/PostIt/ViewModels/Commands/BillingCommandPageViewModel.cs +++ /dev/null @@ -1,108 +0,0 @@ -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(); -} diff --git a/src/PostIt/PostIt/ViewModels/Commands/BrushViewModel.cs b/src/PostIt/PostIt/ViewModels/Commands/BrushViewModel.cs deleted file mode 100644 index d49fde80..00000000 --- a/src/PostIt/PostIt/ViewModels/Commands/BrushViewModel.cs +++ /dev/null @@ -1,165 +0,0 @@ -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; -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 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 - (prestations ?? new List()); - - 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; - } - - } -} diff --git a/src/PostIt/PostIt/ViewModels/Commands/MBrushViewModel.cs b/src/PostIt/PostIt/ViewModels/Commands/MBrushViewModel.cs deleted file mode 100644 index 793a7751..00000000 --- a/src/PostIt/PostIt/ViewModels/Commands/MBrushViewModel.cs +++ /dev/null @@ -1,130 +0,0 @@ -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 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( - AvailablePrestations.Select(SelectableHairPrestationItem.FromDto)); - } - - protected override void ApplyExistingQuery(BillingQueryDetailsDto existingQuery) - { - base.ApplyExistingQuery(existingQuery); - - var selectedIds = existingQuery.PrestationIds is null - ? new HashSet() - : new HashSet(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; - } - } -} diff --git a/src/PostIt/PostIt/ViewModels/Commands/RdvViewModel.cs b/src/PostIt/PostIt/ViewModels/Commands/RdvViewModel.cs deleted file mode 100644 index c2ff3294..00000000 --- a/src/PostIt/PostIt/ViewModels/Commands/RdvViewModel.cs +++ /dev/null @@ -1,211 +0,0 @@ -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; - } -} diff --git a/src/PostIt/PostIt/ViewModels/HomePageViewModel.cs b/src/PostIt/PostIt/ViewModels/HomePageViewModel.cs index 830be4dd..cfd30650 100644 --- a/src/PostIt/PostIt/ViewModels/HomePageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/HomePageViewModel.cs @@ -32,7 +32,7 @@ public class HomePageViewModel : ViewModelBase OpenActivities = new AsyncRelayCommand(OpenActivitiesAsync); } - public IAsyncRelayCommand OpenBlogs { get; } = new AsyncRelayCommand(App.PushBlogsPageAsync); + public IAsyncRelayCommand OpenBlogs { get; } = new AsyncRelayCommand(App.PushMainPageAsync); public IAsyncRelayCommand OpenActivities { get; } private async Task OpenActivitiesAsync() diff --git a/src/PostIt/PostIt/ViewModels/RemoteViewModelBase.cs b/src/PostIt/PostIt/ViewModels/RemoteViewModelBase.cs deleted file mode 100644 index 0229cca4..00000000 --- a/src/PostIt/PostIt/ViewModels/RemoteViewModelBase.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Threading.Tasks; - -namespace PostIt.ViewModels; - -public abstract class RemoteViewModelBase : ViewModelBase -{ - public abstract Task LoadAsync(); - - -} diff --git a/src/PostIt/PostIt/ViewModels/Settings/Settings.cs b/src/PostIt/PostIt/ViewModels/Settings/Settings.cs index e63f0b32..0c7a040b 100644 --- a/src/PostIt/PostIt/ViewModels/Settings/Settings.cs +++ b/src/PostIt/PostIt/ViewModels/Settings/Settings.cs @@ -26,7 +26,7 @@ public partial class Settings : ViewModelBase public partial string BlogsApiUrl { get; set; } = "https://blogs.pschneider.fr/api/v1/"; [ObservableProperty] - public partial string ApiUrl { get; set; } = "https://api.pschneider.fr/api/v1/"; + public partial string BusinessApiUrl { get; set; } = "https://api.pschneider.fr/api/v1/"; [ObservableProperty] public partial string SearchText { get; set; } = string.Empty; @@ -45,7 +45,7 @@ public partial class Settings : ViewModelBase partial void OnDarkModeChanged(bool value) => MarkDirty(); partial void OnBlogsApiUrlChanged(string value) => MarkDirty(); - partial void OnApiUrlChanged(string value) => MarkDirty(); + partial void OnBusinessApiUrlChanged(string value) => MarkDirty(); partial void OnSearchTextChanged(string value) => MarkDirty(); /// @@ -306,9 +306,9 @@ public partial class Settings : ViewModelBase this.BlogsApiUrl = !string.IsNullOrWhiteSpace(settings.BlogsApiUrl) ? settings.BlogsApiUrl : legacyApiUrl ?? this.BlogsApiUrl; - this.ApiUrl = !string.IsNullOrWhiteSpace(settings.ApiUrl) - ? settings.ApiUrl - : this.ApiUrl; + this.BusinessApiUrl = !string.IsNullOrWhiteSpace(settings.BusinessApiUrl) + ? settings.BusinessApiUrl + : this.BusinessApiUrl; this.SearchText = settings.SearchText ?? string.Empty; if (!(settings.Authentication is null)) { @@ -384,7 +384,7 @@ public partial class Settings : ViewModelBase }; this.DarkMode = false; this.BlogsApiUrl = "https://blogs.pschneider.fr/api/v1/"; - this.ApiUrl = "https://api.pschneider.fr/api/v1/"; + this.BusinessApiUrl = "https://api.pschneider.fr/api/v1/"; this.SearchText = string.Empty; } diff --git a/src/PostIt/PostIt/ViewModels/ViewModelBase.cs b/src/PostIt/PostIt/ViewModels/ViewModelBase.cs index 30783764..4ca69eea 100644 --- a/src/PostIt/PostIt/ViewModels/ViewModelBase.cs +++ b/src/PostIt/PostIt/ViewModels/ViewModelBase.cs @@ -1,8 +1,8 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; namespace PostIt.ViewModels; -public abstract class ViewModelBase : ObservableObject +public abstract partial class ViewModelBase : ObservableObject { /// /// Gets if the user can navigate to the next page diff --git a/src/PostIt/PostIt/Views/ActivitiesPage.axaml b/src/PostIt/PostIt/Views/ActivitiesPage.axaml index 16d88344..31d7be5f 100644 --- a/src/PostIt/PostIt/Views/ActivitiesPage.axaml +++ b/src/PostIt/PostIt/Views/ActivitiesPage.axaml @@ -21,8 +21,8 @@ -