Files
Dev fcd5d1d938 feat(04-09): create BulkMembers, BulkSites, and FolderStructure ViewModels and Views
- BulkMembersViewModel: CSV import, validate, preview, confirm, execute, retry failed, export failed
- BulkSitesViewModel: same flow using IBulkSiteService.CreateSitesAsync
- FolderStructureViewModel: site URL + library inputs, CSV folders, FolderStructureService.BuildUniquePaths
- BulkMembersView/BulkSitesView/FolderStructureView: XAML + code-behind wiring ConfirmBulkOperationDialog
- [Rule 3] Fixed duplicate converter definitions: removed untracked standalone EnumBoolConverter/StringToVisibilityConverter/ListToStringConverter files (already defined in IndentConverter.cs)
2026-04-03 10:20:23 +02:00

167 lines
6.6 KiB
C#

using System.Collections.ObjectModel;
using System.IO;
using System.Reflection;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using Microsoft.Win32;
using Serilog;
using SharepointToolbox.Core.Models;
using SharepointToolbox.Localization;
using SharepointToolbox.Services;
using SharepointToolbox.Services.Export;
namespace SharepointToolbox.ViewModels.Tabs;
public partial class BulkMembersViewModel : FeatureViewModelBase
{
private readonly IBulkMemberService _memberService;
private readonly ICsvValidationService _csvService;
private readonly ISessionManager _sessionManager;
private readonly BulkResultCsvExportService _exportService;
private readonly ILogger<FeatureViewModelBase> _logger;
private TenantProfile? _currentProfile;
private List<BulkMemberRow>? _validRows;
private List<BulkMemberRow>? _failedRowsForRetry;
private BulkOperationSummary<BulkMemberRow>? _lastResult;
[ObservableProperty] private string _previewSummary = string.Empty;
[ObservableProperty] private string _resultSummary = string.Empty;
[ObservableProperty] private bool _hasFailures;
[ObservableProperty] private bool _hasPreview;
private ObservableCollection<CsvValidationRow<BulkMemberRow>> _previewRows = new();
public ObservableCollection<CsvValidationRow<BulkMemberRow>> PreviewRows
{
get => _previewRows;
private set { _previewRows = value; OnPropertyChanged(); }
}
public IRelayCommand ImportCsvCommand { get; }
public IRelayCommand LoadExampleCommand { get; }
public IAsyncRelayCommand ExportFailedCommand { get; }
public IAsyncRelayCommand RetryFailedCommand { get; }
public Func<string, bool>? ShowConfirmDialog { get; set; }
public TenantProfile? CurrentProfile => _currentProfile;
public BulkMembersViewModel(
IBulkMemberService memberService,
ICsvValidationService csvService,
ISessionManager sessionManager,
BulkResultCsvExportService exportService,
ILogger<FeatureViewModelBase> logger)
: base(logger)
{
_memberService = memberService;
_csvService = csvService;
_sessionManager = sessionManager;
_exportService = exportService;
_logger = logger;
ImportCsvCommand = new RelayCommand(ImportCsv);
LoadExampleCommand = new RelayCommand(LoadExample);
ExportFailedCommand = new AsyncRelayCommand(ExportFailedAsync, () => HasFailures);
RetryFailedCommand = new AsyncRelayCommand(RetryFailedAsync, () => HasFailures);
}
private void ImportCsv()
{
var dlg = new OpenFileDialog
{
Title = TranslationSource.Instance["bulk.csvimport.title"],
Filter = TranslationSource.Instance["bulk.csvimport.filter"],
};
if (dlg.ShowDialog() != true) return;
using var stream = File.OpenRead(dlg.FileName);
LoadAndPreview(stream);
}
private void LoadExample()
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = assembly.GetManifestResourceNames()
.FirstOrDefault(n => n.EndsWith("bulk_add_members.csv", StringComparison.OrdinalIgnoreCase));
if (resourceName == null) return;
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream != null) LoadAndPreview(stream);
}
private void LoadAndPreview(Stream stream)
{
var rows = _csvService.ParseAndValidateMembers(stream);
PreviewRows = new ObservableCollection<CsvValidationRow<BulkMemberRow>>(rows);
_validRows = rows.Where(r => r.IsValid && r.Record != null).Select(r => r.Record!).ToList();
var invalidCount = rows.Count - _validRows.Count;
PreviewSummary = string.Format(TranslationSource.Instance["bulkmembers.preview"],
rows.Count, _validRows.Count, invalidCount);
HasPreview = true;
ResultSummary = string.Empty;
HasFailures = false;
}
protected override async Task RunOperationAsync(CancellationToken ct, IProgress<OperationProgress> progress)
{
if (_currentProfile == null) throw new InvalidOperationException("No tenant connected.");
if (_validRows == null || _validRows.Count == 0)
throw new InvalidOperationException("No valid rows to process. Import a CSV first.");
var message = string.Format(TranslationSource.Instance["bulk.confirm.message"],
$"{_validRows.Count} members will be added");
if (ShowConfirmDialog != null && !ShowConfirmDialog(message))
return;
var ctx = await _sessionManager.GetOrCreateContextAsync(_currentProfile, ct);
_lastResult = await _memberService.AddMembersAsync(ctx, _currentProfile.ClientId, _validRows, progress, ct);
await Application.Current.Dispatcher.InvokeAsync(() =>
{
HasFailures = _lastResult.HasFailures;
_failedRowsForRetry = _lastResult.HasFailures
? _lastResult.FailedItems.Select(r => r.Item).ToList()
: null;
ExportFailedCommand.NotifyCanExecuteChanged();
RetryFailedCommand.NotifyCanExecuteChanged();
ResultSummary = _lastResult.HasFailures
? string.Format(TranslationSource.Instance["bulk.result.success"],
_lastResult.SuccessCount, _lastResult.FailedCount)
: string.Format(TranslationSource.Instance["bulk.result.allsuccess"],
_lastResult.TotalCount);
});
}
private async Task RetryFailedAsync()
{
if (_failedRowsForRetry == null || _failedRowsForRetry.Count == 0) return;
_validRows = _failedRowsForRetry;
HasFailures = false;
await RunCommand.ExecuteAsync(null);
}
private async Task ExportFailedAsync()
{
if (_lastResult == null || !_lastResult.HasFailures) return;
var dlg = new SaveFileDialog { Filter = "CSV Files (*.csv)|*.csv", FileName = "failed_members.csv" };
if (dlg.ShowDialog() == true)
{
await _exportService.WriteFailedItemsCsvAsync(_lastResult.FailedItems.ToList(), dlg.FileName, CancellationToken.None);
Log.Information("Exported failed member rows to {Path}", dlg.FileName);
}
}
protected override void OnTenantSwitched(TenantProfile profile)
{
_currentProfile = profile;
PreviewRows = new();
_validRows = null;
PreviewSummary = string.Empty;
ResultSummary = string.Empty;
HasFailures = false;
HasPreview = false;
}
}