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 _logger; private TenantProfile? _currentProfile; private List? _validRows; private List? _failedRowsForRetry; private BulkOperationSummary? _lastResult; [ObservableProperty] private string _previewSummary = string.Empty; [ObservableProperty] private string _resultSummary = string.Empty; [ObservableProperty] private bool _hasFailures; [ObservableProperty] private bool _hasPreview; private ObservableCollection> _previewRows = new(); public ObservableCollection> 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? ShowConfirmDialog { get; set; } public TenantProfile? CurrentProfile => _currentProfile; public BulkMembersViewModel( IBulkMemberService memberService, ICsvValidationService csvService, ISessionManager sessionManager, BulkResultCsvExportService exportService, ILogger 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>(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 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; } }