diff --git a/.gitignore b/.gitignore index c1b184a87..93d7878e1 100644 --- a/.gitignore +++ b/.gitignore @@ -44,8 +44,5 @@ DataDir/ *.log # gitleaks scan output (local secret-scan report, not for the repo) -/gitleaks-report.json -/test-report.html -/test-report.xml - +gitleaks-report.json diff --git a/src/PostIt/PostIt.Tests/EstimateEditionPageViewModelTests.cs b/src/PostIt/PostIt.Tests/EstimateEditionPageViewModelTests.cs index 17efd391b..db70e25fa 100644 --- a/src/PostIt/PostIt.Tests/EstimateEditionPageViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/EstimateEditionPageViewModelTests.cs @@ -22,8 +22,7 @@ public class EstimateEditionPageViewModelTests private static EstimateEditionPageViewModel CreateViewModel(StubEstimateApi api, BillingQuerySummaryDto? query = null) { var client = new EstimateApiClient(api, "https://business.example/api/v1/"); - var fsClient = new UserFilesApiClient(api, "https://blogs.example/api/v1/"); - return new EstimateEditionPageViewModel(query ?? SampleQuery(), client, fsClient); + return new EstimateEditionPageViewModel(query ?? SampleQuery(), client); } [Fact] @@ -144,8 +143,10 @@ public class EstimateEditionPageViewModelTests await vm.SendCommand.ExecuteAsync(null); - var postCall = Assert.Single(api.Calls, c => c.Method == HttpMethod.Post && c.Path == "https://business.example/api/v1/estimate"); - var payload = Assert.IsType(postCall.Body); + Assert.Equal("https://business.example/api/v1/estimate", api.LastPath); + Assert.Equal(HttpMethod.Post, api.LastMethod); + + var payload = Assert.IsType(api.LastBody); Assert.Equal(42, payload.CommandId); Assert.Equal("cli-1", payload.ClientId); Assert.Equal("Brush", payload.CommandType); @@ -215,8 +216,7 @@ public class EstimateEditionPageViewModelTests await vm.SendCommand.ExecuteAsync(null); - var postCall = Assert.Single(api.Calls, c => c.Method == HttpMethod.Post && c.Path == "https://business.example/api/v1/estimate"); - var payload = Assert.IsType(postCall.Body); + var payload = Assert.IsType(api.LastBody); Assert.Equal(2, payload.Bill.Count); Assert.Equal(-5m, payload.Bill[1].UnitaryCost); Assert.True(vm.HasSent); @@ -230,20 +230,11 @@ public class EstimateEditionPageViewModelTests public object? LastBody { get; private set; } public Exception? Failure { get; init; } - /// Historique complet des appels (le plus récent en dernier). - public List<(HttpMethod Method, string Path, object? Body)> Calls { get; } = new(); - - private void Record(HttpMethod method, string path, object? body) + public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) { LastMethod = method; LastPath = path; LastBody = body; - Calls.Add((method, path, body)); - } - - public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) - { - Record(method, path, body); if (Failure is not null) { @@ -262,7 +253,9 @@ public class EstimateEditionPageViewModelTests public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) { - Record(method, path, body); + LastMethod = method; + LastPath = path; + LastBody = body; return Task.CompletedTask; } @@ -272,12 +265,6 @@ public class EstimateEditionPageViewModelTests public Task CallAsync(HttpMethod method, string path, Func contentFactory, CancellationToken ct = default) => CallAsync(method, path, (object?)null, ct); - public Task DownloadAsync(HttpMethod method, string path, CancellationToken ct = default) - { - Record(method, path, null); - return Task.FromResult(Array.Empty()); - } - public ValueTask DisposeAsync() => ValueTask.CompletedTask; } } diff --git a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs index 9616e7089..2b243f2d5 100644 --- a/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs +++ b/src/PostIt/PostIt/Helpers/ServiceCollectionHelpers.cs @@ -22,7 +22,6 @@ public static class ServiceCollectionHelpers var api = new YavscApiClient(settings, tokenStore); var client = new BlogApiClient(api, settings.BlogsApiUrl); - var userFilesClient = new UserFilesApiClient(api, settings.BlogsApiUrl); var circleClient = new CircleApiClient(api, settings.BlogsApiUrl); var blogAclClient = new BlogAclApiClient(api, settings.BlogsApiUrl); var userSearchClient = new UserSearchClient(api, settings.BlogsApiUrl); @@ -66,7 +65,6 @@ public static class ServiceCollectionHelpers services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); // ViewModels services.AddSingleton(settings); @@ -79,7 +77,6 @@ public static class ServiceCollectionHelpers services.AddSingleton(billingClient); services.AddSingleton(estimateClient); services.AddSingleton(frontClient); - services.AddSingleton(userFilesClient); services.AddSingleton(reverseGeocoding); services.AddSingleton(userDirectory); services.AddSingleton(); diff --git a/src/PostIt/PostIt/ViewLocator.cs b/src/PostIt/PostIt/ViewLocator.cs index 687a7cbd2..ca9a7ff65 100644 --- a/src/PostIt/PostIt/ViewLocator.cs +++ b/src/PostIt/PostIt/ViewLocator.cs @@ -56,7 +56,6 @@ public class ViewLocator : IDataTemplate EstimateEditionPageViewModel => services.GetRequiredService(), EstimateListPageViewModel => services.GetRequiredService(), EstimateValidationPageViewModel => services.GetRequiredService(), - MyFilesViewModel => services.GetRequiredService(), null => new TextBlock { Text = "No view for " }, _ => new TextBlock { Text = $"No view for {data.GetType().Name}" } }; diff --git a/src/PostIt/PostIt/ViewModels/Activity/BillingQueriesPageViewModel.cs b/src/PostIt/PostIt/ViewModels/Activity/BillingQueriesPageViewModel.cs index f1b05b749..dbd84e5c2 100644 --- a/src/PostIt/PostIt/ViewModels/Activity/BillingQueriesPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/Activity/BillingQueriesPageViewModel.cs @@ -132,18 +132,14 @@ public partial class BillingQueriesPageViewModel : ViewModelBase, IActionStatusV { var details = await _billingClient.GetQueryAsync(Form.ActionName, SelectedQuery.Id).ConfigureAwait(true); var frontClient = app.ServiceProvider?.GetRequiredService(); - var fsClient = app.ServiceProvider?.GetRequiredService(); var vm = new BillingQueryDetailsPageViewModel( Activity, Performer, Form, _billingClient, frontClient, - fsClient, details, - IsReadOnly, - canAttach: !IsReadOnly); - await vm.InitializeAsync().ConfigureAwait(true); + IsReadOnly); await app.PushPageAsync(vm).ConfigureAwait(true); } catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden) diff --git a/src/PostIt/PostIt/ViewModels/Activity/BillingQueryDetailsPageViewModel.cs b/src/PostIt/PostIt/ViewModels/Activity/BillingQueryDetailsPageViewModel.cs index c9f122dce..9ab1b42fd 100644 --- a/src/PostIt/PostIt/ViewModels/Activity/BillingQueryDetailsPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/Activity/BillingQueryDetailsPageViewModel.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using Avalonia; @@ -16,17 +15,8 @@ public partial class BillingQueryDetailsPageViewModel : ViewModelBase, IActionSt { private readonly BillingApiClient _billingClient; private readonly FrontOfficeApiClient _frontClient; - private readonly UserFilesApiClient _fsClient; private readonly BillingQueryDetailsDto _details; - /// - /// Vrai lorsque l'utilisateur courant est le client de la demande - /// et peut y attacher/détacher des fichiers de son espace perso. - /// Le fournisseur consulte et télécharge les pièces mais n'attache - /// pas (il joint ses documents au devis, côté EstimateEditionPage). - /// - public bool CanAttach { get; } - public ActivityInfo Activity { get; } public ActivityUserDisplayItem Performer { get; } public CommandFormSummary Form { get; } @@ -62,10 +52,6 @@ public partial class BillingQueryDetailsPageViewModel : ViewModelBase, IActionSt [ObservableProperty] public partial StatusNotice ActionStatus { get; set; } = StatusNotice.Info("Pret."); - /// Pièces jointes rattachées à la demande (par référence). - [ObservableProperty] - public partial ObservableCollection Attachments { get; set; } = new(); - public override bool CanNavigateNext { get => false; @@ -84,27 +70,20 @@ public partial class BillingQueryDetailsPageViewModel : ViewModelBase, IActionSt CommandFormSummary form, BillingApiClient billingClient, FrontOfficeApiClient frontClient, - UserFilesApiClient fsClient, BillingQueryDetailsDto details, - bool isReadOnly, - bool canAttach) + bool isReadOnly) { 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)); _frontClient = frontClient ?? throw new ArgumentNullException(nameof(frontClient)); - _fsClient = fsClient ?? throw new ArgumentNullException(nameof(fsClient)); _details = details ?? throw new ArgumentNullException(nameof(details)); IsReadOnly = isReadOnly; - CanAttach = canAttach; this.SetInfoStatus("Details de commande charges."); } - /// Constructeur pour le designer Avalonia. - public BillingQueryDetailsPageViewModel() : this(null!, null!, null!, null!, null!, null!, null!, false, false) { } - [RelayCommand] private async Task OpenEditorAsync() { @@ -155,110 +134,6 @@ public partial class BillingQueryDetailsPageViewModel : ViewModelBase, IActionSt await app.GoBackAsync().ConfigureAwait(true); } - /// Charge les pièces jointes de la demande. - public async Task InitializeAsync() - { - await LoadQueryAttachmentsAsync().ConfigureAwait(true); - } - - [RelayCommand] - private async Task LoadQueryAttachmentsAsync() - { - try - { - var list = await _frontClient - .GetQueryAttachmentsAsync(BillingCode, Id) - .ConfigureAwait(true); - Attachments.Clear(); - if (list is not null) - foreach (var a in list) Attachments.Add(a); - } - catch (Exception ex) - { - // Non bloquant : les pièces jointes sont un complément. - this.SetWarningStatus($"Pièces jointes indisponibles : {ex.Message}"); - } - } - - /// - /// Ouvre la page « My Files » en mode sélecteur ; le fichier choisi - /// est rattaché à la demande par référence via - /// POST front/query/{id}/attachments. Réservé au client - /// (). - /// - [RelayCommand] - private async Task AddQueryAttachmentAsync() - { - if (!CanAttach) return; - - var app = (App?)Application.Current; - if (app is null) - { - throw new InvalidOperationException("Application PostIt indisponible."); - } - - var billingCode = BillingCode; - var queryId = Id; - var picker = new MyFilesViewModel(_fsClient, async fileId => - { - try - { - await _frontClient.AttachQueryFileAsync(billingCode, queryId, fileId).ConfigureAwait(true); - await LoadQueryAttachmentsAsync().ConfigureAwait(true); - this.SetInfoStatus("Pièce jointe ajoutée à la demande."); - } - catch (Exception ex) - { - this.SetErrorStatus($"Échec de l'attachement : {ex.Message}"); - } - }); - await picker.InitializeAsync().ConfigureAwait(true); - await app.PushPageAsync(picker).ConfigureAwait(true); - } - - [RelayCommand] - private async Task DetachQueryFileAsync(AttachmentDto? attachment) - { - if (attachment is null || !CanAttach) return; - IsBusy = true; - try - { - await _frontClient.DetachQueryFileAsync(BillingCode, Id, attachment.FileId).ConfigureAwait(true); - Attachments.Remove(attachment); - this.SetInfoStatus("Pièce jointe détachée de la demande."); - } - catch (Exception ex) - { - this.SetErrorStatus($"Échec du détachement : {ex.Message}"); - } - finally - { - IsBusy = false; - } - } - - [RelayCommand] - private async Task DownloadQueryAttachmentAsync(AttachmentDto? attachment) - { - if (attachment is null) return; - IsBusy = true; - try - { - var bytes = await _fsClient.DownloadFileAsync(attachment.FileId).ConfigureAwait(true); - var name = System.IO.Path.GetFileName(attachment.Path ?? "file"); - var saved = await FileSaveHelpers.SaveAsync(name, "bin", bytes).ConfigureAwait(true); - this.SetInfoStatus(saved ? "Fichier enregistré." : "Téléchargement annulé."); - } - catch (Exception ex) - { - this.SetErrorStatus($"Échec du téléchargement : {ex.Message}"); - } - finally - { - IsBusy = false; - } - } - /// /// Download the estimate linked to this command as a LaTeX source /// (GET api/v1/front/query/{Id}/estimate.tex). The estimate is diff --git a/src/PostIt/PostIt/ViewModels/Activity/EstimateEditionPageViewModel.cs b/src/PostIt/PostIt/ViewModels/Activity/EstimateEditionPageViewModel.cs index 1f1e4b4be..8f007dc08 100644 --- a/src/PostIt/PostIt/ViewModels/Activity/EstimateEditionPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/Activity/EstimateEditionPageViewModel.cs @@ -25,7 +25,6 @@ namespace PostIt.ViewModels; public partial class EstimateEditionPageViewModel : ViewModelBase, IActionStatusViewModel { private readonly EstimateApiClient _estimateClient; - private readonly UserFilesApiClient _fsClient; private readonly BillingQuerySummaryDto _query; public long QueryId => _query.Id; @@ -69,14 +68,6 @@ public partial class EstimateEditionPageViewModel : ViewModelBase, IActionStatus [ObservableProperty] public partial StatusNotice ActionStatus { get; set; } = StatusNotice.Info("Prêt."); - /// Identifiant du devis, renseigné après l'envoi. - [ObservableProperty, NotifyCanExecuteChangedFor(nameof(AddAttachmentCommand))] - public partial long? EstimateId { get; set; } - - /// Pièces jointes rattachées au devis (par référence). - [ObservableProperty] - public partial ObservableCollection Attachments { get; set; } = new(); - public decimal Total => Lines.Sum(line => line.LineTotal); public string TotalLabel => $"{Total:0.00} {Lines.FirstOrDefault()?.Currency ?? "EUR"}"; @@ -95,14 +86,10 @@ public partial class EstimateEditionPageViewModel : ViewModelBase, IActionStatus protected set { _ = value; } } - public EstimateEditionPageViewModel( - BillingQuerySummaryDto query, - EstimateApiClient estimateClient, - UserFilesApiClient fsClient) + public EstimateEditionPageViewModel(BillingQuerySummaryDto query, EstimateApiClient estimateClient) { _query = query ?? throw new ArgumentNullException(nameof(query)); _estimateClient = estimateClient ?? throw new ArgumentNullException(nameof(estimateClient)); - _fsClient = fsClient ?? throw new ArgumentNullException(nameof(fsClient)); EstimateDescription = query.Description ?? string.Empty; Lines.CollectionChanged += OnLinesCollectionChanged; @@ -111,9 +98,6 @@ public partial class EstimateEditionPageViewModel : ViewModelBase, IActionStatus this.SetInfoStatus("Complétez le devis puis envoyez-le. La demande associée sera validée."); } - /// Constructeur pour le designer Avalonia. - public EstimateEditionPageViewModel() : this(null!, null!, null!) { } - [RelayCommand] private void AddLine() { @@ -157,11 +141,9 @@ public partial class EstimateEditionPageViewModel : ViewModelBase, IActionStatus var created = await _estimateClient.CreateAsync(payload).ConfigureAwait(true); HasSent = true; - EstimateId = created.Id; OnPropertyChanged(nameof(SendLabel)); this.SetInfoStatus( $"Devis #{created.Id} envoyé ({created.Bill.Count} ligne(s)). La demande #{QueryId} est validée."); - await LoadAttachmentsAsync().ConfigureAwait(true); } catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden) { @@ -189,106 +171,6 @@ public partial class EstimateEditionPageViewModel : ViewModelBase, IActionStatus await app.GoBackAsync().ConfigureAwait(true); } - /// - /// Charge les pièces jointes rattachées au devis courant - /// (GET estimate/{id}/attachments). - /// - [RelayCommand] - private async Task LoadAttachmentsAsync() - { - if (EstimateId is null || _estimateClient is null) return; - try - { - var list = await _estimateClient.GetAttachmentsAsync(EstimateId.Value).ConfigureAwait(true); - Attachments.Clear(); - if (list is not null) - foreach (var a in list) Attachments.Add(a); - } - catch (Exception ex) - { - this.SetWarningStatus($"Impossible de charger les pièces jointes : {ex.Message}"); - } - } - - private bool CanAddAttachment() => EstimateId is not null && !IsBusy; - - /// - /// Ouvre la page « My Files » en mode sélecteur ; le fichier choisi - /// est rattaché au devis par référence via - /// POST estimate/{id}/attachments. - /// - [RelayCommand(CanExecute = nameof(CanAddAttachment))] - private async Task AddAttachmentAsync() - { - if (EstimateId is null) return; - - var app = (App?)Application.Current; - if (app is null) - { - throw new InvalidOperationException("Application PostIt indisponible."); - } - - var estimateId = EstimateId.Value; - var picker = new MyFilesViewModel(_fsClient, async fileId => - { - try - { - await _estimateClient.AttachFileAsync(estimateId, fileId).ConfigureAwait(true); - await LoadAttachmentsAsync().ConfigureAwait(true); - this.SetInfoStatus("Pièce jointe ajoutée au devis."); - } - catch (Exception ex) - { - this.SetErrorStatus($"Échec de l'attachement : {ex.Message}"); - } - }); - await picker.InitializeAsync().ConfigureAwait(true); - await app.PushPageAsync(picker).ConfigureAwait(true); - } - - [RelayCommand] - private async Task DetachFileAsync(AttachmentDto? attachment) - { - if (attachment is null || EstimateId is null) return; - IsBusy = true; - try - { - await _estimateClient.DetachFileAsync(EstimateId.Value, attachment.FileId).ConfigureAwait(true); - Attachments.Remove(attachment); - this.SetInfoStatus("Pièce jointe détachée du devis."); - } - catch (Exception ex) - { - this.SetErrorStatus($"Échec du détachement : {ex.Message}"); - } - finally - { - IsBusy = false; - } - } - - [RelayCommand] - private async Task DownloadAttachmentAsync(AttachmentDto? attachment) - { - if (attachment is null) return; - IsBusy = true; - try - { - var bytes = await _fsClient.DownloadFileAsync(attachment.FileId).ConfigureAwait(true); - var name = System.IO.Path.GetFileName(attachment.Path ?? "file"); - var saved = await FileSaveHelpers.SaveAsync(name, "bin", bytes).ConfigureAwait(true); - this.SetInfoStatus(saved ? "Fichier enregistré." : "Téléchargement annulé."); - } - catch (Exception ex) - { - this.SetErrorStatus($"Échec du téléchargement : {ex.Message}"); - } - finally - { - IsBusy = false; - } - } - internal EstimateDto BuildPayload() { return new EstimateDto diff --git a/src/PostIt/PostIt/ViewModels/Activity/ProviderOngoingRequestsPageViewModel.cs b/src/PostIt/PostIt/ViewModels/Activity/ProviderOngoingRequestsPageViewModel.cs index 7fd84fd88..75a5bdc9a 100644 --- a/src/PostIt/PostIt/ViewModels/Activity/ProviderOngoingRequestsPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/Activity/ProviderOngoingRequestsPageViewModel.cs @@ -155,7 +155,6 @@ public partial class ProviderOngoingRequestsPageViewModel : ViewModelBase, IActi .ConfigureAwait(true); var (activity, performer, form) = BuildNavigationContext(SelectedQuery); var frontClient = app.ServiceProvider?.GetRequiredService(); - var fsClient = app.ServiceProvider?.GetRequiredService(); var vm = new BillingQueryDetailsPageViewModel( activity, @@ -163,12 +162,9 @@ public partial class ProviderOngoingRequestsPageViewModel : ViewModelBase, IActi form, _billingClient, frontClient, - fsClient, details, - isReadOnly: false, - canAttach: false); + isReadOnly: false); - await vm.InitializeAsync().ConfigureAwait(true); await app.PushPageAsync(vm).ConfigureAwait(true); } catch (Exception ex) @@ -247,13 +243,7 @@ public partial class ProviderOngoingRequestsPageViewModel : ViewModelBase, IActi throw new InvalidOperationException("Application PostIt indisponible."); } - var fsClient = app.ServiceProvider?.GetRequiredService(); - if (fsClient is null) - { - throw new InvalidOperationException("Client fichiers indisponible."); - } - - var vm = new EstimateEditionPageViewModel(SelectedQuery, _estimateClient, fsClient); + var vm = new EstimateEditionPageViewModel(SelectedQuery, _estimateClient); await app.PushPageAsync(vm).ConfigureAwait(true); } diff --git a/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs b/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs index 2c138a27d..9d466b26c 100644 --- a/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/Layout/HomePageViewModel.cs @@ -35,14 +35,12 @@ public class HomePageViewModel : ViewModelBase OpenClientEstimates = new AsyncRelayCommand(() => OpenEstimateListAsync(EstimateListPerspective.Client)); OpenProviderEstimates = new AsyncRelayCommand(() => OpenEstimateListAsync(EstimateListPerspective.Provider)); OpenBlogs = new AsyncRelayCommand(App.PushBlogsPageAsync); - OpenMyFiles = new AsyncRelayCommand(OpenMyFilesAsync); } public IAsyncRelayCommand OpenBlogs { get; } public IAsyncRelayCommand OpenActivities { get; } public IAsyncRelayCommand OpenProviderRequests { get; } public IAsyncRelayCommand OpenClientEstimates { get; } public IAsyncRelayCommand OpenProviderEstimates { get; } - public IAsyncRelayCommand OpenMyFiles { get; } private async Task OpenActivitiesAsync() { @@ -103,29 +101,6 @@ public class HomePageViewModel : ViewModelBase await app.PushPageAsync(vm); } - /// - /// Ouvre la page « Mes fichiers » (espace de stockage personnel), - /// servie par le host Blogs via api/v1/fs. - /// - private async Task OpenMyFilesAsync() - { - var app = (App?)Application.Current; - if (app is null) - { - throw new InvalidOperationException("Application PostIt indisponible."); - } - - var fsClient = app.ServiceProvider?.GetRequiredService(); - if (fsClient is null) - { - throw new InvalidOperationException("Client fichiers indisponible."); - } - - var vm = new MyFilesViewModel(fsClient); - await vm.InitializeAsync(); - await app.PushPageAsync(vm); - } - /// /// Avalonia designer constructor. Builds a self-contained VM /// with a freshly-constructed Settings so the XAML preview can diff --git a/src/PostIt/PostIt/ViewModels/MyFiles/MyFilesViewModel.cs b/src/PostIt/PostIt/ViewModels/MyFiles/MyFilesViewModel.cs deleted file mode 100644 index cc4b46828..000000000 --- a/src/PostIt/PostIt/ViewModels/MyFiles/MyFilesViewModel.cs +++ /dev/null @@ -1,229 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Avalonia; -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using PostIt.Helpers; -using Yavsc.Api.Client; - -namespace PostIt.ViewModels; - -/// -/// Gestion de l'espace de stockage personnel (« My Files ») : -/// navigation dans l'arbre, téléversement, suppression, téléchargement. -/// Sert aussi de **sélecteur** pour attacher un fichier à un devis -/// (fournisseur) ou à une demande (client) par référence : lorsque -/// est vrai, la sélection d'un fichier invoque -/// avec l'identifiant -/// puis revient à la page précédente. -/// -public partial class MyFilesViewModel : ViewModelBase, IActionStatusViewModel -{ - private readonly UserFilesApiClient _fs; - private readonly Action? _onFilePicked; - - /// - /// Sous-répertoire courant, relatif à la racine de l'espace perso - /// (chaîne vide à la racine). Servi tel quel à GET/POST fs/{subdir}. - /// - [ObservableProperty] - public partial string CurrentPath { get; set; } = string.Empty; - - public string PathLabel => string.IsNullOrWhiteSpace(CurrentPath) - ? "Mes fichiers" - : $"Mes fichiers / {CurrentPath}"; - - public bool IsPickerMode => _onFilePicked is not null; - - public bool CanGoUp => !string.IsNullOrWhiteSpace(CurrentPath); - - [ObservableProperty] - public partial ObservableCollection Files { get; set; } = new(); - - [ObservableProperty] - public partial ObservableCollection SubDirectories { get; set; } = new(); - - [ObservableProperty] - public partial bool IsBusy { get; set; } - - [ObservableProperty] - public partial string StatusMessage { get; set; } = "Prêt."; - - [ObservableProperty] - public partial StatusNotice ActionStatus { get; set; } = StatusNotice.Info("Prêt."); - - public override bool CanNavigateNext { get => false; protected set { _ = value; } } - public override bool CanNavigatePrevious { get => true; protected set { _ = value; } } - - public MyFilesViewModel(UserFilesApiClient fs, Action? onFilePicked = null) - { - _fs = fs ?? throw new ArgumentNullException(nameof(fs)); - _onFilePicked = onFilePicked; - } - - /// Constructeur pour le designer Avalonia. - public MyFilesViewModel() : this(null!) { } - - public async Task InitializeAsync() - { - await RefreshAsync().ConfigureAwait(true); - } - - [RelayCommand] - private async Task RefreshAsync() - { - IsBusy = true; - try - { - var dir = await _fs.ListAsync(CurrentPath).ConfigureAwait(true); - Files.Clear(); - SubDirectories.Clear(); - if (dir?.Files is not null) - foreach (var f in dir.Files) Files.Add(f); - if (dir?.SubDirectories is not null) - foreach (var d in dir.SubDirectories) SubDirectories.Add(d); - this.SetInfoStatus($"{Files.Count} fichier(s), {SubDirectories.Count} dossier(s)."); - } - catch (Exception ex) - { - this.SetErrorStatus($"Erreur de listage : {ex.Message}"); - } - finally - { - IsBusy = false; - } - } - - [RelayCommand] - private async Task OpenDirAsync(DirEntryDto? dir) - { - if (dir is null) return; - CurrentPath = CombinePath(CurrentPath, dir.Name ?? string.Empty); - OnPropertyChanged(nameof(PathLabel)); - OnPropertyChanged(nameof(CanGoUp)); - await RefreshAsync().ConfigureAwait(true); - } - - [RelayCommand] - private async Task UpAsync() - { - if (!CanGoUp) return; - CurrentPath = ParentPath(CurrentPath); - OnPropertyChanged(nameof(PathLabel)); - OnPropertyChanged(nameof(CanGoUp)); - await RefreshAsync().ConfigureAwait(true); - } - - /// Téléversement déclenché par le code-behind (picker OS). - public async Task UploadAsync(System.Collections.Generic.IReadOnlyCollection uploads) - { - if (uploads is null || uploads.Count == 0) return; - IsBusy = true; - try - { - var received = await _fs.UploadAsync(CurrentPath, uploads).ConfigureAwait(true); - this.SetInfoStatus($"{received.Count} fichier(s) téléversé(s)."); - await RefreshAsync().ConfigureAwait(true); - } - catch (Exception ex) - { - this.SetErrorStatus($"Échec du téléversement : {ex.Message}"); - } - finally - { - IsBusy = false; - } - } - - [RelayCommand] - private async Task DeleteFileAsync(FileEntryDto? file) - { - if (file is null) return; - IsBusy = true; - try - { - await _fs.DeleteAsync(CombinePath(CurrentPath, file.Name ?? string.Empty)).ConfigureAwait(true); - this.SetInfoStatus($"Fichier supprimé."); - await RefreshAsync().ConfigureAwait(true); - } - catch (Exception ex) - { - this.SetErrorStatus($"Échec de la suppression : {ex.Message}"); - } - finally - { - IsBusy = false; - } - } - - [RelayCommand] - private async Task DownloadFileAsync(FileEntryDto? file) - { - if (file is null || file.Id is null) return; - IsBusy = true; - try - { - var bytes = await _fs.DownloadFileAsync(file.Id.Value).ConfigureAwait(true); - var saved = await FileSaveHelpers.SaveAsync(file.Name ?? "file", "bin", bytes).ConfigureAwait(true); - this.SetInfoStatus(saved ? "Fichier enregistré." : "Téléchargement annulé."); - } - catch (Exception ex) - { - this.SetErrorStatus($"Échec du téléchargement : {ex.Message}"); - } - finally - { - IsBusy = false; - } - } - - [RelayCommand] - private async Task PickFileAsync(FileEntryDto? file) - { - if (file is null || file.Id is null) - { - this.SetWarningStatus("Ce fichier n'est pas référencé (aucun id). Téléversez-le d'abord."); - return; - } - - var callback = _onFilePicked; - if (callback is null) - { - // Mode gestion : le téléchargement est l'action par défaut. - await DownloadFileAsync(file).ConfigureAwait(true); - return; - } - - callback(file.Id.Value); - await GoBackAsync().ConfigureAwait(true); - } - - [RelayCommand] - private async Task BackAsync() - { - await GoBackAsync().ConfigureAwait(true); - } - - private static async Task GoBackAsync() - { - var app = (App?)Application.Current; - if (app is null) throw new InvalidOperationException("Application PostIt indisponible."); - await app.GoBackAsync().ConfigureAwait(true); - } - - private static string CombinePath(string current, string name) - { - if (string.IsNullOrWhiteSpace(current)) return name; - return $"{current}/{name}"; - } - - private static string ParentPath(string current) - { - if (string.IsNullOrWhiteSpace(current)) return string.Empty; - var idx = current.LastIndexOf('/'); - return idx < 0 ? string.Empty : current[..idx]; - } -} \ No newline at end of file diff --git a/src/PostIt/PostIt/Views/Activity/BillingQueryDetailsPage.axaml b/src/PostIt/PostIt/Views/Activity/BillingQueryDetailsPage.axaml index 4cbce9a20..904f17350 100644 --- a/src/PostIt/PostIt/Views/Activity/BillingQueryDetailsPage.axaml +++ b/src/PostIt/PostIt/Views/Activity/BillingQueryDetailsPage.axaml @@ -1,7 +1,6 @@ - - - - - - /// public string ContentType { get; set; } - - /// - /// Propriétaire du fichier dans l'espace de stockage personnel. - /// Renseigné pour les fichiers téléversés via api/v1/fs - /// (référencés par / - /// ). Reste null pour les - /// pièces jointes de blog historiques (), - /// qui ne sont pas adressées par propriétaire. - /// - public string? OwnerId { get; set; } - - [ForeignKey("OwnerId")] - public virtual ApplicationUser? Owner { get; set; } } [PrimaryKey(nameof(FileId), nameof(PostId))] diff --git a/src/Yavsc.Server/Models/FileSystem/FileReceivedInfo.cs b/src/Yavsc.Server/Models/FileSystem/FileReceivedInfo.cs index 602cc4bf0..63627c116 100644 --- a/src/Yavsc.Server/Models/FileSystem/FileReceivedInfo.cs +++ b/src/Yavsc.Server/Models/FileSystem/FileReceivedInfo.cs @@ -43,29 +43,11 @@ namespace Yavsc.Server.Models.FileSystem } public string DestDir { get; set; } - + public string FileName { get; set; } public bool Overridden { get; set; } public bool QuotaOffense { get; set; } - - /// - /// Identifiant de la ligne - /// créée à l'enregistrement du fichier dans l'espace perso - /// (api/v1/fs). Nul si aucune métadonnée n'a été persistée. - /// - public long? FileId { get; set; } - - /// - /// Taille du fichier reçu (octets), pour le reflet côté métadonnées. - /// - public long Length { get; set; } - - /// - /// Type MIME du fichier reçu. - /// - public string ContentType { get; set; } - public string FullName { get => Path.Combine(DestDir, FileName); } }