Files
Sharepoint-Toolbox/.planning/phases/11-html-export-branding/11-03-PLAN.md
2026-04-08 14:23:01 +02:00

9.2 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
11-html-export-branding 03 execute 3
11-02
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
SharepointToolbox/App.xaml.cs
true
BRAND-05
truths artifacts key_links
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
path provides contains
SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs Permissions export with branding assembly IBrandingService
path provides contains
SharepointToolbox/ViewModels/Tabs/SearchViewModel.cs Search export with branding assembly IBrandingService
path provides contains
SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs Storage export with branding assembly IBrandingService
path provides contains
SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs Duplicates export with branding assembly IBrandingService
path provides contains
SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs User access export with branding assembly IBrandingService
from to via pattern
SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs SharepointToolbox/Services/IBrandingService.cs constructor injection IBrandingService _brandingService
from to via pattern
SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs SharepointToolbox/Services/Export/HtmlExportService.cs WriteAsync with branding 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.

<execution_context> @C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/dev/.claude/get-shit-done/templates/summary.md </execution_context>

@.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:

public interface IBrandingService
{
    Task<LogoData> ImportLogoAsync(string filePath);
    Task SaveMspLogoAsync(LogoData logo);
    Task ClearMspLogoAsync();
    Task<LogoData?> GetMspLogoAsync();
}

DuplicatesViewModel constructor pattern:

public DuplicatesViewModel(
    IDuplicatesService duplicatesService,
    ISessionManager sessionManager,
    DuplicatesHtmlExportService htmlExportService,
    ILogger<FeatureViewModelBase> 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. The other 3 ViewModels (Search, Storage, Duplicates) have a single constructor each.

DI registrations in App.xaml.cs:

services.AddSingleton<IBrandingService, BrandingService>();

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): 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.

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.

<success_criteria>

  • 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 </success_criteria>
After completion, create `.planning/phases/11-html-export-branding/11-03-SUMMARY.md`