diff --git a/SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs b/SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs
new file mode 100644
index 0000000..4d40b15
--- /dev/null
+++ b/SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs
@@ -0,0 +1,169 @@
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Windows;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Microsoft.Extensions.Logging;
+using Microsoft.Win32;
+using SharepointToolbox.Core.Models;
+using SharepointToolbox.Services;
+using SharepointToolbox.Services.Export;
+
+namespace SharepointToolbox.ViewModels.Tabs;
+
+/// Flat display row wrapping a DuplicateItem with its group name.
+public class DuplicateRow
+{
+ public string GroupName { get; set; } = string.Empty;
+ public string Name { get; set; } = string.Empty;
+ public string Path { get; set; } = string.Empty;
+ public string Library { get; set; } = string.Empty;
+ public long? SizeBytes { get; set; }
+ public DateTime? Created { get; set; }
+ public DateTime? Modified { get; set; }
+ public int? FolderCount { get; set; }
+ public int? FileCount { get; set; }
+ public int GroupSize { get; set; }
+}
+
+public partial class DuplicatesViewModel : FeatureViewModelBase
+{
+ private readonly IDuplicatesService _duplicatesService;
+ private readonly ISessionManager _sessionManager;
+ private readonly DuplicatesHtmlExportService _htmlExportService;
+ private readonly ILogger _logger;
+ private TenantProfile? _currentProfile;
+ private IReadOnlyList _lastGroups = Array.Empty();
+
+ [ObservableProperty] private string _siteUrl = string.Empty;
+ [ObservableProperty] private bool _modeFiles = true;
+ [ObservableProperty] private bool _modeFolders;
+ [ObservableProperty] private bool _matchSize = true;
+ [ObservableProperty] private bool _matchCreated;
+ [ObservableProperty] private bool _matchModified;
+ [ObservableProperty] private bool _matchSubfolders;
+ [ObservableProperty] private bool _matchFileCount;
+ [ObservableProperty] private bool _includeSubsites;
+ [ObservableProperty] private string _library = string.Empty;
+
+ private ObservableCollection _results = new();
+ public ObservableCollection Results
+ {
+ get => _results;
+ private set
+ {
+ _results = value;
+ OnPropertyChanged();
+ ExportHtmlCommand.NotifyCanExecuteChanged();
+ }
+ }
+
+ public IAsyncRelayCommand ExportHtmlCommand { get; }
+ public TenantProfile? CurrentProfile => _currentProfile;
+
+ public DuplicatesViewModel(
+ IDuplicatesService duplicatesService,
+ ISessionManager sessionManager,
+ DuplicatesHtmlExportService htmlExportService,
+ ILogger logger)
+ : base(logger)
+ {
+ _duplicatesService = duplicatesService;
+ _sessionManager = sessionManager;
+ _htmlExportService = htmlExportService;
+ _logger = logger;
+
+ ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport);
+ }
+
+ protected override async Task RunOperationAsync(CancellationToken ct, IProgress progress)
+ {
+ if (_currentProfile == null)
+ {
+ StatusMessage = "No tenant selected. Please connect to a tenant first.";
+ return;
+ }
+ if (string.IsNullOrWhiteSpace(SiteUrl))
+ {
+ StatusMessage = "Please enter a site URL.";
+ return;
+ }
+
+ var siteProfile = new TenantProfile
+ {
+ TenantUrl = SiteUrl.TrimEnd('/'),
+ ClientId = _currentProfile.ClientId,
+ Name = _currentProfile.Name
+ };
+ var ctx = await _sessionManager.GetOrCreateContextAsync(siteProfile, ct);
+
+ var opts = new DuplicateScanOptions(
+ Mode: ModeFiles ? "Files" : "Folders",
+ MatchSize: MatchSize,
+ MatchCreated: MatchCreated,
+ MatchModified: MatchModified,
+ MatchSubfolderCount: MatchSubfolders,
+ MatchFileCount: MatchFileCount,
+ IncludeSubsites: IncludeSubsites,
+ Library: string.IsNullOrWhiteSpace(Library) ? null : Library
+ );
+
+ var groups = await _duplicatesService.ScanDuplicatesAsync(ctx, opts, progress, ct);
+ _lastGroups = groups;
+
+ // Flatten groups to display rows
+ var rows = groups
+ .SelectMany(g => g.Items.Select(item => new DuplicateRow
+ {
+ GroupName = g.Name,
+ Name = item.Name,
+ Path = item.Path,
+ Library = item.Library,
+ SizeBytes = item.SizeBytes,
+ Created = item.Created,
+ Modified = item.Modified,
+ FolderCount = item.FolderCount,
+ FileCount = item.FileCount,
+ GroupSize = g.Items.Count
+ }))
+ .ToList();
+
+ if (Application.Current?.Dispatcher is { } dispatcher)
+ await dispatcher.InvokeAsync(() => Results = new ObservableCollection(rows));
+ else
+ Results = new ObservableCollection(rows);
+ }
+
+ protected override void OnTenantSwitched(Core.Models.TenantProfile profile)
+ {
+ _currentProfile = profile;
+ Results = new ObservableCollection();
+ _lastGroups = Array.Empty();
+ SiteUrl = string.Empty;
+ OnPropertyChanged(nameof(CurrentProfile));
+ ExportHtmlCommand.NotifyCanExecuteChanged();
+ }
+
+ internal void SetCurrentProfile(TenantProfile profile) => _currentProfile = profile;
+
+ private bool CanExport() => _lastGroups.Count > 0;
+
+ private async Task ExportHtmlAsync()
+ {
+ if (_lastGroups.Count == 0) return;
+ var dialog = new SaveFileDialog
+ {
+ Title = "Export duplicates report to HTML",
+ Filter = "HTML files (*.html)|*.html|All files (*.*)|*.*",
+ DefaultExt = "html",
+ FileName = "duplicates_report"
+ };
+ if (dialog.ShowDialog() != true) return;
+ try
+ {
+ await _htmlExportService.WriteAsync(_lastGroups, dialog.FileName, CancellationToken.None);
+ Process.Start(new ProcessStartInfo(dialog.FileName) { UseShellExecute = true });
+ }
+ catch (Exception ex) { StatusMessage = $"Export failed: {ex.Message}"; _logger.LogError(ex, "HTML export failed."); }
+ }
+}
diff --git a/SharepointToolbox/Views/Tabs/DuplicatesView.xaml b/SharepointToolbox/Views/Tabs/DuplicatesView.xaml
new file mode 100644
index 0000000..3f78c0c
--- /dev/null
+++ b/SharepointToolbox/Views/Tabs/DuplicatesView.xaml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SharepointToolbox/Views/Tabs/DuplicatesView.xaml.cs b/SharepointToolbox/Views/Tabs/DuplicatesView.xaml.cs
new file mode 100644
index 0000000..f6c281a
--- /dev/null
+++ b/SharepointToolbox/Views/Tabs/DuplicatesView.xaml.cs
@@ -0,0 +1,12 @@
+using System.Windows.Controls;
+
+namespace SharepointToolbox.Views.Tabs;
+
+public partial class DuplicatesView : UserControl
+{
+ public DuplicatesView(ViewModels.Tabs.DuplicatesViewModel viewModel)
+ {
+ InitializeComponent();
+ DataContext = viewModel;
+ }
+}