diff --git a/SharepointToolbox/App.xaml.cs b/SharepointToolbox/App.xaml.cs index ff48f72..edebd36 100644 --- a/SharepointToolbox/App.xaml.cs +++ b/SharepointToolbox/App.xaml.cs @@ -144,6 +144,7 @@ public partial class App : Application // Versions cleanup services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/SharepointToolbox/Localization/Strings.fr.resx b/SharepointToolbox/Localization/Strings.fr.resx index 2040f58..c9d00aa 100644 --- a/SharepointToolbox/Localization/Strings.fr.resx +++ b/SharepointToolbox/Localization/Strings.fr.resx @@ -592,6 +592,8 @@ Cette action est irréversible. Rapport de détection de doublons Résultats de recherche de fichiers SharePoint Résultats de recherche de fichiers + Rapport de nettoyage des versions SharePoint + Rapport de nettoyage des versions Accès totaux Utilisateurs audités Sites analysés diff --git a/SharepointToolbox/Localization/Strings.resx b/SharepointToolbox/Localization/Strings.resx index 9abcfef..abbda7b 100644 --- a/SharepointToolbox/Localization/Strings.resx +++ b/SharepointToolbox/Localization/Strings.resx @@ -592,6 +592,8 @@ This cannot be undone. Duplicate Detection Report SharePoint File Search Results File Search Results + SharePoint Version Cleanup Report + Version Cleanup Report Total Accesses Users Audited Sites Scanned diff --git a/SharepointToolbox/Services/Export/VersionCleanupHtmlExportService.cs b/SharepointToolbox/Services/Export/VersionCleanupHtmlExportService.cs new file mode 100644 index 0000000..c0ddafb --- /dev/null +++ b/SharepointToolbox/Services/Export/VersionCleanupHtmlExportService.cs @@ -0,0 +1,180 @@ +using System.Text; +using SharepointToolbox.Core.Models; +using SharepointToolbox.Localization; + +namespace SharepointToolbox.Services.Export; + +/// +/// Exports VersionCleanupResult list to a self-contained sortable/filterable HTML report. +/// Summary header shows totals (files trimmed, versions deleted, bytes freed); a single +/// table lists every processed file with sort/filter controls. No external assets. +/// +public class VersionCleanupHtmlExportService +{ + public string BuildHtml(IReadOnlyList results, ReportBranding? branding = null) + { + var T = TranslationSource.Instance; + var sb = new StringBuilder(); + + long totalBytes = results.Sum(r => r.BytesFreed); + int totalDeleted = results.Sum(r => r.VersionsDeleted); + int totalFiles = results.Count(r => r.VersionsDeleted > 0); + + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine($"{T["report.title.versions"]}"); + sb.AppendLine(""" + + + + """); + sb.Append(BrandingHtmlHelper.BuildBrandingHeader(branding)); + sb.AppendLine($"

{T["report.title.versions_short"]}

"); + + sb.AppendLine($""" +
+
{T["versions.summary.files"]}{totalFiles:N0}
+
{T["versions.summary.deleted"]}{totalDeleted:N0}
+
{T["versions.summary.freed"]}{FormatSize(totalBytes)}
+
+
+ + + +
+ """); + + sb.AppendLine($""" + + + + + + + + + + + + + + + + """); + + foreach (var r in results) + { + string rowClass = string.IsNullOrEmpty(r.Error) ? string.Empty : " class=\"err\""; + string errCell = string.IsNullOrEmpty(r.Error) + ? string.Empty + : $"{H(r.Error)}"; + + sb.AppendLine($""" + + + + + + + + + + + + """); + } + + sb.AppendLine(" \n
{T["report.col.site"]}{T["versions.col.library"]}{T["versions.col.file"]}{T["versions.col.path"]}{T["versions.col.before"]}{T["versions.col.deleted"]}{T["versions.col.remaining"]}{T["versions.col.freed"]}{T["versions.col.error"]}
{H(r.SiteUrl)}{H(r.Library)}{H(r.FileName)}{H(r.FileServerRelativeUrl)}{r.VersionsBefore:N0}{r.VersionsDeleted:N0}{r.VersionsRemaining:N0}{FormatSize(r.BytesFreed)}{errCell}
"); + + int count = results.Count; + sb.AppendLine($"

{T["report.text.generated_colon"]} {DateTime.Now:yyyy-MM-dd HH:mm} — {count:N0} {T["report.text.results_parens"]}

"); + + sb.AppendLine($$""" + + + """); + + return sb.ToString(); + } + + /// Writes the HTML report to as UTF-8. + public async Task WriteAsync(IReadOnlyList results, string filePath, CancellationToken ct, ReportBranding? branding = null) + { + var html = BuildHtml(results, branding); + await System.IO.File.WriteAllTextAsync(filePath, html, Encoding.UTF8, ct); + } + + private static string H(string value) => + System.Net.WebUtility.HtmlEncode(value ?? string.Empty); + + private static string FormatSize(long bytes) + { + if (bytes >= 1_073_741_824L) return $"{bytes / 1_073_741_824.0:F2} GB"; + if (bytes >= 1_048_576L) return $"{bytes / 1_048_576.0:F2} MB"; + if (bytes >= 1024L) return $"{bytes / 1024.0:F2} KB"; + return $"{bytes} B"; + } +} diff --git a/SharepointToolbox/ViewModels/Tabs/VersionCleanupViewModel.cs b/SharepointToolbox/ViewModels/Tabs/VersionCleanupViewModel.cs index 83432d0..266f6f5 100644 --- a/SharepointToolbox/ViewModels/Tabs/VersionCleanupViewModel.cs +++ b/SharepointToolbox/ViewModels/Tabs/VersionCleanupViewModel.cs @@ -8,6 +8,7 @@ using Microsoft.Win32; using SharepointToolbox.Core.Models; using SharepointToolbox.Localization; using SharepointToolbox.Services; +using SharepointToolbox.Services.Export; namespace SharepointToolbox.ViewModels.Tabs; @@ -15,6 +16,8 @@ public partial class VersionCleanupViewModel : FeatureViewModelBase { private readonly IVersionCleanupService _versionService; private readonly ISessionManager _sessionManager; + private readonly VersionCleanupHtmlExportService _htmlExportService; + private readonly IBrandingService _brandingService; private readonly ILogger _logger; private TenantProfile? _currentProfile; @@ -50,20 +53,26 @@ public partial class VersionCleanupViewModel : FeatureViewModelBase public IAsyncRelayCommand SelectLibrariesCommand { get; } public IRelayCommand ClearLibrariesCommand { get; } public IAsyncRelayCommand ExportCsvCommand { get; } + public IAsyncRelayCommand ExportHtmlCommand { get; } public VersionCleanupViewModel( IVersionCleanupService versionService, ISessionManager sessionManager, + VersionCleanupHtmlExportService htmlExportService, + IBrandingService brandingService, ILogger logger) : base(logger) { _versionService = versionService; _sessionManager = sessionManager; + _htmlExportService = htmlExportService; + _brandingService = brandingService; _logger = logger; SelectLibrariesCommand = new AsyncRelayCommand(SelectLibrariesAsync, CanPickLibraries); ClearLibrariesCommand = new RelayCommand(ClearLibraries); ExportCsvCommand = new AsyncRelayCommand(ExportCsvAsync, () => Results.Count > 0); + ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, () => Results.Count > 0); SelectedLibraries.CollectionChanged += (_, _) => UpdateSelectedLibrariesLabel(); Results.CollectionChanged += (_, _) => @@ -73,6 +82,7 @@ public partial class VersionCleanupViewModel : FeatureViewModelBase OnPropertyChanged(nameof(TotalVersionsDeleted)); OnPropertyChanged(nameof(TotalFilesAffected)); ExportCsvCommand.NotifyCanExecuteChanged(); + ExportHtmlCommand.NotifyCanExecuteChanged(); }; UpdateSelectedLibrariesLabel(); } @@ -212,8 +222,7 @@ public partial class VersionCleanupViewModel : FeatureViewModelBase r.BytesFreed, Csv(r.Error ?? string.Empty))); } - try { Process.Start(new ProcessStartInfo(dialog.FileName) { UseShellExecute = true }); } - catch { } + OpenFile(dialog.FileName); } catch (Exception ex) { @@ -222,6 +231,39 @@ public partial class VersionCleanupViewModel : FeatureViewModelBase } } + private async Task ExportHtmlAsync() + { + if (Results.Count == 0) return; + var dialog = new SaveFileDialog + { + Title = "Export version cleanup results to HTML", + Filter = "HTML files (*.html)|*.html|All files (*.*)|*.*", + DefaultExt = "html", + FileName = "version_cleanup", + }; + if (dialog.ShowDialog() != true) return; + try + { + var mspLogo = await _brandingService.GetMspLogoAsync(); + var clientLogo = _currentProfile?.ClientLogo; + var branding = new ReportBranding(mspLogo, clientLogo); + + await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, branding); + OpenFile(dialog.FileName); + } + catch (Exception ex) + { + StatusMessage = $"Export failed: {ex.Message}"; + _logger.LogError(ex, "Version cleanup HTML export failed."); + } + } + + private static void OpenFile(string filePath) + { + try { Process.Start(new ProcessStartInfo(filePath) { UseShellExecute = true }); } + catch { } + } + private static string Csv(string value) { if (string.IsNullOrEmpty(value)) return string.Empty; diff --git a/SharepointToolbox/Views/Tabs/VersionCleanupView.xaml b/SharepointToolbox/Views/Tabs/VersionCleanupView.xaml index e17816a..183f1b5 100644 --- a/SharepointToolbox/Views/Tabs/VersionCleanupView.xaml +++ b/SharepointToolbox/Views/Tabs/VersionCleanupView.xaml @@ -43,6 +43,8 @@