---
phase: 07-user-access-audit
plan: 06
type: execute
wave: 2
depends_on: ["07-01"]
files_modified:
  - SharepointToolbox/Services/Export/UserAccessCsvExportService.cs
  - SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs
autonomous: true
requirements:
  - UACC-02
must_haves:
  truths:
    - "CSV export produces one file per audited user with summary section at top and flat data rows"
    - "CSV filenames include user email and date (e.g. audit_alice@contoso.com_2026-04-07.csv)"
    - "HTML export produces a single self-contained report with collapsible groups, sortable columns, search filter"
    - "HTML report has both group-by-user and group-by-site views togglable via tab/button in header"
    - "HTML report shows per-user summary stats and risk highlights (high-privilege, external users)"
    - "Both exports follow established patterns: UTF-8+BOM for CSV, inline CSS/JS for HTML"
  artifacts:
    - path: "SharepointToolbox/Services/Export/UserAccessCsvExportService.cs"
      provides: "CSV export for user access audit results"
      contains: "class UserAccessCsvExportService"
    - path: "SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs"
      provides: "HTML export for user access audit results"
      contains: "class UserAccessHtmlExportService"
  key_links:
    - from: "SharepointToolbox/Services/Export/UserAccessCsvExportService.cs"
      to: "SharepointToolbox/Core/Models/UserAccessEntry.cs"
      via: "Takes IReadOnlyList<UserAccessEntry> as input"
      pattern: "UserAccessEntry"
    - from: "SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs"
      to: "SharepointToolbox/Core/Models/UserAccessEntry.cs"
      via: "Takes IReadOnlyList<UserAccessEntry> as input"
      pattern: "UserAccessEntry"
---

<objective>
Implement the two export services for User Access Audit: per-user CSV files with summary headers, and a single interactive HTML report with dual-view toggle, collapsible groups, and risk highlighting.

Purpose: Audit results must be exportable for compliance documentation and sharing with stakeholders.
Output: UserAccessCsvExportService.cs, UserAccessHtmlExportService.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

<interfaces>
<!-- From 07-01: Data model for export -->
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);
```

<!-- Existing export patterns to follow -->
From SharepointToolbox/Services/Export/CsvExportService.cs:
```csharp
public class CsvExportService
{
    public string BuildCsv(IReadOnlyList<PermissionEntry> entries) { ... }
    public async Task WriteAsync(IReadOnlyList<PermissionEntry> entries, string filePath, CancellationToken ct)
    {
        var csv = BuildCsv(entries);
        await File.WriteAllTextAsync(filePath, csv, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), ct);
    }
    private static string Csv(string value) { /* RFC 4180 escaping */ }
}
```

From SharepointToolbox/Services/Export/HtmlExportService.cs:
```csharp
public class HtmlExportService
{
    public string BuildHtml(IReadOnlyList<PermissionEntry> entries) { ... }
    public async Task WriteAsync(IReadOnlyList<PermissionEntry> entries, string filePath, CancellationToken ct)
    {
        var html = BuildHtml(entries);
        await File.WriteAllTextAsync(filePath, html, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), ct);
    }
}
// Pattern: stats cards, filter input, table, inline JS for filter, inline CSS, badges, user pills
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Implement UserAccessCsvExportService</name>
  <files>SharepointToolbox/Services/Export/UserAccessCsvExportService.cs</files>
  <action>
    Create `SharepointToolbox/Services/Export/UserAccessCsvExportService.cs`:

    ```csharp
    using System.IO;
    using System.Text;
    using SharepointToolbox.Core.Models;

    namespace SharepointToolbox.Services.Export;

    /// <summary>
    /// Exports user access audit results to CSV format.
    /// Produces one CSV file per audited user with a summary section at the top.
    /// </summary>
    public class UserAccessCsvExportService
    {
        private const string DataHeader =
            "\"Site\",\"Object Type\",\"Object\",\"URL\",\"Permission Level\",\"Access Type\",\"Granted Through\"";

        /// <summary>
        /// Builds a CSV string for a single user's access entries.
        /// Includes a summary section at the top followed by data rows.
        /// </summary>
        public string BuildCsv(string userDisplayName, string userLogin, IReadOnlyList<UserAccessEntry> entries)
        {
            var sb = new StringBuilder();

            // Summary section
            var sitesCount = entries.Select(e => e.SiteUrl).Distinct().Count();
            var highPrivCount = entries.Count(e => e.IsHighPrivilege);

            sb.AppendLine($"\"User Access Audit Report\"");
            sb.AppendLine($"\"User\",\"{Csv(userDisplayName)} ({Csv(userLogin)})\"");
            sb.AppendLine($"\"Total Accesses\",\"{entries.Count}\"");
            sb.AppendLine($"\"Sites\",\"{sitesCount}\"");
            sb.AppendLine($"\"High Privilege\",\"{highPrivCount}\"");
            sb.AppendLine($"\"Generated\",\"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\"");
            sb.AppendLine(); // Blank line separating summary from data

            // Data rows
            sb.AppendLine(DataHeader);
            foreach (var entry in entries)
            {
                sb.AppendLine(string.Join(",", new[]
                {
                    Csv(entry.SiteTitle),
                    Csv(entry.ObjectType),
                    Csv(entry.ObjectTitle),
                    Csv(entry.ObjectUrl),
                    Csv(entry.PermissionLevel),
                    Csv(entry.AccessType.ToString()),
                    Csv(entry.GrantedThrough)
                }));
            }

            return sb.ToString();
        }

        /// <summary>
        /// Writes one CSV file per user to the specified directory.
        /// File names: audit_{email}_{date}.csv
        /// </summary>
        public async Task WriteAsync(
            IReadOnlyList<UserAccessEntry> allEntries,
            string directoryPath,
            CancellationToken ct)
        {
            Directory.CreateDirectory(directoryPath);
            var dateStr = DateTime.Now.ToString("yyyy-MM-dd");

            // Group by user
            var byUser = allEntries.GroupBy(e => e.UserLogin);

            foreach (var group in byUser)
            {
                ct.ThrowIfCancellationRequested();

                var userLogin = group.Key;
                var displayName = group.First().UserDisplayName;
                var entries = group.ToList();

                // Sanitize email for filename (replace @ and other invalid chars)
                var safeLogin = SanitizeFileName(userLogin);
                var fileName = $"audit_{safeLogin}_{dateStr}.csv";
                var filePath = Path.Combine(directoryPath, fileName);

                var csv = BuildCsv(displayName, userLogin, entries);
                await File.WriteAllTextAsync(filePath, csv,
                    new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), ct);
            }
        }

        /// <summary>
        /// Writes all entries to a single CSV file (alternative for single-file export).
        /// Used when the ViewModel export command picks a single file path.
        /// </summary>
        public async Task WriteSingleFileAsync(
            IReadOnlyList<UserAccessEntry> entries,
            string filePath,
            CancellationToken ct)
        {
            var sb = new StringBuilder();
            var fullHeader = "\"User\",\"User Login\"," + DataHeader;

            // Summary
            var users = entries.Select(e => e.UserLogin).Distinct().ToList();
            sb.AppendLine($"\"User Access Audit Report\"");
            sb.AppendLine($"\"Users Audited\",\"{users.Count}\"");
            sb.AppendLine($"\"Total Accesses\",\"{entries.Count}\"");
            sb.AppendLine($"\"Generated\",\"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\"");
            sb.AppendLine();

            sb.AppendLine(fullHeader);
            foreach (var entry in entries)
            {
                sb.AppendLine(string.Join(",", new[]
                {
                    Csv(entry.UserDisplayName),
                    Csv(entry.UserLogin),
                    Csv(entry.SiteTitle),
                    Csv(entry.ObjectType),
                    Csv(entry.ObjectTitle),
                    Csv(entry.ObjectUrl),
                    Csv(entry.PermissionLevel),
                    Csv(entry.AccessType.ToString()),
                    Csv(entry.GrantedThrough)
                }));
            }

            await File.WriteAllTextAsync(filePath, sb.ToString(),
                new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), ct);
        }

        private static string Csv(string value)
        {
            if (string.IsNullOrEmpty(value)) return "\"\"";
            return $"\"{value.Replace("\"", "\"\"")}\"";
        }

        private static string SanitizeFileName(string name)
        {
            var invalid = Path.GetInvalidFileNameChars();
            var sb = new StringBuilder(name.Length);
            foreach (var c in name)
                sb.Append(invalid.Contains(c) ? '_' : c);
            return sb.ToString();
        }
    }
    ```

    Design notes:
    - Two write modes: WriteAsync (per-user files to directory) and WriteSingleFileAsync (all in one file)
    - The ViewModel will use WriteSingleFileAsync for the SaveFileDialog export (simpler UX)
    - WriteAsync with per-user files available for batch export scenarios
    - Summary section at top of each file per CONTEXT.md decision
    - RFC 4180 CSV escaping following existing CsvExportService.Csv() pattern
    - UTF-8 with BOM for Excel compatibility (same as existing exports)
  </action>
  <verify>
    <automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5</automated>
  </verify>
  <done>UserAccessCsvExportService.cs compiles, has BuildCsv for per-user CSV, WriteAsync for per-user files, WriteSingleFileAsync for combined export, RFC 4180 escaping, UTF-8+BOM encoding.</done>
</task>

<task type="auto">
  <name>Task 2: Implement UserAccessHtmlExportService</name>
  <files>SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs</files>
  <action>
    Create `SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs`. Follow the HtmlExportService pattern (self-contained HTML with inline CSS/JS, stats cards, filter, table).

    The HTML report must include:
    1. **Title**: "User Access Audit Report"
    2. **Stats cards** row: Total Accesses, Users Audited, Sites Scanned, High Privilege Count, External Users Count
    3. **Per-user summary section**: For each user, show a card with their name, total accesses, sites count, high-privilege count. Highlight if user has Site Collection Admin access.
    4. **View toggle**: Two buttons "By User" / "By Site" that show/hide the corresponding grouped table (JavaScript toggle, no page reload)
    5. **Filter input**: Text filter that searches across all visible rows
    6. **Table (By User view)**: Grouped by user (collapsible sections). Each group header shows user name + count. Rows: Site, Object Type, Object, Permission Level, Access Type badge, Granted Through
    7. **Table (By Site view)**: Grouped by site (collapsible sections). Each group header shows site title + count. Rows: User, Object Type, Object, Permission Level, Access Type badge, Granted Through
    8. **Access Type badges**: Colored badges — Direct (blue), Group (green), Inherited (gray)
    9. **High-privilege rows**: Warning icon + bold text
    10. **External user badge**: Orange "Guest" pill next to user name
    11. **Inline JS**:
        - `toggleView(view)`: Shows "by-user" or "by-site" div, updates active button state
        - `filterTable()`: Filters visible rows in the active view
        - `toggleGroup(id)`: Collapses/expands a group section
        - `sortTable(col)`: Sorts rows within groups by column

    The HTML should be ~300-400 lines of generated content. Use StringBuilder like the existing HtmlExportService.

    Follow the exact same CSS style as HtmlExportService (same font-family, stat-card styles, table styles, badge styles) with additions for:
    - `.access-direct { background: #dbeafe; color: #1e40af; }` (blue)
    - `.access-group { background: #dcfce7; color: #166534; }` (green)
    - `.access-inherited { background: #f3f4f6; color: #374151; }` (gray)
    - `.high-priv { font-weight: 700; }` + warning icon
    - `.guest-badge { background: #fff7ed; color: #9a3412; border: 1px solid #fed7aa; }` (reuse external-user style)
    - `.view-toggle button.active { background: #1a1a2e; color: #fff; }`
    - `.group-header { cursor: pointer; background: #f0f0f0; padding: 10px; font-weight: 600; }`

    The service should have:
    - `BuildHtml(IReadOnlyList<UserAccessEntry> entries)` — returns full HTML string
    - `WriteAsync(IReadOnlyList<UserAccessEntry> entries, string filePath, CancellationToken ct)` — writes to file (UTF-8 without BOM, same as HtmlExportService)
  </action>
  <verify>
    <automated>cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5</automated>
  </verify>
  <done>UserAccessHtmlExportService.cs compiles, produces self-contained HTML with: stats cards, per-user summary, dual-view toggle (by-user/by-site), collapsible groups, filter input, sortable columns, color-coded access type badges, high-privilege warnings, external user badges, inline CSS/JS.</done>
</task>

</tasks>

<verification>
- `dotnet build SharepointToolbox/SharepointToolbox.csproj` succeeds with 0 errors
- UserAccessCsvExportService has BuildCsv + WriteAsync + WriteSingleFileAsync
- UserAccessHtmlExportService has BuildHtml + WriteAsync
- HTML output contains inline CSS and JS (no external dependencies)
- CSV uses RFC 4180 escaping and UTF-8+BOM
</verification>

<success_criteria>
Both export services compile and follow established patterns. CSV produces per-user files with summary headers. HTML produces an interactive report with dual-view toggle, collapsible groups, color-coded badges, and risk highlighting. Ready for ViewModel export commands in 07-04.
</success_criteria>

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