feat(postit): migrate editable profile data and admin profile flow
This commit is contained in:
parent
f8c2a01ef4
commit
af1e572f32
38 changed files with 6573 additions and 19 deletions
|
|
@ -12,4 +12,68 @@ public class HomePageProviderFlowTests
|
|||
Assert.NotNull(vm.OpenProviderRequests);
|
||||
Assert.True(vm.OpenProviderRequests.CanExecute(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HomePage_exposes_profile_command()
|
||||
{
|
||||
var vm = new HomePageViewModel();
|
||||
|
||||
Assert.NotNull(vm.OpenProfile);
|
||||
Assert.True(vm.OpenProfile.CanExecute(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HomePage_exposes_performer_configuration_command()
|
||||
{
|
||||
var vm = new HomePageViewModel();
|
||||
|
||||
Assert.NotNull(vm.OpenPerformerConfiguration);
|
||||
Assert.True(vm.OpenPerformerConfiguration.CanExecute(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HomePage_exposes_administration_command()
|
||||
{
|
||||
var vm = new HomePageViewModel();
|
||||
|
||||
Assert.NotNull(vm.OpenAdministration);
|
||||
Assert.True(vm.OpenAdministration.CanExecute(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Administration_page_filters_users_by_search_text()
|
||||
{
|
||||
var vm = new AdministrationPageViewModel();
|
||||
vm.Users.Add(new Yavsc.Abstract.Identity.UserInfo { UserId = "1", UserName = "alice" });
|
||||
vm.Users.Add(new Yavsc.Abstract.Identity.UserInfo { UserId = "2", UserName = "bob" });
|
||||
vm.Users.Add(new Yavsc.Abstract.Identity.UserInfo { UserId = "3", UserName = "carol" });
|
||||
|
||||
vm.SearchText = "ali";
|
||||
|
||||
Assert.Single(vm.FilteredUsers);
|
||||
Assert.Equal("alice", vm.FilteredUsers[0].UserName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserProfilePage_exposes_full_name_and_address_fields()
|
||||
{
|
||||
var vm = new UserProfilePageViewModel();
|
||||
|
||||
Assert.Equal("—", vm.FullName);
|
||||
Assert.Equal("—", vm.Address);
|
||||
Assert.NotNull(vm.RefreshCommand);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UserProfilePage_can_save_profile_values()
|
||||
{
|
||||
var vm = new UserProfilePageViewModel
|
||||
{
|
||||
FullName = "Alice Example",
|
||||
Address = "14 rue de l’Érable"
|
||||
};
|
||||
|
||||
Assert.NotNull(vm.SaveProfileCommand);
|
||||
Assert.True(vm.SaveProfileCommand.CanExecute(null));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
117
src/PostIt/PostIt.Tests/ResourceUsageApiClientTests.cs
Normal file
117
src/PostIt/PostIt.Tests/ResourceUsageApiClientTests.cs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using Yavsc.Abstract.Resources;
|
||||
using Yavsc.Api.Client;
|
||||
|
||||
namespace PostIt.Tests;
|
||||
|
||||
public class ResourceUsageApiClientTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetCurrentAsync_calls_user_usage_endpoint()
|
||||
{
|
||||
var fake = new FakeYavscApiClient();
|
||||
var client = new ResourceUsageApiClient(fake, "https://example.invalid/api/v1/");
|
||||
|
||||
var usage = await client.GetCurrentAsync();
|
||||
|
||||
Assert.Equal("https://example.invalid/api/v1/resource-usage/me", fake.LastPath);
|
||||
Assert.Equal("GET", fake.LastMethod);
|
||||
Assert.Equal(12.5m, usage.ApiCalls);
|
||||
Assert.Equal(3.2m, usage.CpuSeconds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAdminOverviewAsync_calls_admin_usage_endpoint()
|
||||
{
|
||||
var fake = new FakeYavscApiClient();
|
||||
var client = new ResourceUsageApiClient(fake, "https://example.invalid/api/v1/");
|
||||
|
||||
var usage = await client.GetAdminOverviewAsync();
|
||||
|
||||
Assert.Equal("https://example.invalid/api/v1/resource-usage/admin", fake.LastPath);
|
||||
Assert.Equal("GET", fake.LastMethod);
|
||||
Assert.Equal(2, usage.Count);
|
||||
Assert.Equal("administrator", usage[0].UserName);
|
||||
}
|
||||
|
||||
private sealed class FakeYavscApiClient : IYavscApiClient
|
||||
{
|
||||
public HttpClient Http { get; } = new();
|
||||
public string? LastPath { get; private set; }
|
||||
public string? LastMethod { get; private set; }
|
||||
|
||||
public Task<T> CallAsync<T>(HttpMethod method, string path, object? body = null, CancellationToken ct = default)
|
||||
{
|
||||
LastMethod = method.Method;
|
||||
LastPath = path;
|
||||
|
||||
if (typeof(T) == typeof(ResourceUsageSummary))
|
||||
{
|
||||
var summary = new ResourceUsageSummary
|
||||
{
|
||||
UserId = "user-42",
|
||||
UserName = "alice",
|
||||
ApiCalls = 12.5m,
|
||||
CpuSeconds = 3.2m,
|
||||
BandwidthMb = 18.4m,
|
||||
StorageMb = 51.2m,
|
||||
EstimatedCompensation = 4.9m,
|
||||
Currency = "EUR",
|
||||
RecordedAtUtc = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
return Task.FromResult((T)(object)summary);
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(List<ResourceUsageSummary>))
|
||||
{
|
||||
var list = new List<ResourceUsageSummary>
|
||||
{
|
||||
new()
|
||||
{
|
||||
UserId = "admin-user",
|
||||
UserName = "administrator",
|
||||
ApiCalls = 90m,
|
||||
CpuSeconds = 14m,
|
||||
BandwidthMb = 500m,
|
||||
StorageMb = 2048m,
|
||||
EstimatedCompensation = 32m,
|
||||
Currency = "EUR",
|
||||
RecordedAtUtc = DateTimeOffset.UtcNow
|
||||
},
|
||||
new()
|
||||
{
|
||||
UserId = "user-99",
|
||||
UserName = "bob",
|
||||
ApiCalls = 40m,
|
||||
CpuSeconds = 7m,
|
||||
BandwidthMb = 120m,
|
||||
StorageMb = 600m,
|
||||
EstimatedCompensation = 15m,
|
||||
Currency = "EUR",
|
||||
RecordedAtUtc = DateTimeOffset.UtcNow
|
||||
}
|
||||
};
|
||||
|
||||
return Task.FromResult((T)(object)list);
|
||||
}
|
||||
|
||||
throw new NotSupportedException($"Unsupported type: {typeof(T).FullName}");
|
||||
}
|
||||
|
||||
public Task<T> CallAsync<T>(HttpMethod method, string path, Func<HttpContent> contentFactory, CancellationToken ct = default)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task CallAsync(HttpMethod method, string path, Func<HttpContent> contentFactory, CancellationToken ct = default)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
public Task<byte[]> DownloadAsync(HttpMethod method, string path, CancellationToken ct = default)
|
||||
=> Task.FromResult(Array.Empty<byte>());
|
||||
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
@ -76,6 +76,9 @@ public static class ServiceCollectionHelpers
|
|||
services.AddTransient<EstimateListPage>();
|
||||
services.AddTransient<EstimateValidationPage>();
|
||||
services.AddTransient<MyFilesPage>();
|
||||
services.AddTransient<UserProfilePage>();
|
||||
services.AddTransient<PerformerConfigurationPage>();
|
||||
services.AddTransient<AdministrationPage>();
|
||||
services.AddTransient<ResourceUsagePage>();
|
||||
|
||||
// ViewModels
|
||||
|
|
@ -95,6 +98,9 @@ public static class ServiceCollectionHelpers
|
|||
services.AddSingleton<IUserDirectory>(userDirectory);
|
||||
services.AddSingleton<AboutPageViewModel>();
|
||||
services.AddSingleton<HomePageViewModel>();
|
||||
services.AddTransient<UserProfilePageViewModel>();
|
||||
services.AddTransient<PerformerConfigurationPageViewModel>();
|
||||
services.AddTransient<AdministrationPageViewModel>();
|
||||
services.AddSingleton<SignaturePageViewModel>();
|
||||
services.AddSingleton<CirclesPageViewModel>();
|
||||
services.AddSingleton<ActivitiesPageViewModel>();
|
||||
|
|
|
|||
|
|
@ -60,6 +60,9 @@ public class ViewLocator : IDataTemplate
|
|||
EstimateListPageViewModel => services.GetRequiredService<EstimateListPage>(),
|
||||
EstimateValidationPageViewModel => services.GetRequiredService<EstimateValidationPage>(),
|
||||
MyFilesViewModel => services.GetRequiredService<MyFilesPage>(),
|
||||
UserProfilePageViewModel => services.GetRequiredService<UserProfilePage>(),
|
||||
PerformerConfigurationPageViewModel => services.GetRequiredService<PerformerConfigurationPage>(),
|
||||
AdministrationPageViewModel => services.GetRequiredService<AdministrationPage>(),
|
||||
ResourceUsagePageViewModel => services.GetRequiredService<ResourceUsagePage>(),
|
||||
ChatViewModel => services.GetRequiredService<ChatPage>(),
|
||||
null => new TextBlock { Text = "No view for <null>" },
|
||||
|
|
|
|||
95
src/PostIt/PostIt/ViewModels/AdministrationPageViewModel.cs
Normal file
95
src/PostIt/PostIt/ViewModels/AdministrationPageViewModel.cs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using PostIt.Services;
|
||||
using Yavsc.Abstract.Identity;
|
||||
|
||||
namespace PostIt.ViewModels;
|
||||
|
||||
public partial class AdministrationPageViewModel : ViewModelBase
|
||||
{
|
||||
private readonly YavscApiClient? _api;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsBusy { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<UserInfo> Users { get; set; } = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string SearchText { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string StatusMessage { get; set; } = "Chargement…";
|
||||
|
||||
public IReadOnlyList<UserInfo> FilteredUsers =>
|
||||
string.IsNullOrWhiteSpace(SearchText)
|
||||
? Users.ToList()
|
||||
: Users.Where(user =>
|
||||
(user.UserName ?? string.Empty).Contains(SearchText, StringComparison.OrdinalIgnoreCase)
|
||||
|| (user.Email ?? string.Empty).Contains(SearchText, StringComparison.OrdinalIgnoreCase)
|
||||
|| (user.UserId ?? string.Empty).Contains(SearchText, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
public override bool CanNavigateNext { get => false; protected set { _ = value; } }
|
||||
public override bool CanNavigatePrevious { get => true; protected set { _ = value; } }
|
||||
|
||||
public AdministrationPageViewModel(YavscApiClient? api)
|
||||
{
|
||||
_api = api;
|
||||
}
|
||||
|
||||
public AdministrationPageViewModel() : this(null) { }
|
||||
|
||||
public Task InitializeAsync() => RefreshAsync();
|
||||
|
||||
partial void OnSearchTextChanged(string value)
|
||||
{
|
||||
OnPropertyChanged(nameof(FilteredUsers));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task RefreshAsync()
|
||||
{
|
||||
if (_api is null)
|
||||
{
|
||||
StatusMessage = "Client API d’administration indisponible.";
|
||||
return;
|
||||
}
|
||||
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
var query = SearchText.Trim();
|
||||
var usersEndpoint = string.IsNullOrWhiteSpace(query)
|
||||
? new Uri(new Uri(_api.Settings.ApiUrl.TrimEnd('/') + "/", UriKind.Absolute), "users").ToString()
|
||||
: new Uri(new Uri(_api.Settings.ApiUrl.TrimEnd('/') + "/", UriKind.Absolute), $"users/search/{Uri.EscapeDataString(query)}").ToString();
|
||||
|
||||
var users = await _api.CallAsync<List<UserInfo>>(HttpMethod.Get, usersEndpoint).ConfigureAwait(true);
|
||||
|
||||
Users.Clear();
|
||||
foreach (var user in users ?? [])
|
||||
{
|
||||
Users.Add(user);
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(FilteredUsers));
|
||||
StatusMessage = Users.Count > 0 ? $"{Users.Count} utilisateur(s) chargé(s)." : "Aucun utilisateur trouvé.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Users.Clear();
|
||||
OnPropertyChanged(nameof(FilteredUsers));
|
||||
StatusMessage = $"Accès administration refusé ou indisponible : {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -36,6 +36,9 @@ public class HomePageViewModel : ViewModelBase
|
|||
OpenProviderEstimates = new AsyncRelayCommand(() => OpenEstimateListAsync(EstimateListPerspective.Provider));
|
||||
OpenBlogs = new AsyncRelayCommand(App.PushBlogsPageAsync);
|
||||
OpenMyFiles = new AsyncRelayCommand(OpenMyFilesAsync);
|
||||
OpenProfile = new AsyncRelayCommand(OpenProfileAsync);
|
||||
OpenPerformerConfiguration = new AsyncRelayCommand(OpenPerformerConfigurationAsync);
|
||||
OpenAdministration = new AsyncRelayCommand(OpenAdministrationAsync);
|
||||
OpenResourceUsage = new AsyncRelayCommand(OpenResourceUsageAsync);
|
||||
OpenChat = new AsyncRelayCommand(OpenChatAsync);
|
||||
}
|
||||
|
|
@ -43,6 +46,9 @@ public class HomePageViewModel : ViewModelBase
|
|||
|
||||
public IAsyncRelayCommand OpenChat { get; }
|
||||
public IAsyncRelayCommand OpenBlogs { get; }
|
||||
public IAsyncRelayCommand OpenProfile { get; }
|
||||
public IAsyncRelayCommand OpenPerformerConfiguration { get; }
|
||||
public IAsyncRelayCommand OpenAdministration { get; }
|
||||
public IAsyncRelayCommand OpenResourceUsage { get; }
|
||||
public IAsyncRelayCommand OpenActivities { get; }
|
||||
public IAsyncRelayCommand OpenProviderRequests { get; }
|
||||
|
|
@ -140,6 +146,60 @@ public class HomePageViewModel : ViewModelBase
|
|||
await app.PushPageAsync(vm);
|
||||
}
|
||||
|
||||
private async Task OpenProfileAsync()
|
||||
{
|
||||
var app = (App?)Application.Current;
|
||||
if (app is null)
|
||||
{
|
||||
throw new InvalidOperationException("Application PostIt indisponible.");
|
||||
}
|
||||
|
||||
var vm = app.ServiceProvider?.GetRequiredService<UserProfilePageViewModel>();
|
||||
if (vm is null)
|
||||
{
|
||||
throw new InvalidOperationException("Page profil indisponible.");
|
||||
}
|
||||
|
||||
await vm.InitializeAsync();
|
||||
await app.PushPageAsync(vm);
|
||||
}
|
||||
|
||||
private async Task OpenPerformerConfigurationAsync()
|
||||
{
|
||||
var app = (App?)Application.Current;
|
||||
if (app is null)
|
||||
{
|
||||
throw new InvalidOperationException("Application PostIt indisponible.");
|
||||
}
|
||||
|
||||
var vm = app.ServiceProvider?.GetRequiredService<PerformerConfigurationPageViewModel>();
|
||||
if (vm is null)
|
||||
{
|
||||
throw new InvalidOperationException("Page performer indisponible.");
|
||||
}
|
||||
|
||||
await vm.InitializeAsync();
|
||||
await app.PushPageAsync(vm);
|
||||
}
|
||||
|
||||
private async Task OpenAdministrationAsync()
|
||||
{
|
||||
var app = (App?)Application.Current;
|
||||
if (app is null)
|
||||
{
|
||||
throw new InvalidOperationException("Application PostIt indisponible.");
|
||||
}
|
||||
|
||||
var vm = app.ServiceProvider?.GetRequiredService<AdministrationPageViewModel>();
|
||||
if (vm is null)
|
||||
{
|
||||
throw new InvalidOperationException("Page administration indisponible.");
|
||||
}
|
||||
|
||||
await vm.InitializeAsync();
|
||||
await app.PushPageAsync(vm);
|
||||
}
|
||||
|
||||
private async Task OpenResourceUsageAsync()
|
||||
{
|
||||
var app = (App?)Application.Current;
|
||||
|
|
|
|||
125
src/PostIt/PostIt/ViewModels/Layout/UserProfilePageViewModel.cs
Normal file
125
src/PostIt/PostIt/ViewModels/Layout/UserProfilePageViewModel.cs
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using PostIt.Services;
|
||||
using Yavsc.Abstract.Identity;
|
||||
|
||||
namespace PostIt.ViewModels;
|
||||
|
||||
public partial class UserProfilePageViewModel : ViewModelBase
|
||||
{
|
||||
private readonly YavscApiClient? _api;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsBusy { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string UserName { get; set; } = "—";
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string FullName { get; set; } = "—";
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Email { get; set; } = "—";
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Address { get; set; } = "—";
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Avatar { get; set; } = "—";
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Roles { get; set; } = "—";
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string DedicatedGoogleCalendar { get; set; } = "Non configuré";
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string StatusMessage { get; set; } = "Prêt.";
|
||||
|
||||
public override bool CanNavigateNext { get => false; protected set { _ = value; } }
|
||||
public override bool CanNavigatePrevious { get => true; protected set { _ = value; } }
|
||||
|
||||
public UserProfilePageViewModel(YavscApiClient? api)
|
||||
{
|
||||
_api = api;
|
||||
}
|
||||
|
||||
public UserProfilePageViewModel() : this(null) { }
|
||||
|
||||
public Task InitializeAsync() => RefreshAsync();
|
||||
|
||||
[RelayCommand]
|
||||
public async Task RefreshAsync()
|
||||
{
|
||||
if (_api is null)
|
||||
{
|
||||
StatusMessage = "Client API de profil indisponible.";
|
||||
return;
|
||||
}
|
||||
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
var me = await _api.CallAsync<Me>(HttpMethod.Get, new Uri(new Uri(_api.Settings.ApiUrl.TrimEnd('/') + "/", UriKind.Absolute), "account/me").ToString()).ConfigureAwait(true);
|
||||
UserName = me?.UserName ?? "—";
|
||||
FullName = me?.FullName ?? "—";
|
||||
Email = me?.Email ?? "—";
|
||||
Address = me?.Address ?? "—";
|
||||
Avatar = string.IsNullOrWhiteSpace(me?.Avatar) ? "Aucun avatar" : me.Avatar;
|
||||
Roles = me?.Roles is { Length: > 0 } ? string.Join(", ", me.Roles) : "Aucun rôle";
|
||||
DedicatedGoogleCalendar = string.IsNullOrWhiteSpace(me?.DedicatedGoogleCalendar) ? "Non configuré" : me.DedicatedGoogleCalendar;
|
||||
StatusMessage = "Profil chargé.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UserName = "—";
|
||||
FullName = "—";
|
||||
Email = "—";
|
||||
Address = "—";
|
||||
Avatar = "—";
|
||||
Roles = "—";
|
||||
DedicatedGoogleCalendar = "Non configuré";
|
||||
StatusMessage = $"Impossible de charger le profil : {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task SaveProfileAsync()
|
||||
{
|
||||
if (_api is null)
|
||||
{
|
||||
StatusMessage = "Client API de profil indisponible.";
|
||||
return;
|
||||
}
|
||||
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
var payload = new ProfileUpdateRequest
|
||||
{
|
||||
FullName = FullName,
|
||||
Address = Address
|
||||
};
|
||||
|
||||
var endpoint = new Uri(new Uri(_api.Settings.ApiUrl.TrimEnd('/') + "/", UriKind.Absolute), "account/me").ToString();
|
||||
await _api.CallAsync(HttpMethod.Put, endpoint, payload).ConfigureAwait(true);
|
||||
StatusMessage = "Profil enregistré.";
|
||||
await RefreshAsync().ConfigureAwait(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Impossible d’enregistrer le profil : {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
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;
|
||||
|
||||
public partial class ActivitySelectionOption : ObservableObject
|
||||
{
|
||||
public string Code { get; }
|
||||
public string Name { get; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsSelected { get; set; }
|
||||
|
||||
public ActivitySelectionOption(string code, string name)
|
||||
{
|
||||
Code = code;
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class PerformerConfigurationPageViewModel : ViewModelBase
|
||||
{
|
||||
private readonly YavscApiClient _api;
|
||||
private readonly ActivityApiClient _activityClient;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsBusy { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string UserName { get; set; } = "—";
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string ExerciseCountryCode { get; set; } = "fr";
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string SIREN { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Website { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool Active { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool AcceptNotifications { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool AcceptPublicContact { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool UseGeoLocalizationToReduceDistanceWithClients { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<ActivitySelectionOption> AvailableActivities { get; set; } = new();
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string StatusMessage { get; set; } = "Prêt.";
|
||||
|
||||
public override bool CanNavigateNext { get => false; protected set { _ = value; } }
|
||||
public override bool CanNavigatePrevious { get => true; protected set { _ = value; } }
|
||||
|
||||
public PerformerConfigurationPageViewModel(YavscApiClient api, ActivityApiClient activityClient)
|
||||
{
|
||||
_api = api ?? throw new ArgumentNullException(nameof(api));
|
||||
_activityClient = activityClient ?? throw new ArgumentNullException(nameof(activityClient));
|
||||
}
|
||||
|
||||
public PerformerConfigurationPageViewModel() : this(null!, null!) { }
|
||||
|
||||
public Task InitializeAsync() => RefreshAsync();
|
||||
|
||||
[RelayCommand]
|
||||
public async Task RefreshAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
var endpoint = new Uri(new Uri(_api.Settings.ApiUrl.TrimEnd('/') + "/", UriKind.Absolute), "account/performer-profile").ToString();
|
||||
var dto = await _api.CallAsync<PerformerProfileSettings>(HttpMethod.Get, endpoint).ConfigureAwait(true);
|
||||
|
||||
UserName = dto?.UserName ?? "—";
|
||||
ExerciseCountryCode = string.IsNullOrWhiteSpace(dto?.ExerciseCountryCode) ? "fr" : dto.ExerciseCountryCode;
|
||||
SIREN = dto?.SIREN ?? string.Empty;
|
||||
Website = dto?.WebSite ?? string.Empty;
|
||||
Active = dto?.Active ?? false;
|
||||
AcceptNotifications = dto?.AcceptNotifications ?? false;
|
||||
AcceptPublicContact = dto?.AcceptPublicContact ?? false;
|
||||
UseGeoLocalizationToReduceDistanceWithClients = dto?.UseGeoLocalizationToReduceDistanceWithClients ?? false;
|
||||
|
||||
var catalog = await _activityClient.GetCatalogAsync().ConfigureAwait(true);
|
||||
var options = Flatten(catalog)
|
||||
.OrderBy(a => a.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(a => new ActivitySelectionOption(a.Code, a.Name)
|
||||
{
|
||||
IsSelected = dto?.SelectedActivityCodes.Contains(a.Code, StringComparer.OrdinalIgnoreCase) == true
|
||||
})
|
||||
.ToList();
|
||||
|
||||
AvailableActivities.Clear();
|
||||
foreach (var option in options)
|
||||
{
|
||||
AvailableActivities.Add(option);
|
||||
}
|
||||
|
||||
StatusMessage = "Profil performer chargé.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Impossible de charger la configuration performer : {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task SaveAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
var dto = new PerformerProfileSettings
|
||||
{
|
||||
PerformerId = string.Empty,
|
||||
UserName = UserName,
|
||||
ExerciseCountryCode = string.IsNullOrWhiteSpace(ExerciseCountryCode) ? "fr" : ExerciseCountryCode,
|
||||
SIREN = SIREN,
|
||||
WebSite = Website,
|
||||
Active = Active,
|
||||
AcceptNotifications = AcceptNotifications,
|
||||
AcceptPublicContact = AcceptPublicContact,
|
||||
UseGeoLocalizationToReduceDistanceWithClients = UseGeoLocalizationToReduceDistanceWithClients,
|
||||
SelectedActivityCodes = AvailableActivities
|
||||
.Where(a => a.IsSelected)
|
||||
.Select(a => a.Code)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList()
|
||||
};
|
||||
|
||||
var endpoint = new Uri(new Uri(_api.Settings.ApiUrl.TrimEnd('/') + "/", UriKind.Absolute), "account/performer-profile").ToString();
|
||||
var response = await _api.CallAsync<PerformerProfileSettings>(HttpMethod.Put, endpoint, dto).ConfigureAwait(true);
|
||||
StatusMessage = response is not null ? "Configuration performer enregistrée." : "Configuration performer enregistrée.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Impossible d’enregistrer la configuration performer : {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<ActivityInfo> Flatten(IEnumerable<ActivityInfo> activities)
|
||||
{
|
||||
foreach (var activity in activities)
|
||||
{
|
||||
yield return activity;
|
||||
foreach (var child in Flatten(activity.Children ?? Enumerable.Empty<ActivityInfo>()))
|
||||
{
|
||||
yield return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/PostIt/PostIt/Views/Layout/AdministrationPage.axaml
Normal file
42
src/PostIt/PostIt/Views/Layout/AdministrationPage.axaml
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<ContentPage xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:PostIt.ViewModels"
|
||||
x:Class="PostIt.Views.AdministrationPage"
|
||||
x:DataType="vm:AdministrationPageViewModel"
|
||||
Header="Administration">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*" Margin="16" RowSpacing="12">
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="Administration"
|
||||
FontSize="22"
|
||||
FontWeight="SemiBold"/>
|
||||
|
||||
<StackPanel Grid.Row="1" Spacing="8">
|
||||
<TextBox Text="{Binding SearchText, Mode=TwoWay}"
|
||||
PlaceholderText="Rechercher un utilisateur..."/>
|
||||
<Button Content="Actualiser"
|
||||
Command="{Binding RefreshCommand}"
|
||||
IsEnabled="{Binding !IsBusy}"/>
|
||||
<ProgressBar IsVisible="{Binding IsBusy}"
|
||||
IsIndeterminate="True"
|
||||
Height="8"/>
|
||||
<TextBlock Text="{Binding StatusMessage}"
|
||||
TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
|
||||
<ListBox Grid.Row="3"
|
||||
ItemsSource="{Binding FilteredUsers}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Padding="8" BorderBrush="#D0D7DE" BorderThickness="1" CornerRadius="6">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="{Binding UserName, StringFormat='Nom : {0}'}" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding Email, StringFormat='Email : {0}'}" TextWrapping="Wrap"/>
|
||||
<TextBlock Text="{Binding UserId, StringFormat='ID : {0}'}" Foreground="#4B5563" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</ContentPage>
|
||||
11
src/PostIt/PostIt/Views/Layout/AdministrationPage.axaml.cs
Normal file
11
src/PostIt/PostIt/Views/Layout/AdministrationPage.axaml.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using Avalonia.Controls;
|
||||
|
||||
namespace PostIt.Views;
|
||||
|
||||
public partial class AdministrationPage : ContentPage
|
||||
{
|
||||
public AdministrationPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +44,18 @@
|
|||
Command="{Binding OpenMyFiles}"
|
||||
HorizontalAlignment="Center"
|
||||
IsEnabled="{Binding SessionStatus.IsLoggedIn}"/>
|
||||
<Button Content="Mon profil"
|
||||
Command="{Binding OpenProfile}"
|
||||
HorizontalAlignment="Center"
|
||||
IsEnabled="{Binding SessionStatus.IsLoggedIn}"/>
|
||||
<Button Content="Mon activité / performer"
|
||||
Command="{Binding OpenPerformerConfiguration}"
|
||||
HorizontalAlignment="Center"
|
||||
IsEnabled="{Binding SessionStatus.IsLoggedIn}"/>
|
||||
<Button Content="Administration"
|
||||
Command="{Binding OpenAdministration}"
|
||||
HorizontalAlignment="Center"
|
||||
IsEnabled="{Binding SessionStatus.IsLoggedIn}"/>
|
||||
<Button Content="Mon usage / administration"
|
||||
Command="{Binding OpenResourceUsage}"
|
||||
HorizontalAlignment="Center"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
<ContentPage xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:PostIt.ViewModels"
|
||||
x:Class="PostIt.Views.PerformerConfigurationPage"
|
||||
x:DataType="vm:PerformerConfigurationPageViewModel"
|
||||
Header="Mon activité / performer">
|
||||
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="16" Spacing="12">
|
||||
<TextBlock Text="Mon activité / performer"
|
||||
FontSize="22"
|
||||
FontWeight="SemiBold"/>
|
||||
|
||||
<Button Content="Actualiser"
|
||||
Command="{Binding RefreshCommand}"
|
||||
IsEnabled="{Binding !IsBusy}"/>
|
||||
|
||||
<ProgressBar IsVisible="{Binding IsBusy}"
|
||||
IsIndeterminate="True"
|
||||
Height="8"/>
|
||||
|
||||
<TextBlock Text="{Binding UserName, StringFormat='Nom d’utilisateur : {0}'}"/>
|
||||
<TextBox Text="{Binding ExerciseCountryCode, Mode=TwoWay}"
|
||||
Watermark="Code pays (fr, en...)"/>
|
||||
<TextBox Text="{Binding SIREN, Mode=TwoWay}"
|
||||
Watermark="SIREN"/>
|
||||
<TextBox Text="{Binding Website, Mode=TwoWay}"
|
||||
Watermark="Site web"/>
|
||||
|
||||
<CheckBox Content="Disponible / actif"
|
||||
IsChecked="{Binding Active, Mode=TwoWay}"/>
|
||||
<CheckBox Content="Accepter les notifications"
|
||||
IsChecked="{Binding AcceptNotifications, Mode=TwoWay}"/>
|
||||
<CheckBox Content="Accepter les demandes publiques"
|
||||
IsChecked="{Binding AcceptPublicContact, Mode=TwoWay}"/>
|
||||
<CheckBox Content="Utiliser la géolocalisation"
|
||||
IsChecked="{Binding UseGeoLocalizationToReduceDistanceWithClients, Mode=TwoWay}"/>
|
||||
|
||||
<TextBlock Text="Activités" FontWeight="SemiBold"/>
|
||||
<ItemsControl ItemsSource="{Binding AvailableActivities}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox Margin="0,0,0,6"
|
||||
Content="{Binding Name}"
|
||||
IsChecked="{Binding IsSelected, Mode=TwoWay}"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Button Content="Enregistrer"
|
||||
Command="{Binding SaveCommand}"
|
||||
IsEnabled="{Binding !IsBusy}"/>
|
||||
|
||||
<TextBlock Text="{Binding StatusMessage}"
|
||||
Foreground="#4B5563"
|
||||
TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</ContentPage>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
using Avalonia.Controls;
|
||||
|
||||
namespace PostIt.Views;
|
||||
|
||||
public partial class PerformerConfigurationPage : ContentPage
|
||||
{
|
||||
public PerformerConfigurationPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
34
src/PostIt/PostIt/Views/Layout/UserProfilePage.axaml
Normal file
34
src/PostIt/PostIt/Views/Layout/UserProfilePage.axaml
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<ContentPage xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:PostIt.ViewModels"
|
||||
x:Class="PostIt.Views.UserProfilePage"
|
||||
x:DataType="vm:UserProfilePageViewModel"
|
||||
Header="Mon profil">
|
||||
|
||||
<StackPanel Margin="16" Spacing="12">
|
||||
<TextBlock Text="Mon profil"
|
||||
FontSize="22"
|
||||
FontWeight="SemiBold"/>
|
||||
|
||||
<Button Content="Actualiser"
|
||||
Command="{Binding RefreshCommand}"
|
||||
IsEnabled="{Binding !IsBusy}"/>
|
||||
|
||||
<ProgressBar IsVisible="{Binding IsBusy}"
|
||||
IsIndeterminate="True"
|
||||
Height="8"/>
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto" ColumnDefinitions="*" RowSpacing="8">
|
||||
<TextBox Watermark="Nom complet" Text="{Binding FullName}"/>
|
||||
<TextBox Grid.Row="1" Watermark="Adresse" Text="{Binding Address}" AcceptsReturn="True" TextWrapping="Wrap"/>
|
||||
<Button Grid.Row="2" Content="Enregistrer" Command="{Binding SaveProfileCommand}" IsEnabled="{Binding !IsBusy}"/>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="{Binding UserName, StringFormat='Nom d’utilisateur : {0}'}"/>
|
||||
<TextBlock Text="{Binding Email, StringFormat='Email : {0}'}"/>
|
||||
<TextBlock Text="{Binding Roles, StringFormat='Rôles : {0}'}"/>
|
||||
<TextBlock Text="{Binding DedicatedGoogleCalendar, StringFormat='Calendrier dédié : {0}'}"/>
|
||||
<TextBlock Text="{Binding Avatar, StringFormat='Avatar : {0}'}" TextWrapping="Wrap"/>
|
||||
<TextBlock Text="{Binding StatusMessage}" Foreground="#4B5563" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</ContentPage>
|
||||
11
src/PostIt/PostIt/Views/Layout/UserProfilePage.axaml.cs
Normal file
11
src/PostIt/PostIt/Views/Layout/UserProfilePage.axaml.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using Avalonia.Controls;
|
||||
|
||||
namespace PostIt.Views;
|
||||
|
||||
public partial class UserProfilePage : ContentPage
|
||||
{
|
||||
public UserProfilePage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue