---
phase: 07-user-access-audit
plan: 04
subsystem: viewmodel
tags: [viewmodel, wpf, people-picker, debounce, collectionview, grouping, filtering, export, mvvm]

requires:
  - phase: 07-01
    provides: [UserAccessEntry, AccessType, IUserAccessAuditService, IGraphUserSearchService, GraphUserResult]
  - phase: 07-02
    provides: [UserAccessAuditService]
  - phase: 07-03
    provides: [GraphUserSearchService]
  - phase: 07-06
    provides: [UserAccessCsvExportService, UserAccessHtmlExportService]
provides:
  - UserAccessAuditViewModel with full orchestration of people picker, site selection, audit execution, grouping, filtering, summary banner, export
affects: [07-05, 07-07, 07-08]

tech-stack:
  added: []
  patterns: [CollectionViewSource grouping toggle, debounced CancellationTokenSource search, FeatureViewModelBase extension, dual-constructor pattern, _hasLocalSiteOverride site override]

key-files:
  created:
    - SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs
  modified: []

key-decisions:
  - "CollectionViewSource is created over Results in constructor; ApplyGrouping() clears and re-adds PropertyGroupDescription on IsGroupByUser toggle (UserLogin or SiteUrl)"
  - "Debounced search uses _searchCts CancellationTokenSource cancelled on each SearchQuery change; Task.Delay(300, ct) pattern with OperationCanceledException swallowed"
  - "OnResultsChanged partial rebuilds grouping/filter when Results collection reference is replaced after RunOperationAsync"
  - "ExportCsvAsync calls WriteSingleFileAsync (combined single-file export) rather than WriteAsync (per-user directory) to match SaveFileDialog single-path UX"

patterns-established:
  - "UserAccessAuditViewModel: same _hasLocalSiteOverride + OnGlobalSitesChanged guard as PermissionsViewModel"
  - "Dual constructor: full DI constructor + internal test constructor omitting export services — both initialize all commands and wire collection events"
  - "Summary properties (TotalAccessCount, SitesCount, HighPrivilegeCount) are computed getters calling Results LINQ — NotifySummaryProperties() triggers all three"

requirements-completed: [UACC-01, UACC-02]

duration: 2min
completed: 2026-04-07
---

# Phase 7 Plan 04: UserAccessAuditViewModel Summary

**UserAccessAuditViewModel wires people-picker (300ms debounced Graph search), multi-site selection with override guard, IUserAccessAuditService.AuditUsersAsync execution, CollectionViewSource group-by-user/site toggle with real-time filter, computed summary banner (TotalAccessCount, SitesCount, HighPrivilegeCount), and CSV/HTML export commands — zero-error build.**

## Performance

- **Duration:** ~2 min
- **Started:** 2026-04-07T10:42:51Z
- **Completed:** 2026-04-07T10:44:56Z
- **Tasks:** 1
- **Files modified:** 1

## Accomplishments

- UserAccessAuditViewModel.cs (~300 lines) extends FeatureViewModelBase and implements all 10 observable properties, 5 commands, CollectionViewSource grouping/filtering, and dual constructors
- Debounced people-picker: _searchCts cancelled/recreated on SearchQuery change, 300ms Task.Delay, IsSearching spinner, 2-char minimum guard consistent with GraphUserSearchService
- CollectionViewSource grouping: ApplyGrouping() swaps PropertyGroupDescription between UserLogin and SiteUrl; FilterPredicate applies to 6 fields case-insensitively
- Summary banner computed properties (TotalAccessCount, SitesCount, HighPrivilegeCount) notified via NotifySummaryProperties() after each RunOperationAsync and tenant switch

## Task Commits

1. **Task 1: Implement UserAccessAuditViewModel** - `3de737a` (feat)

**Plan metadata:** (docs commit pending)

## Files Created/Modified

- `SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs` — Full orchestration ViewModel for User Access Audit tab

## Decisions Made

1. **CollectionViewSource bound at construction** — ResultsView is created from a `new CollectionViewSource { Source = Results }` in the constructor. When Results is replaced by a new collection in RunOperationAsync, OnResultsChanged re-applies grouping and filter. This avoids ICollectionView rebinding complexity in XAML.

2. **WriteSingleFileAsync for CSV export** — UserAccessCsvExportService has two modes: WriteAsync (per-user files to directory) and WriteSingleFileAsync (combined). The ViewModel uses WriteSingleFileAsync since the SaveFileDialog returns a single file path — the per-directory mode is for batch export scenarios.

3. **SelectedUsers UPNs as login keys** — AuditUsersAsync receives `SelectedUsers.Select(u => u.UserPrincipalName)` as the targetUserLogins parameter, matching the UPN-based bidirectional matching in UserAccessAuditService.

## Deviations from Plan

None — plan executed exactly as written.

## Issues Encountered

None.

## User Setup Required

None - no external service configuration required.

## Next Phase Readiness

- UserAccessAuditViewModel ready for XAML binding in 07-05 (View)
- All observable properties, commands, and ResultsView ICollectionView available for DataGrid/ComboBox/AutoComplete binding
- Export commands wired to UserAccessCsvExportService.WriteSingleFileAsync and UserAccessHtmlExportService.WriteAsync

---
*Phase: 07-user-access-audit*
*Completed: 2026-04-07*
