Files
Sharepoint-Toolbox/.planning/phases/11-html-export-branding/11-03-PLAN.md
Dev df6f4949a8 docs(13-02): complete User Directory ViewModel plan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 16:44:56 +02:00

221 lines
9.5 KiB
Markdown

---
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"
---
<objective>
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.
</objective>
<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>
<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
<interfaces>
<!-- From Plan 11-01 -->
From SharepointToolbox/Core/Models/ReportBranding.cs:
```csharp
public record ReportBranding(LogoData? MspLogo, LogoData? ClientLogo);
```
<!-- From Phase 10 -->
From SharepointToolbox/Services/IBrandingService.cs:
```csharp
public interface IBrandingService
{
Task<LogoData> ImportLogoAsync(string filePath);
Task SaveMspLogoAsync(LogoData logo);
Task ClearMspLogoAsync();
Task<LogoData?> GetMspLogoAsync();
}
```
<!-- Current ViewModel patterns (all 5 follow this same shape) -->
DuplicatesViewModel constructor pattern:
```csharp
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.
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, 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.
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Inject IBrandingService into all 5 export ViewModels and assemble branding in ExportHtmlAsync</name>
<files>
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
</files>
<action>
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);
}
```
</action>
<verify>
<automated>dotnet build --no-restore -warnaserror && dotnet test SharepointToolbox.Tests --no-build -q</automated>
</verify>
<done>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.</done>
</task>
</tasks>
<verification>
```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.
</verification>
<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>
<output>
After completion, create `.planning/phases/11-html-export-branding/11-03-SUMMARY.md`
</output>