--- phase: 11-html-export-branding plan: 03 type: execute wave: 3 depends_on: ["11-02"] files_modified: - SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs - SharepointToolbox/ViewModels/Tabs/SearchViewModel.cs - SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs - SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs - SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs # Note: App.xaml.cs does NOT need changes — DI container auto-resolves IBrandingService for ViewModel constructors autonomous: true requirements: - BRAND-05 must_haves: truths: - "Each of the 5 export ViewModels injects IBrandingService and assembles ReportBranding before calling WriteAsync" - "ReportBranding is assembled from IBrandingService.GetMspLogoAsync() for MSP logo and _currentProfile.ClientLogo for client logo" - "The branding ReportBranding is passed as the last parameter to WriteAsync" - "DI container provides IBrandingService to all 5 export ViewModels" artifacts: - path: "SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs" provides: "Permissions export with branding assembly" contains: "IBrandingService" - path: "SharepointToolbox/ViewModels/Tabs/SearchViewModel.cs" provides: "Search export with branding assembly" contains: "IBrandingService" - path: "SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs" provides: "Storage export with branding assembly" contains: "IBrandingService" - path: "SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs" provides: "Duplicates export with branding assembly" contains: "IBrandingService" - path: "SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs" provides: "User access export with branding assembly" contains: "IBrandingService" key_links: - from: "SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs" to: "SharepointToolbox/Services/IBrandingService.cs" via: "constructor injection" pattern: "IBrandingService _brandingService" - from: "SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs" to: "SharepointToolbox/Services/Export/HtmlExportService.cs" via: "WriteAsync with branding" pattern: "WriteAsync.*branding" --- Wire IBrandingService into all 5 export ViewModels so each ExportHtmlAsync method assembles a ReportBranding from the MSP logo and the active tenant's client logo, then passes it to WriteAsync. Purpose: Connects the branding infrastructure (Plan 01) and export service changes (Plan 02) to the user-facing export commands. After this plan, HTML exports include branding logos when configured. Output: All 5 export ViewModels inject IBrandingService, assemble ReportBranding in ExportHtmlAsync, and pass it to WriteAsync. @C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/dev/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/11-html-export-branding/11-CONTEXT.md @.planning/phases/11-html-export-branding/11-RESEARCH.md @.planning/phases/11-html-export-branding/11-02-SUMMARY.md From SharepointToolbox/Core/Models/ReportBranding.cs: ```csharp public record ReportBranding(LogoData? MspLogo, LogoData? ClientLogo); ``` From SharepointToolbox/Services/IBrandingService.cs: ```csharp public interface IBrandingService { Task ImportLogoAsync(string filePath); Task SaveMspLogoAsync(LogoData logo); Task ClearMspLogoAsync(); Task GetMspLogoAsync(); } ``` DuplicatesViewModel constructor pattern: ```csharp public DuplicatesViewModel( IDuplicatesService duplicatesService, ISessionManager sessionManager, DuplicatesHtmlExportService htmlExportService, ILogger logger) : base(logger) ``` Each ViewModel has: - `private TenantProfile? _currentProfile;` field set via OnTenantSwitched - `ExportHtmlAsync()` method calling `_htmlExportService.WriteAsync(..., CancellationToken.None)` PermissionsViewModel has two constructors: full (DI) and test (internal, omits export services). UserAccessAuditViewModel also has two constructors. StorageViewModel also has two constructors (test constructor at line 151). The other 2 ViewModels (Search, Duplicates) have a single constructor each. DI registrations in App.xaml.cs: ```csharp services.AddSingleton(); ``` IBrandingService is already registered — no new DI registration needed for the service itself. But each ViewModel registration must now resolve IBrandingService in addition to existing deps. Task 1: Inject IBrandingService into all 5 export ViewModels and assemble branding in ExportHtmlAsync SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs, SharepointToolbox/ViewModels/Tabs/SearchViewModel.cs, SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs, SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs, SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs Apply the same pattern to all 5 ViewModels: **For each ViewModel:** 1. Add `using SharepointToolbox.Core.Models;` if not already present (needed for `ReportBranding`). 2. Add field: `private readonly IBrandingService _brandingService;` 3. Modify the DI constructor to accept `IBrandingService brandingService` parameter and assign `_brandingService = brandingService;`. 4. For ViewModels with a test constructor (PermissionsViewModel, UserAccessAuditViewModel, StorageViewModel): add `IBrandingService? brandingService = null` as the last parameter, assign `_brandingService = brandingService!;`. Using `null!` is acceptable because test constructors are only used in tests where branding is not exercised. Alternatively, create a no-op implementation — but `null!` matches existing pattern where `_htmlExportService = null` is already used in test constructors. **Verify that existing test files for all 3 ViewModels still compile after the constructor changes.** 5. Modify `ExportHtmlAsync()` — add branding assembly BEFORE the WriteAsync call: ```csharp // Assemble branding var mspLogo = await _brandingService.GetMspLogoAsync(); var clientLogo = _currentProfile?.ClientLogo; var branding = new ReportBranding(mspLogo, clientLogo); ``` Then pass `branding` as the last argument to each `WriteAsync` call. **Specific details per ViewModel:** **PermissionsViewModel** (2 WriteAsync calls in ExportHtmlAsync): ```csharp // Before: await _htmlExportService.WriteAsync(SimplifiedResults.ToList(), dialog.FileName, CancellationToken.None); // After: await _htmlExportService.WriteAsync(SimplifiedResults.ToList(), dialog.FileName, CancellationToken.None, branding); ``` Same for the non-simplified path. **SearchViewModel** (1 WriteAsync call): ```csharp await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, branding); ``` **StorageViewModel** (1 WriteAsync call — the one with FileTypeMetrics): ```csharp await _htmlExportService.WriteAsync(Results, FileTypeMetrics, dialog.FileName, CancellationToken.None, branding); ``` **DuplicatesViewModel** (1 WriteAsync call): ```csharp await _htmlExportService.WriteAsync(_lastGroups, dialog.FileName, CancellationToken.None, branding); ``` **UserAccessAuditViewModel** (1 WriteAsync call): ```csharp await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, branding); ``` **Guard clause:** Add a null check on `_brandingService` before the branding assembly to be safe (in case the test constructor was used). If `_brandingService is null`, set `branding = null` (which means no branding header — graceful degradation): ```csharp ReportBranding? branding = null; if (_brandingService is not null) { var mspLogo = await _brandingService.GetMspLogoAsync(); var clientLogo = _currentProfile?.ClientLogo; branding = new ReportBranding(mspLogo, clientLogo); } ``` dotnet build --no-restore -warnaserror && dotnet test SharepointToolbox.Tests --no-build -q All 5 export ViewModels inject IBrandingService, assemble ReportBranding from MSP logo + active profile's ClientLogo, and pass it to WriteAsync. Build and all tests pass. Test constructors gracefully handle null IBrandingService. ```bash dotnet build --no-restore -warnaserror dotnet test SharepointToolbox.Tests --no-build -q ``` Both commands must pass. Full test suite must pass — existing ViewModel tests must not break from the constructor changes. - All 5 export ViewModels have IBrandingService injected via constructor - ExportHtmlAsync assembles ReportBranding from GetMspLogoAsync + _currentProfile.ClientLogo - ReportBranding is passed to WriteAsync as the last parameter - Test constructors handle null IBrandingService gracefully (branding = null fallback) - All existing ViewModel and export tests pass without modification - Build succeeds with zero warnings After completion, create `.planning/phases/11-html-export-branding/11-03-SUMMARY.md`