--- phase: 02-permissions plan: 04 type: execute wave: 2 depends_on: - 02-02 files_modified: - SharepointToolbox/Services/Export/CsvExportService.cs - SharepointToolbox/Services/Export/HtmlExportService.cs autonomous: true requirements: - PERM-05 - PERM-06 must_haves: truths: - "CsvExportService.BuildCsv produces a valid CSV string with the correct 9-column header and one data row per merged permission entry" - "Entries with identical Users + PermissionLevels + GrantedThrough but different URLs are merged into one row with pipe-joined URLs (Merge-PermissionRows port)" - "HtmlExportService.BuildHtml produces a self-contained HTML file (no external CSS/JS dependencies) that contains all user display names from the input" - "HTML report includes stats cards: Total Entries, Unique Permission Sets, Distinct Users/Groups" - "CSV fields with commas or quotes are correctly escaped per RFC 4180" artifacts: - path: "SharepointToolbox/Services/Export/CsvExportService.cs" provides: "Merges PermissionEntry rows and writes CSV" exports: ["CsvExportService"] - path: "SharepointToolbox/Services/Export/HtmlExportService.cs" provides: "Generates self-contained interactive HTML report" exports: ["HtmlExportService"] key_links: - from: "CsvExportService.cs" to: "PermissionEntry" via: "groups by (Users, PermissionLevels, GrantedThrough)" pattern: "GroupBy" - from: "HtmlExportService.cs" to: "PermissionEntry" via: "iterates all entries to build HTML rows" pattern: "foreach.*PermissionEntry" --- Implement the two export services: `CsvExportService` (port of PS `Merge-PermissionRows` + `Export-Csv`) and `HtmlExportService` (port of PS `Export-PermissionsToHTML`). Both services consume `IReadOnlyList` and write files. Purpose: Deliver PERM-05 (CSV export) and PERM-06 (HTML export). These are pure data-transformation services with no UI dependency — they can be verified fully by the automated test stubs created in Plan 01. Output: 2 export service files. @C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/phases/02-permissions/02-RESEARCH.md From SharepointToolbox/Core/Models/PermissionEntry.cs: ```csharp public record PermissionEntry( string ObjectType, // "Site Collection" | "Site" | "List" | "Folder" string Title, string Url, bool HasUniquePermissions, string Users, // Semicolon-joined display names string UserLogins, // Semicolon-joined login names string PermissionLevels, // Semicolon-joined role names string GrantedThrough, // "Direct Permissions" | "SharePoint Group: " string PrincipalType // "SharePointGroup" | "User" | "External User" ); ``` CSV merge logic (port of PS Merge-PermissionRows): - Group by key: (Users, PermissionLevels, GrantedThrough) - For each group: collect all Urls, join with " | " - Collect all Titles, join with " | " - Take first ObjectType, HasUniquePermissions from group CSV columns (9 total): Object, Title, URL, HasUniquePermissions, Users, UserLogins, Type, Permissions, GrantedThrough CSV escaping: enclose every field in double quotes, escape internal quotes by doubling them. HTML report key features (port of PS Export-PermissionsToHTML): - Stats cards: Total Entries (count of entries), Unique Permission Sets (count of distinct PermissionLevels values), Distinct Users/Groups (count of distinct users across all UserLogins) - Filter input (vanilla JS filterTable()) - Type badge: color-coded span for ObjectType ("Site Collection"=blue, "Site"=green, "List"=yellow, "Folder"=gray) - Unique vs Inherited badge per row (HasUniquePermissions → green "Unique", else gray "Inherited") - User pills with data-email attribute for each login in UserLogins (split by ;) - Self-contained: all CSS and JS inline in the HTML string — no external file dependencies - Table columns: Object, Title, URL, Unique, Users, Permissions, Granted Through Task 1: Implement CsvExportService SharepointToolbox/Services/Export/CsvExportService.cs - BuildCsv(IReadOnlyList<PermissionEntry> entries) returns string - Header row: Object,Title,URL,HasUniquePermissions,Users,UserLogins,Type,Permissions,GrantedThrough (all quoted) - Merge rows: entries grouped by (Users, PermissionLevels, GrantedThrough) → one output row per group with URLs pipe-joined - Fields with commas, double quotes, or newlines are wrapped in double quotes with internal quotes doubled - WriteAsync(entries, filePath, ct) calls BuildCsv then writes UTF-8 with BOM (for Excel compatibility) - The test from Plan 01 (BuildCsv_WithDuplicateUserPermissionGrantedThrough_MergesLocations) passes Create `SharepointToolbox/Services/Export/` directory if it doesn't exist. Create `SharepointToolbox/Services/Export/CsvExportService.cs`: ```csharp namespace SharepointToolbox.Services.Export; public class CsvExportService { private const string Header = "\"Object\",\"Title\",\"URL\",\"HasUniquePermissions\",\"Users\",\"UserLogins\",\"Type\",\"Permissions\",\"GrantedThrough\""; public string BuildCsv(IReadOnlyList entries) { var sb = new StringBuilder(); sb.AppendLine(Header); // Merge: group by (Users, PermissionLevels, GrantedThrough) var merged = entries .GroupBy(e => (e.Users, e.PermissionLevels, e.GrantedThrough)) .Select(g => new { ObjectType = g.First().ObjectType, Title = string.Join(" | ", g.Select(e => e.Title).Distinct()), Url = string.Join(" | ", g.Select(e => e.Url).Distinct()), HasUnique = g.First().HasUniquePermissions, Users = g.Key.Users, UserLogins = g.First().UserLogins, PrincipalType= g.First().PrincipalType, Permissions = g.Key.PermissionLevels, GrantedThrough = g.Key.GrantedThrough }); foreach (var row in merged) sb.AppendLine(string.Join(",", new[] { Csv(row.ObjectType), Csv(row.Title), Csv(row.Url), Csv(row.HasUnique.ToString()), Csv(row.Users), Csv(row.UserLogins), Csv(row.PrincipalType), Csv(row.Permissions), Csv(row.GrantedThrough) })); return sb.ToString(); } public async Task WriteAsync(IReadOnlyList entries, string filePath, CancellationToken ct) { var csv = BuildCsv(entries); await File.WriteAllTextAsync(filePath, csv, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), ct); } private static string Csv(string value) { if (string.IsNullOrEmpty(value)) return "\"\""; return $"\"{value.Replace("\"", "\"\"")}\""; } } ``` Usings: `System.IO`, `System.Text`, `SharepointToolbox.Core.Models`. Namespace: `SharepointToolbox.Services.Export`. dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~CsvExportServiceTests" -x All 3 CsvExportServiceTests pass (header row present, merge works, empty list returns header only). dotnet build 0 errors. Task 2: Implement HtmlExportService SharepointToolbox/Services/Export/HtmlExportService.cs - BuildHtml(IReadOnlyList<PermissionEntry> entries) returns a self-contained HTML string - Output contains user display names from the input (test: BuildHtml_WithKnownEntries_ContainsUserNames passes) - Output contains all inline CSS and JS — no <link> or <script src=...> tags - Stats cards reflect: Total Entries count, Unique Permission Sets (distinct PermissionLevels values), Distinct Users (distinct entries in UserLogins split by semicolon) - Type badge CSS classes: site-coll, site, list, folder — color-coded - Unique/Inherited badge based on HasUniquePermissions - Filter input calls JS filterTable() on keyup — filters by any visible text in the row - External user tag: if UserLogins contains "#EXT#", user pill gets class "external-user" and data-email attribute - WriteAsync(entries, filePath, ct) writes UTF-8 (no BOM for HTML) - The test from Plan 01 (BuildHtml_WithExternalUser_ContainsExtHashMarker) passes — HTML contains "external-user" class Create `SharepointToolbox/Services/Export/HtmlExportService.cs`. Structure the HTML report as a multi-line C# string literal inside BuildHtml(). Use `StringBuilder` to assemble: 1. HTML head (with inline CSS): table styles, badge styles (site-coll=blue, site=green, list=amber, folder=gray, unique=green, inherited=gray), user pill styles, external-user pill style (orange border), stats card styles, filter input style 2. Body open: h1 "SharePoint Permissions Report", stats cards div (compute counts from entries), filter input 3. Table with columns: Object | Title | URL | Unique | Users/Groups | Permission Level | Granted Through 4. For each entry: one `` with: - `{ObjectType}` - `{Title}` - `Link` - `{Unique/Inherited}` - `` + user pills: split UserLogins by ';', split Users by ';', zip them, render `{name}` - `{PermissionLevels}` - `{GrantedThrough}` 5. Inline JS: filterTable() function that iterates `` elements and shows/hides based on input text match against `tr.textContent` 6. Close body/html Helper method `private static string ObjectTypeCss(string t)`: - "Site Collection" → "badge site-coll" - "Site" → "badge site" - "List" → "badge list" - "Folder" → "badge folder" - else → "badge" Stats computation: ```csharp var totalEntries = entries.Count; var uniquePermSets = entries.Select(e => e.PermissionLevels).Distinct().Count(); var distinctUsers = entries.SelectMany(e => e.UserLogins.Split(';', StringSplitOptions.RemoveEmptyEntries)) .Select(u => u.Trim()).Where(u => u.Length > 0).Distinct().Count(); ``` Namespace: `SharepointToolbox.Services.Export`. Usings: `System.IO`, `System.Text`, `SharepointToolbox.Core.Models`. dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj --filter "FullyQualifiedName~HtmlExportServiceTests" -x All 3 HtmlExportServiceTests pass (user name present, empty list produces valid HTML, external user gets external-user class). dotnet build 0 errors. - `dotnet test C:/Users/dev/Documents/projets/Sharepoint/SharepointToolbox.slnx` → all tests pass (Phase 1 + new export tests) - CsvExportServiceTests: 3 green - HtmlExportServiceTests: 3 green - HTML output contains no external script/link tags (grep verifiable: no `src=` or `href=` outside the table) - CsvExportService merges rows by (Users, PermissionLevels, GrantedThrough) before writing - CSV uses UTF-8 with BOM for Excel compatibility - HtmlExportService produces self-contained HTML with inline CSS and JS - HTML correctly marks external users with "external-user" CSS class - All 6 export tests pass (3 CSV + 3 HTML) After completion, create `.planning/phases/02-permissions/02-04-SUMMARY.md`