--- phase: 16-report-consolidation-toggle plan: "01" type: execute wave: 1 depends_on: [] files_modified: - SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs - SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs - SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml - SharepointToolbox/Views/Tabs/PermissionsView.xaml - SharepointToolbox/Localization/Strings.resx - SharepointToolbox/Localization/Strings.fr.resx - SharepointToolbox/Services/Export/UserAccessCsvExportService.cs autonomous: true requirements: [RPT-03] must_haves: truths: - "MergePermissions property exists on UserAccessAuditViewModel and defaults to false" - "MergePermissions property exists on PermissionsViewModel and defaults to false (no-op placeholder)" - "Export Options GroupBox with 'Merge duplicate permissions' checkbox is visible in both audit tabs" - "CSV export with mergePermissions=false produces byte-identical output to current behavior" - "CSV export with mergePermissions=true writes consolidated rows with Locations column" artifacts: - path: "SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs" provides: "MergePermissions ObservableProperty + export call site wiring" contains: "_mergePermissions" - path: "SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs" provides: "MergePermissions ObservableProperty (no-op placeholder)" contains: "_mergePermissions" - path: "SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml" provides: "Export Options GroupBox with checkbox" contains: "Export Options" - path: "SharepointToolbox/Views/Tabs/PermissionsView.xaml" provides: "Export Options GroupBox with checkbox" contains: "Export Options" - path: "SharepointToolbox/Services/Export/UserAccessCsvExportService.cs" provides: "Consolidated CSV export path" contains: "mergePermissions" key_links: - from: "UserAccessAuditView.xaml" to: "UserAccessAuditViewModel.MergePermissions" via: "XAML Binding" pattern: "IsChecked.*Binding MergePermissions" - from: "UserAccessAuditViewModel.ExportCsvAsync" to: "UserAccessCsvExportService.WriteSingleFileAsync" via: "MergePermissions parameter passthrough" pattern: "WriteSingleFileAsync.*MergePermissions" --- Add the MergePermissions toggle property to both ViewModels, wire Export Options GroupBox in both XAML views, add localization keys, and implement the consolidated CSV export path. Purpose: Establishes the user-facing toggle and the simpler CSV consolidation path, leaving the complex HTML rendering for Plan 02. Output: Working toggle UI in both tabs, consolidated CSV export, non-consolidated paths unchanged. @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/ROADMAP.md @.planning/STATE.md @.planning/phases/16-report-consolidation-toggle/16-CONTEXT.md @.planning/phases/16-report-consolidation-toggle/16-RESEARCH.md @.planning/phases/15-consolidation-data-model/15-01-SUMMARY.md From SharepointToolbox/Core/Helpers/PermissionConsolidator.cs: ```csharp public static class PermissionConsolidator { internal static string MakeKey(UserAccessEntry e); public static IReadOnlyList Consolidate(IReadOnlyList entries); } ``` From SharepointToolbox/Core/Models/ConsolidatedPermissionEntry.cs: ```csharp public record ConsolidatedPermissionEntry( string UserDisplayName, string UserLogin, string PermissionLevel, string AccessType, string GrantedThrough, bool IsExternalUser, bool IsHighPrivilege, IReadOnlyList Locations) { public int LocationCount => Locations.Count; } ``` From SharepointToolbox/Core/Models/LocationInfo.cs: ```csharp public record LocationInfo(string SiteUrl, string SiteTitle, string ObjectTitle, string ObjectUrl, string ObjectType); ``` From SharepointToolbox/Services/Export/UserAccessCsvExportService.cs: ```csharp public class UserAccessCsvExportService { public string BuildCsv(string userDisplayName, string userLogin, IReadOnlyList entries); public async Task WriteAsync(IReadOnlyList entries, string outputDirectory, CancellationToken ct); public async Task WriteSingleFileAsync(IReadOnlyList entries, string filePath, CancellationToken ct); } ``` From SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs (export call sites): ```csharp // Line 495 — ExportCsvAsync: await _csvExportService.WriteSingleFileAsync(Results, dialog.FileName, CancellationToken.None); // Line 526 — ExportHtmlAsync: await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, branding); ``` Task 1: Add MergePermissions property to both ViewModels and localization keys SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs, SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs, SharepointToolbox/Localization/Strings.resx, SharepointToolbox/Localization/Strings.fr.resx 1. In `UserAccessAuditViewModel.cs`, add a new `[ObservableProperty]` field after the existing observable properties block (around line 101): ```csharp [ObservableProperty] private bool _mergePermissions; ``` No partial handler needed — the property defaults to `false` and has no side effects on change. 2. In `PermissionsViewModel.cs`, add the same `[ObservableProperty]` field in the observable properties section: ```csharp [ObservableProperty] private bool _mergePermissions; ``` This is a no-op placeholder — PermissionsViewModel does NOT use this value in any export logic. 3. In `Strings.resx`, add two new entries following the existing naming convention (look at existing keys like `audit.grp.scanOptions`, `chk.includeInherited` etc. for the exact naming pattern): - Key: `audit.grp.export` — Value: `Export Options` - Key: `chk.merge.permissions` — Value: `Merge duplicate permissions` 4. In `Strings.fr.resx`, add the same two keys: - Key: `audit.grp.export` — Value: `Options d'exportation` - Key: `chk.merge.permissions` — Value: `Fusionner les permissions en double` IMPORTANT: Both .resx files MUST have the keys added. Missing French keys cause empty strings in French locale. cd "C:/Users/dev/Documents/projets/Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-restore -v q 2>&1 | tail -5 Both ViewModels have MergePermissions property that defaults to false. Both .resx files have the two new localization keys. Solution builds without errors or warnings. Task 2: Add Export Options GroupBox to both XAML views SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml, SharepointToolbox/Views/Tabs/PermissionsView.xaml 1. In `UserAccessAuditView.xaml`, locate the "Scan Options" GroupBox (around lines 199-210 — look for `GroupBox Header="{Binding [audit.grp.scanOptions]..."` or similar). Add a new GroupBox immediately AFTER the Scan Options GroupBox, within the same DockPanel, using the identical pattern: ```xml ``` Match the exact `Source={x:Static loc:TranslationSource.Instance}` pattern used by the existing Scan Options GroupBox for localized headers and checkbox labels. 2. In `PermissionsView.xaml`, locate the "Display Options" GroupBox in the left panel. Add the same Export Options GroupBox after it, using the same XAML pattern as above. The binding `{Binding MergePermissions}` will bind to `PermissionsViewModel.MergePermissions` (the no-op placeholder). IMPORTANT: Do NOT modify any existing XAML elements. Only ADD the new GroupBox. The GroupBox must be always visible (not conditionally hidden). cd "C:/Users/dev/Documents/projets/Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-restore -v q 2>&1 | tail -5 Both XAML views show an "Export Options" GroupBox with a "Merge duplicate permissions" checkbox bound to MergePermissions. Existing UI elements are unchanged. Task 3: Implement consolidated CSV export path and wire ViewModel call site SharepointToolbox/Services/Export/UserAccessCsvExportService.cs, SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs, SharepointToolbox.Tests/Services/Export/UserAccessCsvExportServiceTests.cs - RPT-03-f: `WriteSingleFileAsync(entries, path, ct, mergePermissions: false)` produces byte-identical output to current `WriteSingleFileAsync(entries, path, ct)` — capture current output first, then verify no change - RPT-03-g: `WriteSingleFileAsync(entries, path, ct, mergePermissions: true)` writes consolidated CSV with header `"User","User Login","Permission Level","Access Type","Granted Through","Locations","Location Count"` and semicolon-separated site titles in Locations column - Edge case: single-location consolidated entry has LocationCount=1 and Locations=single site title (no semicolons) 1. In `UserAccessCsvExportService.cs`, add `bool mergePermissions = false` parameter to `WriteSingleFileAsync`: ```csharp public async Task WriteSingleFileAsync( IReadOnlyList entries, string filePath, CancellationToken ct, bool mergePermissions = false) ``` 2. At the top of `WriteSingleFileAsync`, add an early-return consolidated branch: ```csharp if (mergePermissions) { var consolidated = PermissionConsolidator.Consolidate(entries); // Build consolidated CSV with distinct header and rows var sb = new StringBuilder(); // Summary section (same pattern as existing) sb.AppendLine("\"User Access Audit Report (Consolidated)\""); sb.AppendLine($"\"Users Audited\",\"{consolidated.Select(e => e.UserLogin).Distinct().Count()}\""); sb.AppendLine($"\"Total Entries\",\"{consolidated.Count}\""); sb.AppendLine($"\"Generated\",\"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\""); sb.AppendLine(); sb.AppendLine("\"User\",\"User Login\",\"Permission Level\",\"Access Type\",\"Granted Through\",\"Locations\",\"Location Count\""); foreach (var entry in consolidated) { var locations = string.Join("; ", entry.Locations.Select(l => l.SiteTitle)); sb.AppendLine(string.Join(",", new[] { $"\"{entry.UserDisplayName}\"", $"\"{entry.UserLogin}\"", $"\"{entry.PermissionLevel}\"", $"\"{entry.AccessType}\"", $"\"{entry.GrantedThrough}\"", $"\"{locations}\"", $"\"{entry.LocationCount}\"" })); } await File.WriteAllTextAsync(filePath, sb.ToString(), new UTF8Encoding(false), ct); return; } ``` Leave the existing code path below this branch COMPLETELY UNTOUCHED. 3. In `UserAccessAuditViewModel.cs`, update the `ExportCsvAsync` method call site (line ~495): Change: `await _csvExportService.WriteSingleFileAsync(Results, dialog.FileName, CancellationToken.None);` To: `await _csvExportService.WriteSingleFileAsync(Results, dialog.FileName, CancellationToken.None, MergePermissions);` 4. Add test methods in `UserAccessCsvExportServiceTests.cs` for RPT-03-f (non-consolidated identical) and RPT-03-g (consolidated CSV format). Use test data with 2-3 UserAccessEntry rows where 2 share the same consolidation key. IMPORTANT: Do NOT modify `BuildCsv` — consolidation applies only at `WriteSingleFileAsync` level per RESEARCH.md Pitfall 4. Do NOT touch the existing code path below the `if (mergePermissions)` branch. cd "C:/Users/dev/Documents/projets/Sharepoint" && dotnet test --filter "FullyQualifiedName~UserAccessCsvExportServiceTests" --no-restore -v q 2>&1 | tail -10 CSV export with mergePermissions=false is identical to pre-toggle output. CSV export with mergePermissions=true writes consolidated rows with Locations and LocationCount columns. ViewModel passes MergePermissions to the service. 1. `dotnet build` succeeds with 0 errors, 0 warnings 2. `dotnet test --filter "FullyQualifiedName~UserAccessCsvExportServiceTests"` — all tests pass including new consolidation tests 3. `dotnet test` — full suite green, no regressions - MergePermissions property exists on both ViewModels, defaults to false - Export Options GroupBox visible in both XAML tabs with localized labels - CSV consolidated path produces correct output with merged rows - Non-consolidated CSV path is byte-identical to pre-Phase-16 output - All existing tests pass without modification After completion, create `.planning/phases/16-report-consolidation-toggle/16-01-SUMMARY.md`