---
phase: 07-user-access-audit
plan: 04
type: execute
wave: 3
depends_on: ["07-01", "07-02", "07-03"]
files_modified:
  - SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs
autonomous: true
requirements:
  - UACC-01
  - UACC-02
must_haves:
  truths:
    - "ViewModel extends FeatureViewModelBase with RunOperationAsync that calls IUserAccessAuditService.AuditUsersAsync"
    - "People picker search is debounced (300ms) and calls IGraphUserSearchService.SearchUsersAsync"
    - "Selected users are stored in an ObservableCollection<GraphUserResult>"
    - "Results are ObservableCollection<UserAccessEntry> with CollectionViewSource for grouping toggle"
    - "ExportCsvCommand and ExportHtmlCommand follow PermissionsViewModel pattern"
    - "Site selection follows _hasLocalSiteOverride + OnGlobalSitesChanged pattern from PermissionsViewModel"
    - "Per-user summary banner properties (TotalAccesses, SitesCount, HighPrivilegeCount) are computed from results"
    - "FilterText property filters the CollectionView in real-time"
  artifacts:
    - path: "SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs"
      provides: "Tab ViewModel for User Access Audit"
      contains: "class UserAccessAuditViewModel"
  key_links:
    - from: "SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs"
      to: "SharepointToolbox/Services/IUserAccessAuditService.cs"
      via: "Constructor injection, AuditUsersAsync call in RunOperationAsync"
      pattern: "AuditUsersAsync"
    - from: "SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs"
      to: "SharepointToolbox/Services/IGraphUserSearchService.cs"
      via: "Constructor injection, SearchUsersAsync call in debounced search"
      pattern: "SearchUsersAsync"
    - from: "SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs"
      to: "SharepointToolbox/ViewModels/FeatureViewModelBase.cs"
      via: "Extends base class, overrides RunOperationAsync and OnGlobalSitesChanged"
      pattern: "FeatureViewModelBase"
---

<objective>
Implement UserAccessAuditViewModel — the tab ViewModel that orchestrates people picker search, site selection, audit execution, result grouping/filtering, summary banner, and export commands.

Purpose: Central coordinator between UI and services. This is the largest single file in the phase, connecting all pieces.
Output: UserAccessAuditViewModel.cs
</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/07-user-access-audit/07-CONTEXT.md
@.planning/phases/07-user-access-audit/07-01-SUMMARY.md
@.planning/phases/07-user-access-audit/07-02-SUMMARY.md
@.planning/phases/07-user-access-audit/07-03-SUMMARY.md

<interfaces>
<!-- From 07-01: Models -->
From SharepointToolbox/Core/Models/UserAccessEntry.cs:
```csharp
public enum AccessType { Direct, Group, Inherited }

public record UserAccessEntry(
    string UserDisplayName, string UserLogin,
    string SiteUrl, string SiteTitle,
    string ObjectType, string ObjectTitle, string ObjectUrl,
    string PermissionLevel,
    AccessType AccessType, string GrantedThrough,
    bool IsHighPrivilege, bool IsExternalUser);
```

From SharepointToolbox/Services/IGraphUserSearchService.cs:
```csharp
public record GraphUserResult(string DisplayName, string UserPrincipalName, string? Mail);

public interface IGraphUserSearchService
{
    Task<IReadOnlyList<GraphUserResult>> SearchUsersAsync(
        string clientId, string query, int maxResults = 10, CancellationToken ct = default);
}
```

From SharepointToolbox/Services/IUserAccessAuditService.cs:
```csharp
public interface IUserAccessAuditService
{
    Task<IReadOnlyList<UserAccessEntry>> AuditUsersAsync(
        ISessionManager sessionManager,
        IReadOnlyList<string> targetUserLogins,
        IReadOnlyList<SiteInfo> sites,
        ScanOptions options,
        IProgress<OperationProgress> progress,
        CancellationToken ct);
}
```

<!-- Base class pattern (from FeatureViewModelBase.cs) -->
```csharp
public abstract partial class FeatureViewModelBase : ObservableRecipient
{
    protected IReadOnlyList<SiteInfo> GlobalSites { get; private set; }
    protected abstract Task RunOperationAsync(CancellationToken ct, IProgress<OperationProgress> progress);
    protected virtual void OnTenantSwitched(TenantProfile profile) { }
    protected virtual void OnGlobalSitesChanged(IReadOnlyList<SiteInfo> sites) { }
    // RunCommand, CancelCommand, IsRunning, StatusMessage, ProgressValue auto-provided
}
```

<!-- PermissionsViewModel pattern for site picker + export (reference) -->
From SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs:
```csharp
public ObservableCollection<SiteInfo> SelectedSites { get; } = new();
private bool _hasLocalSiteOverride;
public Func<Window>? OpenSitePickerDialog { get; set; }
internal TenantProfile? _currentProfile;
public IAsyncRelayCommand ExportCsvCommand { get; }
public IAsyncRelayCommand ExportHtmlCommand { get; }
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Implement UserAccessAuditViewModel</name>
  <files>SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs</files>
  <action>
    Create `SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs`. This is a substantial file (~350 lines). Follow the PermissionsViewModel pattern exactly for site selection, tenant switching, export commands, and dialog factories.

    Structure:
    1. **Fields**: inject IUserAccessAuditService, IGraphUserSearchService, ISessionManager, export services, logger
    2. **Observable properties**:
       - `SearchQuery` (string) — people picker text input, triggers debounced search on change
       - `SearchResults` (ObservableCollection<GraphUserResult>) — autocomplete dropdown items
       - `SelectedUsers` (ObservableCollection<GraphUserResult>) — users added for audit
       - `Results` (ObservableCollection<UserAccessEntry>) — audit output
       - `FilterText` (string) — real-time filter on results grid
       - `IsGroupByUser` (bool, default true) — toggle between group-by-user and group-by-site
       - `IncludeInherited` (bool) — scan option
       - `ScanFolders` (bool, default true) — scan option
       - `IncludeSubsites` (bool) — scan option
       - `IsSearching` (bool) — shows spinner during Graph search
    3. **Summary properties** (computed, not stored):
       - `TotalAccessCount` => Results.Count
       - `SitesCount` => Results.Select(r => r.SiteUrl).Distinct().Count()
       - `HighPrivilegeCount` => Results.Count(r => r.IsHighPrivilege)
       - `SelectedUsersLabel` => e.g. "2 user(s) selected"
    4. **Commands**:
       - `ExportCsvCommand` (AsyncRelayCommand, CanExport)
       - `ExportHtmlCommand` (AsyncRelayCommand, CanExport)
       - `OpenSitePickerCommand` (RelayCommand)
       - `AddUserCommand` (RelayCommand<GraphUserResult>) — adds to SelectedUsers
       - `RemoveUserCommand` (RelayCommand<GraphUserResult>) — removes from SelectedUsers
    5. **Site picker**: SelectedSites, _hasLocalSiteOverride, OpenSitePickerDialog factory, SitesSelectedLabel — identical pattern to PermissionsViewModel
    6. **People picker debounce**: Use a CancellationTokenSource that is cancelled/recreated each time SearchQuery changes. Delay 300ms before calling SearchUsersAsync. Set IsSearching during search.
    7. **RunOperationAsync**: Build ScanOptions, call AuditUsersAsync with SelectedUsers UPNs + effective sites, update Results on UI thread, notify summary properties and export CanExecute.
    8. **CollectionViewSource**: Create a ResultsView (ICollectionView) backed by Results. When IsGroupByUser changes, update GroupDescriptions (group by UserLogin or SiteUrl). When FilterText changes, apply filter predicate.
    9. **Constructors**: Full DI constructor + internal test constructor (omit export services) — same dual-constructor pattern as PermissionsViewModel.
    10. **Tenant switching**: Reset all state (results, selected users, search, sites) in OnTenantSwitched.

    Important implementation details:
    - The debounced search should use `Task.Delay(300, ct)` pattern with a field `_searchCts` that gets cancelled on each new keystroke
    - partial void OnSearchQueryChanged(string value) triggers the debounced search
    - partial void OnFilterTextChanged(string value) triggers ResultsView.Refresh()
    - partial void OnIsGroupByUserChanged(bool value) triggers re-grouping of ResultsView
    - Export CSV/HTML: use SaveFileDialog pattern from PermissionsViewModel, calling the audit-specific export services (UserAccessCsvExportService, UserAccessHtmlExportService) that will be created in plan 07-06
    - Export services are typed as object references (UserAccessCsvExportService? and UserAccessHtmlExportService?) since they haven't been created yet — the plan 07-06 export service files will be the concrete types
    - For the test constructor, pass null for export services

    The ViewModel needs these `using` statements:
    ```
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Diagnostics;
    using System.Windows;
    using System.Windows.Data;
    using CommunityToolkit.Mvvm.ComponentModel;
    using CommunityToolkit.Mvvm.Input;
    using Microsoft.Extensions.Logging;
    using Microsoft.Win32;
    using SharepointToolbox.Core.Models;
    using SharepointToolbox.Services;
    using SharepointToolbox.Services.Export;
    ```
  </action>
  <verify>
    <automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5</automated>
  </verify>
  <done>UserAccessAuditViewModel.cs compiles and extends FeatureViewModelBase. It has: people picker with debounced Graph search, site selection with override pattern, RunOperationAsync calling AuditUsersAsync, Results with CollectionViewSource grouping and filtering, summary properties, dual constructors, export commands, tenant switching reset.</done>
</task>

</tasks>

<verification>
- `dotnet build SharepointToolbox/SharepointToolbox.csproj` succeeds with 0 errors
- UserAccessAuditViewModel extends FeatureViewModelBase
- Has ObservableProperty for SearchQuery, SelectedUsers, Results, FilterText, IsGroupByUser
- Has ExportCsvCommand, ExportHtmlCommand, OpenSitePickerCommand, AddUserCommand, RemoveUserCommand
- RunOperationAsync calls IUserAccessAuditService.AuditUsersAsync
- OnSearchQueryChanged triggers debounced IGraphUserSearchService.SearchUsersAsync
- ResultsView ICollectionView supports group-by toggle and text filter
</verification>

<success_criteria>
The ViewModel is the orchestration hub for the audit tab. All UI interactions (search users, select sites, run audit, filter results, toggle grouping, export) are wired to service calls and observable state. Ready for View binding in 07-05 and export service implementation in 07-06.
</success_criteria>

<output>
After completion, create `.planning/phases/07-user-access-audit/07-04-SUMMARY.md`
</output>
