diff --git a/SharepointToolbox/Services/Export/UserAccessCsvExportService.cs b/SharepointToolbox/Services/Export/UserAccessCsvExportService.cs
index 6a4577d..7cc8c35 100644
--- a/SharepointToolbox/Services/Export/UserAccessCsvExportService.cs
+++ b/SharepointToolbox/Services/Export/UserAccessCsvExportService.cs
@@ -1,5 +1,6 @@
using System.IO;
using System.Text;
+using SharepointToolbox.Core.Helpers;
using SharepointToolbox.Core.Models;
namespace SharepointToolbox.Services.Export;
@@ -89,12 +90,51 @@ public class UserAccessCsvExportService
///
/// Writes all entries to a single CSV file (alternative for single-file export).
/// Used when the ViewModel export command picks a single file path.
+ /// When is true, entries are consolidated using
+ /// and written in a compact multi-location format.
///
public async Task WriteSingleFileAsync(
IReadOnlyList entries,
string filePath,
- CancellationToken ct)
+ CancellationToken ct,
+ bool mergePermissions = false)
{
+ if (mergePermissions)
+ {
+ var consolidated = PermissionConsolidator.Consolidate(entries);
+ var sb = new StringBuilder();
+
+ // Summary section
+ sb.AppendLine("\"User Access Audit Report (Consolidated)\"");
+ sb.AppendLine($"\"Users Audited\",\"{consolidated.Select(e => e.UserLogin).Distinct().Count()}\"");
+ sb.AppendLine($"\"Total Entries\",\"{consolidated.Count}\"");
+ sb.AppendLine($"\"Generated\",\"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\"");
+ sb.AppendLine();
+
+ // Header
+ sb.AppendLine("\"User\",\"User Login\",\"Permission Level\",\"Access Type\",\"Granted Through\",\"Locations\",\"Location Count\"");
+
+ // Data rows
+ foreach (var entry in consolidated)
+ {
+ var locations = string.Join("; ", entry.Locations.Select(l => l.SiteTitle));
+ sb.AppendLine(string.Join(",", new[]
+ {
+ $"\"{entry.UserDisplayName}\"",
+ $"\"{entry.UserLogin}\"",
+ $"\"{entry.PermissionLevel}\"",
+ $"\"{entry.AccessType}\"",
+ $"\"{entry.GrantedThrough}\"",
+ $"\"{locations}\"",
+ $"\"{entry.LocationCount}\""
+ }));
+ }
+
+ await File.WriteAllTextAsync(filePath, sb.ToString(), new UTF8Encoding(false), ct);
+ return;
+ }
+
+ {
var sb = new StringBuilder();
var fullHeader = "\"User\",\"User Login\"," + DataHeader;
@@ -125,6 +165,7 @@ public class UserAccessCsvExportService
await File.WriteAllTextAsync(filePath, sb.ToString(),
new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), ct);
+ }
}
/// RFC 4180 CSV field escaping: wrap in double quotes, double internal quotes.
diff --git a/SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs b/SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs
index 14b24a8..ecac2a1 100644
--- a/SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs
+++ b/SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs
@@ -496,7 +496,7 @@ public partial class UserAccessAuditViewModel : FeatureViewModelBase
if (dialog.ShowDialog() != true) return;
try
{
- await _csvExportService.WriteSingleFileAsync(Results, dialog.FileName, CancellationToken.None);
+ await _csvExportService.WriteSingleFileAsync(Results, dialog.FileName, CancellationToken.None, MergePermissions);
OpenFile(dialog.FileName);
}
catch (Exception ex)