---
phase: 17-group-expansion-html-reports
plan: 02
type: execute
wave: 2
depends_on: ["17-01"]
files_modified:
  - SharepointToolbox/Services/Export/HtmlExportService.cs
  - SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs
  - SharepointToolbox.Tests/Services/Export/HtmlExportServiceTests.cs
autonomous: true
requirements: [RPT-01, RPT-02]

must_haves:
  truths:
    - "SharePoint group pills in the HTML report are clickable and expand to show group members"
    - "When group members are resolved, clicking the pill reveals member names inline"
    - "When group resolution fails, the pill expands to show 'members unavailable' label"
    - "When no groupMembers dict is passed, HTML output is identical to pre-Phase 17 output"
    - "toggleGroup() JS function exists in HtmlExportService inline JS"
    - "PermissionsViewModel calls ISharePointGroupResolver before HTML export and passes results to BuildHtml"
  artifacts:
    - path: "SharepointToolbox/Services/Export/HtmlExportService.cs"
      provides: "Expandable group pill rendering + toggleGroup JS"
      contains: "toggleGroup"
    - path: "SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs"
      provides: "Group resolution orchestration before export"
      contains: "_groupResolver"
    - path: "SharepointToolbox.Tests/Services/Export/HtmlExportServiceTests.cs"
      provides: "Tests for group pill expansion and backward compatibility"
      contains: "grpmem"
  key_links:
    - from: "SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs"
      to: "ISharePointGroupResolver"
      via: "constructor injection + call in ExportHtmlAsync"
      pattern: "_groupResolver.ResolveGroupsAsync"
    - from: "SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs"
      to: "HtmlExportService.BuildHtml"
      via: "passing groupMembers dict"
      pattern: "groupMembers"
    - from: "SharepointToolbox/Services/Export/HtmlExportService.cs"
      to: "toggleGroup JS"
      via: "inline script block"
      pattern: "function toggleGroup"
---

<objective>
Wire group member expansion into HtmlExportService rendering and PermissionsViewModel export flow.

Purpose: This is the user-visible feature — SharePoint group pills become clickable in HTML reports, expanding to show resolved members or a "members unavailable" fallback.
Output: Modified HtmlExportService (both overloads), modified PermissionsViewModel, new HTML export tests.
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/17-group-expansion-html-reports/17-RESEARCH.md
@.planning/phases/17-group-expansion-html-reports/17-01-SUMMARY.md

<interfaces>
<!-- From Plan 01 outputs -->

From SharepointToolbox/Core/Models/ResolvedMember.cs:
```csharp
public record ResolvedMember(string DisplayName, string Login);
```

From SharepointToolbox/Services/ISharePointGroupResolver.cs:
```csharp
public interface ISharePointGroupResolver
{
    Task<IReadOnlyDictionary<string, IReadOnlyList<ResolvedMember>>> ResolveGroupsAsync(
        ClientContext ctx, string clientId,
        IReadOnlyList<string> groupNames, CancellationToken ct);
}
```

<!-- Existing signatures that will be modified -->

From SharepointToolbox/Services/Export/HtmlExportService.cs:
```csharp
public class HtmlExportService
{
    // Overload 1 — standard PermissionEntry
    public string BuildHtml(IReadOnlyList<PermissionEntry> entries, ReportBranding? branding = null)
    public async Task WriteAsync(IReadOnlyList<PermissionEntry> entries, string filePath, CancellationToken ct, ReportBranding? branding = null)

    // Overload 2 — simplified
    public string BuildHtml(IReadOnlyList<SimplifiedPermissionEntry> entries, ReportBranding? branding = null)
    public async Task WriteAsync(IReadOnlyList<SimplifiedPermissionEntry> entries, string filePath, CancellationToken ct, ReportBranding? branding = null)
}
```

From SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs:
```csharp
public partial class PermissionsViewModel : FeatureViewModelBase
{
    private readonly HtmlExportService? _htmlExportService;
    private readonly ISessionManager _sessionManager;
    private readonly IBrandingService? _brandingService;
    private TenantProfile? _currentProfile;

    public PermissionsViewModel(
        IPermissionsService permissionsService,
        ISiteListService siteListService,
        ISessionManager sessionManager,
        CsvExportService csvExportService,
        HtmlExportService htmlExportService,
        IBrandingService brandingService,
        ILogger<FeatureViewModelBase> logger)

    // Test constructor (no export services)
    internal PermissionsViewModel(
        IPermissionsService permissionsService,
        ISiteListService siteListService,
        ISessionManager sessionManager,
        ILogger<FeatureViewModelBase> logger,
        IBrandingService? brandingService = null)

    private async Task ExportHtmlAsync()
    {
        // Currently calls: _htmlExportService.WriteAsync(entries, path, ct, branding)
    }
}
```

From SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs (toggleGroup JS to copy):
```javascript
function toggleGroup(id) {
  var rows = document.querySelectorAll('tr[data-group="' + id + '"]');
  rows.forEach(function(r) { r.style.display = r.style.display === 'none' ? '' : 'none'; });
}
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Extend HtmlExportService with groupMembers parameter and expandable group pills</name>
  <files>
    SharepointToolbox/Services/Export/HtmlExportService.cs,
    SharepointToolbox.Tests/Services/Export/HtmlExportServiceTests.cs
  </files>
  <behavior>
    - RPT-01-a: BuildHtml with no groupMembers (null/omitted) produces output identical to pre-Phase 17
    - RPT-01-b: BuildHtml with groupMembers containing a group name renders clickable pill with onclick="toggleGroup('grpmem0')" and class "group-expandable"
    - RPT-01-c: BuildHtml with resolved members renders hidden sub-row (data-group="grpmem0", display:none) containing member display names
    - RPT-01-d: BuildHtml with empty member list (resolution failed) renders sub-row with "members unavailable" italic label
    - RPT-01-e: BuildHtml output contains "function toggleGroup" in inline JS
    - RPT-01-f: Simplified BuildHtml overload also accepts groupMembers and renders expandable pills identically
  </behavior>
  <action>
    1. Add `groupMembers` optional parameter to BOTH `BuildHtml` overloads and BOTH `WriteAsync` methods:
       ```csharp
       public string BuildHtml(IReadOnlyList<PermissionEntry> entries,
           ReportBranding? branding = null,
           IReadOnlyDictionary<string, IReadOnlyList<ResolvedMember>>? groupMembers = null)

       public async Task WriteAsync(IReadOnlyList<PermissionEntry> entries, string filePath,
           CancellationToken ct, ReportBranding? branding = null,
           IReadOnlyDictionary<string, IReadOnlyList<ResolvedMember>>? groupMembers = null)
       ```
       Same for the `SimplifiedPermissionEntry` overloads. `WriteAsync` passes `groupMembers` through to `BuildHtml`.

    2. Add CSS for `.group-expandable` in the inline `<style>` block (both overloads):
       ```css
       .group-expandable { cursor: pointer; }
       .group-expandable:hover { opacity: 0.8; }
       ```

    3. Modify the user-pill rendering loop in BOTH `BuildHtml` overloads. The logic:
       - Track a `int grpMemIdx = 0` counter and a `StringBuilder memberSubRows` outside the foreach loop
       - For each pill: check if `entry.PrincipalType == "SharePointGroup"` AND `groupMembers != null` AND `groupMembers.TryGetValue(name, out var members)`:
         - YES with `members.Count > 0`: render `<span class="user-pill group-expandable" onclick="toggleGroup('grpmem{idx}')">Name &#9660;</span>` + append hidden member sub-row to `memberSubRows`
         - YES with `members.Count == 0`: render same expandable pill + append sub-row with `<em style="color:#888">members unavailable</em>`
         - NO (not in dict or groupMembers is null): render existing plain pill (no change)
       - After `</tr>`, append `memberSubRows` content and clear it
       - Member sub-row format: `<tr data-group="grpmem{idx}" style="display:none"><td colspan="7" style="padding-left:2em;font-size:.8rem;color:#555">Alice &lt;alice@co.com&gt; &bull; Bob &lt;bob@co.com&gt;</td></tr>`

    4. Add `toggleGroup()` JS function to the inline `<script>` block (both overloads), right after `filterTable()`:
       ```javascript
       function toggleGroup(id) {
         var rows = document.querySelectorAll('tr[data-group="' + id + '"]');
         rows.forEach(function(r) { r.style.display = r.style.display === 'none' ? '' : 'none'; });
       }
       ```
       NOTE: Also update the `filterTable()` function to skip hidden member sub-rows (rows with `data-group` attribute) so they are not shown/hidden by the text filter. Add a guard: `if (row.hasAttribute('data-group')) return;` at the start of the forEach callback. Use `rows.forEach(function(row) { if (row.hasAttribute('data-group')) return; ... })`.

    5. Write tests in `HtmlExportServiceTests.cs`:
       - `BuildHtml_NoGroupMembers_IdenticalToDefault`: call BuildHtml(entries) and BuildHtml(entries, null, null) — output must match
       - `BuildHtml_WithGroupMembers_RendersExpandablePill`: create a PermissionEntry with PrincipalType="SharePointGroup" and Users="Site Members", pass groupMembers dict with "Site Members" -> [ResolvedMember("Alice", "alice@co.com")], assert output contains `onclick="toggleGroup('grpmem0')"` and `class="user-pill group-expandable"`
       - `BuildHtml_WithGroupMembers_RendersHiddenMemberSubRow`: same setup, assert output contains `data-group="grpmem0"` and `display:none` and `Alice` and `alice@co.com`
       - `BuildHtml_WithEmptyMemberList_RendersMembersUnavailable`: pass groupMembers with "Site Members" -> empty list, assert output contains `members unavailable`
       - `BuildHtml_ContainsToggleGroupJs`: assert output contains `function toggleGroup`
       - `BuildHtml_Simplified_WithGroupMembers_RendersExpandablePill`: same test for the simplified overload
  </action>
  <verify>
    <automated>dotnet test --filter "FullyQualifiedName~HtmlExportServiceTests" --no-build</automated>
  </verify>
  <done>Both BuildHtml overloads render expandable group pills with toggleGroup JS, backward compatibility preserved when groupMembers is null, 6+ new tests pass</done>
</task>

<task type="auto">
  <name>Task 2: Wire PermissionsViewModel to call ISharePointGroupResolver before HTML export</name>
  <files>SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs</files>
  <action>
    1. Add `ISharePointGroupResolver?` field and inject it via constructor:
       - Add `private readonly ISharePointGroupResolver? _groupResolver;` field
       - Main constructor: add `ISharePointGroupResolver? groupResolver = null` as the LAST parameter (optional so existing DI still works if resolver not registered yet — but it will be registered from Plan 01)
       - Assign `_groupResolver = groupResolver;`
       - Test constructor: no change needed (resolver is null by default = no group expansion in tests)

    2. Modify `ExportHtmlAsync()` to resolve groups before export:
       ```csharp
       // After branding resolution, before WriteAsync calls:
       IReadOnlyDictionary<string, IReadOnlyList<ResolvedMember>>? groupMembers = null;
       if (_groupResolver != null && Results.Count > 0)
       {
           var groupNames = Results
               .Where(r => r.PrincipalType == "SharePointGroup")
               .SelectMany(r => r.Users.Split(';', StringSplitOptions.RemoveEmptyEntries))
               .Select(n => n.Trim())
               .Where(n => n.Length > 0)
               .Distinct(StringComparer.OrdinalIgnoreCase)
               .ToList();

           if (groupNames.Count > 0 && _currentProfile != null)
           {
               try
               {
                   var ctx = await _sessionManager.GetOrCreateContextAsync(
                       _currentProfile, CancellationToken.None);
                   groupMembers = await _groupResolver.ResolveGroupsAsync(
                       ctx, _currentProfile.ClientId, groupNames, CancellationToken.None);
               }
               catch (Exception ex)
               {
                   _logger.LogWarning(ex, "Group resolution failed — exporting without member expansion.");
               }
           }
       }
       ```

    3. Update both WriteAsync call sites to pass `groupMembers`:
       ```csharp
       if (IsSimplifiedMode && SimplifiedResults.Count > 0)
           await _htmlExportService.WriteAsync(SimplifiedResults.ToList(), dialog.FileName, CancellationToken.None, branding, groupMembers);
       else
           await _htmlExportService.WriteAsync(Results, dialog.FileName, CancellationToken.None, branding, groupMembers);
       ```

    4. Add `using SharepointToolbox.Core.Models;` if not already present (for `ResolvedMember`).
  </action>
  <verify>
    <automated>dotnet build --no-restore 2>&1 | tail -5</automated>
  </verify>
  <done>PermissionsViewModel collects group names from Results, calls ISharePointGroupResolver, passes resolved dict to HtmlExportService.WriteAsync for both standard and simplified paths, gracefully handles resolution failure</done>
</task>

</tasks>

<verification>
- `dotnet build` — 0 errors, 0 warnings
- `dotnet test --filter "FullyQualifiedName~HtmlExportServiceTests"` — all tests pass (including 6+ new group expansion tests)
- `dotnet test` — full suite green (no regressions)
- HTML output with groupMembers=null is identical to pre-Phase 17 output
- HTML output with groupMembers contains clickable group pills and hidden member sub-rows
</verification>

<success_criteria>
- HtmlExportService both overloads accept optional groupMembers parameter
- Group pills render as expandable with toggleGroup JS when groupMembers is provided
- Empty member lists show "members unavailable" label
- Null/missing groupMembers preserves exact pre-Phase 17 output
- PermissionsViewModel resolves groups before export and passes dict to service
- All new tests green, full suite green
</success_criteria>

<output>
After completion, create `.planning/phases/17-group-expansion-html-reports/17-02-SUMMARY.md`
</output>
