From fc1ba00aa8a6c6debb9a8e486c859b379839889c Mon Sep 17 00:00:00 2001 From: Dev Date: Thu, 2 Apr 2026 15:38:43 +0200 Subject: [PATCH] feat(03-05): implement DuplicatesHtmlExportService with grouped cards - Replace stub with full grouped HTML export (port of PS Export-DuplicatesToHTML) - One collapsible card per DuplicateGroup with item count badge and path table - Uses System.IO.File explicitly per WPF project pattern - 3/3 DuplicatesHtmlExportServiceTests pass; 9/9 total export tests pass --- .../Export/DuplicatesHtmlExportService.cs | 126 +++++++++++++++++- 1 file changed, 124 insertions(+), 2 deletions(-) diff --git a/SharepointToolbox/Services/Export/DuplicatesHtmlExportService.cs b/SharepointToolbox/Services/Export/DuplicatesHtmlExportService.cs index 3376a86..3055346 100644 --- a/SharepointToolbox/Services/Export/DuplicatesHtmlExportService.cs +++ b/SharepointToolbox/Services/Export/DuplicatesHtmlExportService.cs @@ -1,14 +1,136 @@ using SharepointToolbox.Core.Models; +using System.Text; namespace SharepointToolbox.Services.Export; +/// +/// Exports DuplicateGroup list to a self-contained HTML with collapsible group cards. +/// Port of PS Export-DuplicatesToHTML (PS lines 2235-2406). +/// Each group gets a card showing item count badge and a table of paths. +/// public class DuplicatesHtmlExportService { - public string BuildHtml(IReadOnlyList groups) => string.Empty; // implemented in Plan 03-05 + public string BuildHtml(IReadOnlyList groups) + { + var sb = new StringBuilder(); + + sb.AppendLine(""" + + + + + + SharePoint Duplicate Detection Report + + + + +

Duplicate Detection Report

+ """); + + sb.AppendLine($"

{groups.Count:N0} duplicate group(s) found.

"); + + for (int i = 0; i < groups.Count; i++) + { + var g = groups[i]; + int count = g.Items.Count; + string badgeClass = "badge-dup"; + + sb.AppendLine($""" +
+
+ {H(g.Name)} + {count} copies +
+
+ + + + + + + + + + + + + """); + + for (int j = 0; j < g.Items.Count; j++) + { + var item = g.Items[j]; + string size = item.SizeBytes.HasValue ? FormatSize(item.SizeBytes.Value) : string.Empty; + string created = item.Created.HasValue ? item.Created.Value.ToString("yyyy-MM-dd") : string.Empty; + string modified = item.Modified.HasValue ? item.Modified.Value.ToString("yyyy-MM-dd") : string.Empty; + + sb.AppendLine($""" + + + + + + + + + """); + } + + sb.AppendLine(""" + +
#LibraryPathSizeCreatedModified
{j + 1}{H(item.Library)}{H(item.Path)}{size}{created}{modified}
+
+
+ """); + } + + sb.AppendLine($"

Generated: {DateTime.Now:yyyy-MM-dd HH:mm}

"); + sb.AppendLine(""); + + return sb.ToString(); + } public async Task WriteAsync(IReadOnlyList groups, string filePath, CancellationToken ct) { var html = BuildHtml(groups); - await System.IO.File.WriteAllTextAsync(filePath, html, System.Text.Encoding.UTF8, ct); + 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"; } }