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>
93 lines
4.3 KiB
C#
93 lines
4.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using StarfaceOutlookSync.Models;
|
|
|
|
namespace StarfaceOutlookSync.Services
|
|
{
|
|
/// <summary>
|
|
/// Erkennt, ob zwei Kontakte dieselbe Person/Firma sind. Wird sowohl vom
|
|
/// Sync (Wiederzuordnung, Duplikat-Vermeidung) als auch von der
|
|
/// Dubletten-Zusammenfuehrung genutzt - beide MUESSEN dieselbe Logik
|
|
/// verwenden, sonst laufen sie gegeneinander.
|
|
/// </summary>
|
|
public static class ContactMatcher
|
|
{
|
|
public static UnifiedContact FindMatch(UnifiedContact contact, List<UnifiedContact> candidates)
|
|
{
|
|
if (candidates == null || candidates.Count == 0) return null;
|
|
foreach (var c in candidates)
|
|
if (IsMatch(contact, c))
|
|
return c;
|
|
return null;
|
|
}
|
|
|
|
public static bool IsMatch(UnifiedContact a, UnifiedContact b)
|
|
{
|
|
bool hasName = (!string.IsNullOrEmpty(a.FirstName) || !string.IsNullOrEmpty(a.LastName))
|
|
&& (!string.IsNullOrEmpty(b.FirstName) || !string.IsNullOrEmpty(b.LastName));
|
|
|
|
bool emailMatch = !string.IsNullOrEmpty(a.Email) && !string.IsNullOrEmpty(b.Email)
|
|
&& a.Email.Equals(b.Email, StringComparison.OrdinalIgnoreCase);
|
|
bool nameMatch = hasName
|
|
&& (a.FirstName ?? "").Equals(b.FirstName ?? "", StringComparison.OrdinalIgnoreCase)
|
|
&& (a.LastName ?? "").Equals(b.LastName ?? "", StringComparison.OrdinalIgnoreCase);
|
|
// Telefon feldUEBERGREIFEND und formatunabhaengig (+49 == 0).
|
|
bool phoneMatch = SharedPhone(a, b);
|
|
bool companyMatch = !string.IsNullOrEmpty(a.Company) && !string.IsNullOrEmpty(b.Company)
|
|
&& a.Company.Equals(b.Company, StringComparison.OrdinalIgnoreCase);
|
|
|
|
bool emailContradiction = !string.IsNullOrEmpty(a.Email) && !string.IsNullOrEmpty(b.Email) && !emailMatch;
|
|
|
|
if (emailMatch) return true;
|
|
if (nameMatch && !emailContradiction) return true;
|
|
|
|
// Firma-/Service-Eintraege OHNE Personennamen: gleiche Firma + gemeinsame
|
|
// Nummer. Bewusst nur ohne Namen, damit nicht zwei Kollegen mit gleicher
|
|
// Zentrale-Nummer + Firma faelschlich verschmolzen werden.
|
|
bool aHasName = !string.IsNullOrEmpty(a.FirstName) || !string.IsNullOrEmpty(a.LastName);
|
|
bool bHasName = !string.IsNullOrEmpty(b.FirstName) || !string.IsNullOrEmpty(b.LastName);
|
|
if (phoneMatch && companyMatch && !aHasName && !bHasName && !emailContradiction) return true;
|
|
|
|
// Reine Nummern-Eintraege: eine gemeinsame Rufnummer identifiziert den Eintrag.
|
|
if (phoneMatch && IsBareNumberEntry(a) && IsBareNumberEntry(b)) return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool IsBareNumberEntry(UnifiedContact c) =>
|
|
string.IsNullOrEmpty(c.FirstName) && string.IsNullOrEmpty(c.LastName)
|
|
&& string.IsNullOrEmpty(c.Company) && string.IsNullOrEmpty(c.Email);
|
|
|
|
/// <summary>True, wenn a und b mindestens eine (normalisierte) Rufnummer gemeinsam haben.</summary>
|
|
public static bool SharedPhone(UnifiedContact a, UnifiedContact b)
|
|
{
|
|
var pa = PhoneSet(a);
|
|
if (pa.Count == 0) return false;
|
|
var pb = PhoneSet(b);
|
|
return pb.Count > 0 && pa.Overlaps(pb);
|
|
}
|
|
|
|
private static HashSet<string> PhoneSet(UnifiedContact c)
|
|
{
|
|
var set = new HashSet<string>();
|
|
foreach (var p in new[] { c.PhoneWork, c.PhoneMobile, c.PhoneHome, c.Fax })
|
|
{
|
|
var n = NormalizePhone(p);
|
|
if (n.Length >= 5) set.Add(n); // zu kurze (z.B. reine Durchwahlen) ignorieren
|
|
}
|
|
return set;
|
|
}
|
|
|
|
public static string NormalizePhone(string phone)
|
|
{
|
|
if (string.IsNullOrEmpty(phone)) return "";
|
|
var s = new string(phone.Where(c => char.IsDigit(c) || c == '+').ToArray());
|
|
if (s.StartsWith("+49")) s = "0" + s.Substring(3);
|
|
else if (s.StartsWith("0049")) s = "0" + s.Substring(4);
|
|
else if (s.StartsWith("+")) s = s.Substring(1);
|
|
return s;
|
|
}
|
|
}
|
|
}
|