diff --git a/SharepointToolbox/ViewModels/Tabs/BulkMembersViewModel.cs b/SharepointToolbox/ViewModels/Tabs/BulkMembersViewModel.cs new file mode 100644 index 0000000..2c703d7 --- /dev/null +++ b/SharepointToolbox/ViewModels/Tabs/BulkMembersViewModel.cs @@ -0,0 +1,166 @@ +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; + } +} diff --git a/SharepointToolbox/ViewModels/Tabs/BulkSitesViewModel.cs b/SharepointToolbox/ViewModels/Tabs/BulkSitesViewModel.cs new file mode 100644 index 0000000..032213a --- /dev/null +++ b/SharepointToolbox/ViewModels/Tabs/BulkSitesViewModel.cs @@ -0,0 +1,165 @@ +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 BulkSitesViewModel : FeatureViewModelBase +{ + private readonly IBulkSiteService _siteService; + 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 BulkSitesViewModel( + IBulkSiteService siteService, + ICsvValidationService csvService, + ISessionManager sessionManager, + BulkResultCsvExportService exportService, + ILogger logger) + : base(logger) + { + _siteService = siteService; + _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_create_sites.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.ParseAndValidateSites(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["bulksites.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} sites will be created"); + if (ShowConfirmDialog != null && !ShowConfirmDialog(message)) + return; + + var ctx = await _sessionManager.GetOrCreateContextAsync(_currentProfile, ct); + _lastResult = await _siteService.CreateSitesAsync(ctx, _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_sites.csv" }; + if (dlg.ShowDialog() == true) + { + await _exportService.WriteFailedItemsCsvAsync(_lastResult.FailedItems.ToList(), dlg.FileName, CancellationToken.None); + } + } + + protected override void OnTenantSwitched(TenantProfile profile) + { + _currentProfile = profile; + PreviewRows = new(); + _validRows = null; + PreviewSummary = string.Empty; + ResultSummary = string.Empty; + HasFailures = false; + HasPreview = false; + } +} diff --git a/SharepointToolbox/ViewModels/Tabs/FolderStructureViewModel.cs b/SharepointToolbox/ViewModels/Tabs/FolderStructureViewModel.cs new file mode 100644 index 0000000..10dde3c --- /dev/null +++ b/SharepointToolbox/ViewModels/Tabs/FolderStructureViewModel.cs @@ -0,0 +1,166 @@ +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 FolderStructureViewModel : FeatureViewModelBase +{ + private readonly IFolderStructureService _folderService; + private readonly ICsvValidationService _csvService; + private readonly ISessionManager _sessionManager; + private readonly BulkResultCsvExportService _exportService; + private readonly ILogger _logger; + private TenantProfile? _currentProfile; + private List? _validRows; + private BulkOperationSummary? _lastResult; + + [ObservableProperty] private string _siteUrl = string.Empty; + [ObservableProperty] private string _libraryTitle = string.Empty; + [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 Func? ShowConfirmDialog { get; set; } + public TenantProfile? CurrentProfile => _currentProfile; + + public FolderStructureViewModel( + IFolderStructureService folderService, + ICsvValidationService csvService, + ISessionManager sessionManager, + BulkResultCsvExportService exportService, + ILogger logger) + : base(logger) + { + _folderService = folderService; + _csvService = csvService; + _sessionManager = sessionManager; + _exportService = exportService; + _logger = logger; + + ImportCsvCommand = new RelayCommand(ImportCsv); + LoadExampleCommand = new RelayCommand(LoadExample); + ExportFailedCommand = new AsyncRelayCommand(ExportFailedAsync, () => 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("folder_structure.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.ParseAndValidateFolders(stream); + PreviewRows = new ObservableCollection>(rows); + _validRows = rows.Where(r => r.IsValid && r.Record != null).Select(r => r.Record!).ToList(); + + var uniquePaths = FolderStructureService.BuildUniquePaths(_validRows); + PreviewSummary = string.Format(TranslationSource.Instance["folderstruct.preview"], uniquePaths.Count); + 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. Import a CSV first."); + if (string.IsNullOrWhiteSpace(SiteUrl)) + throw new InvalidOperationException("Site URL is required."); + if (string.IsNullOrWhiteSpace(LibraryTitle)) + throw new InvalidOperationException("Library title is required."); + + var uniquePaths = FolderStructureService.BuildUniquePaths(_validRows); + var message = string.Format(TranslationSource.Instance["bulk.confirm.message"], + $"{uniquePaths.Count} folders will be created in {LibraryTitle}"); + if (ShowConfirmDialog != null && !ShowConfirmDialog(message)) + return; + + var profile = new TenantProfile + { + Name = _currentProfile.Name, + TenantUrl = SiteUrl, + ClientId = _currentProfile.ClientId, + }; + var ctx = await _sessionManager.GetOrCreateContextAsync(profile, ct); + + _lastResult = await _folderService.CreateFoldersAsync(ctx, LibraryTitle, _validRows, progress, ct); + + await Application.Current.Dispatcher.InvokeAsync(() => + { + HasFailures = _lastResult.HasFailures; + ExportFailedCommand.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 ExportFailedAsync() + { + if (_lastResult == null || !_lastResult.HasFailures) return; + var dlg = new SaveFileDialog { Filter = "CSV Files (*.csv)|*.csv", FileName = "failed_folders.csv" }; + if (dlg.ShowDialog() == true) + { + await _exportService.WriteFailedItemsCsvAsync(_lastResult.FailedItems.ToList(), dlg.FileName, CancellationToken.None); + } + } + + protected override void OnTenantSwitched(TenantProfile profile) + { + _currentProfile = profile; + SiteUrl = string.Empty; + LibraryTitle = string.Empty; + PreviewRows = new(); + _validRows = null; + PreviewSummary = string.Empty; + ResultSummary = string.Empty; + HasFailures = false; + HasPreview = false; + } +} diff --git a/SharepointToolbox/Views/Tabs/BulkMembersView.xaml b/SharepointToolbox/Views/Tabs/BulkMembersView.xaml new file mode 100644 index 0000000..2a27d09 --- /dev/null +++ b/SharepointToolbox/Views/Tabs/BulkMembersView.xaml @@ -0,0 +1,50 @@ + + + + +