Add "Dubletten zusammenfuehren" (merge duplicates) per profile
Neuer Button "Dubletten" im Hauptfenster fuehrt doppelte Kontakte fuer das ausgewaehlte Profil zusammen - je Seite (Telefonanlage + Outlook) getrennt. - Matching in ContactMatcher extrahiert (Sync UND Dedupe nutzen exakt dieselbe Erkennung). - ContactMerger.FillEmptyInto / FilledFieldCount: leere Felder des Gewinners auffuellen (kein Datenverlust), Gewinner-Wahl nach Zuordnung/Vollstaendigkeit. - DedupeService: Analyse (Vorschau) + Ausfuehrung (Merge, Loeschen der Dubletten, Bereinigen verwaister Zuordnungen). - SyncCoordinator.RunExclusiveAsync: Dedupe laeuft unter demselben Guard + Lock-Datei wie ein Sync (nie gleichzeitig; kein Konflikt zwischen Clients). - DedupeForm: Vorschau mit optionaler Detailliste, Bestaetigung, Live-Log; danach Hinweis, einmal zu synchronisieren. - MainForm: Button "Dubletten" (Profil-Auswahl noetig), Fenster verbreitert. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using StarfaceOutlookSync.Models;
|
||||
using StarfaceOutlookSync.Services;
|
||||
|
||||
namespace StarfaceOutlookSync.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Dialog zum Zusammenfuehren doppelter Kontakte fuer EIN Profil.
|
||||
/// Analysiert zuerst (Vorschau), fuehrt nach Bestaetigung unter derselben
|
||||
/// Sperre wie ein Sync zusammen.
|
||||
/// </summary>
|
||||
public class DedupeForm : Form
|
||||
{
|
||||
private readonly SyncProfile _profile;
|
||||
private readonly DedupeService _dedupe = new DedupeService();
|
||||
private readonly SyncCoordinator _coordinator = new SyncCoordinator();
|
||||
|
||||
private Label _lblInfo, _lblSummary;
|
||||
private CheckBox _chkDetail;
|
||||
private TextBox _txtLog;
|
||||
private Button _btnStart, _btnClose;
|
||||
|
||||
private DedupeService.DedupePlan _plan;
|
||||
private bool _busy;
|
||||
|
||||
public DedupeForm(SyncProfile profile)
|
||||
{
|
||||
_profile = profile;
|
||||
_dedupe.OnProgress += AppendLog;
|
||||
InitializeComponent();
|
||||
Load += async (s, e) => await Analyze();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
Text = $"Dubletten zusammenfuehren - {_profile.Name}";
|
||||
Size = new Size(640, 500);
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
Font = new Font("Segoe UI", 9);
|
||||
MinimizeBox = false;
|
||||
|
||||
_lblInfo = new Label
|
||||
{
|
||||
Text = $"Profil: {_profile.Name} ({_profile.Connection.System} <-> {_profile.OutlookFolderName})",
|
||||
Left = 12, Top = 12, Width = 600, Height = 20
|
||||
};
|
||||
|
||||
_lblSummary = new Label
|
||||
{
|
||||
Text = "Analysiere...",
|
||||
Left = 12, Top = 36, Width = 600, Height = 40, ForeColor = Color.DimGray
|
||||
};
|
||||
|
||||
_chkDetail = new CheckBox
|
||||
{
|
||||
Text = "Detailvorschau anzeigen (welche Kontakte zusammengefuehrt werden)",
|
||||
Left = 12, Top = 80, AutoSize = true, Enabled = false
|
||||
};
|
||||
_chkDetail.CheckedChanged += (s, e) => { if (_chkDetail.Checked) RenderDetail(); };
|
||||
|
||||
_txtLog = new TextBox
|
||||
{
|
||||
Left = 12, Top = 108, Width = 604, Height = 300,
|
||||
Multiline = true, ReadOnly = true, ScrollBars = ScrollBars.Both, WordWrap = false,
|
||||
BackColor = Color.FromArgb(30, 30, 30), ForeColor = Color.FromArgb(212, 212, 212),
|
||||
Font = new Font("Consolas", 9),
|
||||
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom
|
||||
};
|
||||
|
||||
_btnStart = new Button
|
||||
{
|
||||
Text = "Zusammenfuehren starten", Left = 12, Top = 420, Width = 190, Height = 30,
|
||||
Enabled = false, Anchor = AnchorStyles.Bottom | AnchorStyles.Left
|
||||
};
|
||||
_btnStart.Click += async (s, e) => await Execute();
|
||||
|
||||
_btnClose = new Button
|
||||
{
|
||||
Text = "Schliessen", Left = 526, Top = 420, Width = 90, Height = 30,
|
||||
DialogResult = DialogResult.Cancel, Anchor = AnchorStyles.Bottom | AnchorStyles.Right
|
||||
};
|
||||
|
||||
Controls.AddRange(new Control[] { _lblInfo, _lblSummary, _chkDetail, _txtLog, _btnStart, _btnClose });
|
||||
CancelButton = _btnClose;
|
||||
}
|
||||
|
||||
private void AppendLog(string message)
|
||||
{
|
||||
if (InvokeRequired) { Invoke(new Action(() => AppendLog(message))); return; }
|
||||
_txtLog.AppendText(message + "\r\n");
|
||||
}
|
||||
|
||||
private async Task Analyze()
|
||||
{
|
||||
_busy = true;
|
||||
_btnStart.Enabled = false;
|
||||
try
|
||||
{
|
||||
_plan = await Task.Run(() => _dedupe.AnalyzeAsync(_profile));
|
||||
|
||||
var s = $"Telefonanlage: {_plan.RemoteGroups.Count} Gruppe(n) mit Dubletten, {_plan.RemoteDeletions} Kontakt(e) werden zusammengefuehrt.\r\n" +
|
||||
$"Outlook: {_plan.OutlookGroups.Count} Gruppe(n) mit Dubletten, {_plan.OutlookDeletions} Kontakt(e) werden zusammengefuehrt.";
|
||||
_lblSummary.Text = _plan.HasWork ? s : "Keine Dubletten gefunden. Alles sauber. ✓";
|
||||
_lblSummary.ForeColor = _plan.HasWork ? Color.OrangeRed : Color.Green;
|
||||
|
||||
_chkDetail.Enabled = _plan.HasWork;
|
||||
_btnStart.Enabled = _plan.HasWork;
|
||||
if (_chkDetail.Checked) RenderDetail();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_lblSummary.Text = "Analyse fehlgeschlagen: " + ex.Message;
|
||||
_lblSummary.ForeColor = Color.Red;
|
||||
AppendLog("FEHLER: " + ex.Message);
|
||||
}
|
||||
finally { _busy = false; }
|
||||
}
|
||||
|
||||
private void RenderDetail()
|
||||
{
|
||||
if (_plan == null) return;
|
||||
AppendLog("");
|
||||
AppendLog("=== Vorschau: Telefonanlage ===");
|
||||
foreach (var g in _plan.RemoteGroups)
|
||||
AppendLog($" Behalten: '{g.Survivor.DisplayName}' <= {g.Duplicates.Count} Dublette(n)");
|
||||
AppendLog("=== Vorschau: Outlook ===");
|
||||
foreach (var g in _plan.OutlookGroups)
|
||||
AppendLog($" Behalten: '{g.Survivor.DisplayName}' <= {g.Duplicates.Count} Dublette(n)");
|
||||
AppendLog("");
|
||||
}
|
||||
|
||||
private async Task Execute()
|
||||
{
|
||||
if (_busy || _plan == null || !_plan.HasWork) return;
|
||||
|
||||
var confirm = MessageBox.Show(this,
|
||||
$"Jetzt zusammenfuehren?\n\n" +
|
||||
$"Telefonanlage: {_plan.RemoteDeletions} Dublette(n) werden geloescht.\n" +
|
||||
$"Outlook: {_plan.OutlookDeletions} Dublette(n) werden geloescht.\n\n" +
|
||||
$"Die Daten der Dubletten werden vorher in den behaltenen Kontakt uebernommen (leere Felder aufgefuellt).\n" +
|
||||
$"Danach bitte einmal synchronisieren.",
|
||||
"Dubletten zusammenfuehren", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
|
||||
if (confirm != DialogResult.Yes) return;
|
||||
|
||||
_busy = true;
|
||||
_btnStart.Enabled = false;
|
||||
_btnClose.Enabled = false;
|
||||
AppendLog("Starte Zusammenfuehrung...");
|
||||
Logger.Log($"Dubletten-Zusammenfuehrung gestartet: Profil '{_profile.Name}'");
|
||||
|
||||
try
|
||||
{
|
||||
DedupeService.DedupeReport report = null;
|
||||
bool ran = await _coordinator.RunExclusiveAsync(
|
||||
async () => { report = await Task.Run(() => _dedupe.ExecuteAsync(_profile)); },
|
||||
status: AppendLog);
|
||||
|
||||
if (!ran)
|
||||
{
|
||||
AppendLog("Abgebrochen (Sync/Aktion laeuft oder anderer Arbeitsplatz aktiv).");
|
||||
}
|
||||
else if (report != null)
|
||||
{
|
||||
var summary = $"Fertig. Anlage: {report.RemoteMerged} Gruppe(n), {report.RemoteDeleted} geloescht. " +
|
||||
$"Outlook: {report.OutlookMerged} Gruppe(n), {report.OutlookDeleted} geloescht. " +
|
||||
$"Fehler: {report.Errors}.";
|
||||
AppendLog(summary);
|
||||
foreach (var m in report.Messages) AppendLog(" " + m);
|
||||
AppendLog("Bitte jetzt einmal synchronisieren, damit die Zuordnungen sauber neu aufgebaut werden.");
|
||||
Logger.Log("Dubletten-Zusammenfuehrung: " + summary);
|
||||
foreach (var m in report.Messages) Logger.Log(" " + m);
|
||||
|
||||
_lblSummary.Text = summary + " -> Jetzt synchronisieren.";
|
||||
_lblSummary.ForeColor = report.Errors > 0 ? Color.OrangeRed : Color.Green;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppendLog("FEHLER: " + ex.Message);
|
||||
Logger.Log($"Dubletten-Zusammenfuehrung FEHLER '{_profile.Name}': {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_busy = false;
|
||||
_btnClose.Enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ namespace StarfaceOutlookSync.UI
|
||||
private NotifyIcon _trayIcon;
|
||||
private ContextMenuStrip _trayMenu;
|
||||
private ListView _profileList;
|
||||
private Button _btnNew, _btnEdit, _btnDelete, _btnSync, _btnReset, _btnSettings, _btnLog, _btnInfo;
|
||||
private Button _btnNew, _btnEdit, _btnDelete, _btnSync, _btnDedupe, _btnReset, _btnSettings, _btnLog, _btnInfo;
|
||||
private StatusStrip _statusBar;
|
||||
private ToolStripStatusLabel _statusLabel;
|
||||
private Timer _autoSyncTimer;
|
||||
@@ -83,8 +83,8 @@ namespace StarfaceOutlookSync.UI
|
||||
private void InitializeComponent()
|
||||
{
|
||||
Text = "Starface Kontakt-Sync";
|
||||
Size = new Size(830, 450);
|
||||
MinimumSize = new Size(830, 350);
|
||||
Size = new Size(930, 450);
|
||||
MinimumSize = new Size(930, 350);
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
Font = new Font("Segoe UI", 9);
|
||||
Icon = AppIcon.GetIcon();
|
||||
@@ -126,6 +126,9 @@ namespace StarfaceOutlookSync.UI
|
||||
_btnSync = new Button { Text = "Synchronisieren", Width = 110, Height = 30 };
|
||||
_btnSync.Click += async (s, e) => await SyncSelectedProfile();
|
||||
|
||||
_btnDedupe = new Button { Text = "Dubletten", Width = 85, Height = 30 };
|
||||
_btnDedupe.Click += (s, e) => ShowDedupe();
|
||||
|
||||
_btnReset = new Button { Text = "Sync Reset", Width = 80, Height = 30 };
|
||||
_btnReset.Click += (s, e) => ResetSync();
|
||||
|
||||
@@ -138,7 +141,7 @@ namespace StarfaceOutlookSync.UI
|
||||
_btnInfo = new Button { Text = "Info", Width = 50, Height = 30 };
|
||||
_btnInfo.Click += (s, e) => ShowAbout();
|
||||
|
||||
buttonPanel.Controls.AddRange(new Control[] { _btnNew, _btnEdit, _btnDelete, _btnSync, _btnReset, _btnSettings, _btnLog, _btnInfo });
|
||||
buttonPanel.Controls.AddRange(new Control[] { _btnNew, _btnEdit, _btnDelete, _btnSync, _btnDedupe, _btnReset, _btnSettings, _btnLog, _btnInfo });
|
||||
|
||||
// Statusbar
|
||||
_statusBar = new StatusStrip();
|
||||
@@ -458,6 +461,24 @@ namespace StarfaceOutlookSync.UI
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowDedupe()
|
||||
{
|
||||
if (_profileList.SelectedItems.Count == 0)
|
||||
{
|
||||
MessageBox.Show("Bitte zuerst ein Profil auswaehlen.", "Dubletten",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
var profile = _profileList.SelectedItems[0].Tag as SyncProfile;
|
||||
if (profile == null) return;
|
||||
|
||||
using (var form = new DedupeForm(profile))
|
||||
{
|
||||
form.ShowDialog(this);
|
||||
}
|
||||
RefreshProfileList();
|
||||
}
|
||||
|
||||
private void ExitApplication()
|
||||
{
|
||||
_autoSyncTimer?.Stop();
|
||||
|
||||
Reference in New Issue
Block a user