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:
@@ -7,6 +7,18 @@ Versionsschema ist `x.x.x.x` (siehe `release.sh`).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Hinzugefuegt
|
||||||
|
|
||||||
|
- **Dubletten zusammenfuehren (Button "Dubletten").** Fuehrt doppelte Kontakte
|
||||||
|
fuer das ausgewaehlte Profil zusammen - getrennt je Seite (Telefonanlage und
|
||||||
|
Outlook). Zuerst wird analysiert und eine Vorschau (optional mit Detailliste)
|
||||||
|
gezeigt; nach Bestaetigung bleibt je Gruppe ein "Gewinner", dessen leere
|
||||||
|
Felder aus den Dubletten aufgefuellt werden (kein Datenverlust), die uebrigen
|
||||||
|
werden geloescht. Verwaiste Zuordnungen werden bereinigt, sodass der naechste
|
||||||
|
bidirektionale Sync die Gewinner sauber 1:1 verbindet. Laeuft unter derselben
|
||||||
|
Sperre wie ein Sync (kein gleichzeitiger Lauf), nutzt dieselbe Kontakt-
|
||||||
|
Erkennung wie der Sync und wird vollstaendig protokolliert.
|
||||||
|
|
||||||
### Behoben
|
### Behoben
|
||||||
|
|
||||||
- **Dubletten bei Firmen-/Service-Eintraegen (nur Nummer + Firma).** Solche
|
- **Dubletten bei Firmen-/Service-Eintraegen (nur Nummer + Firma).** Solche
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,6 +46,34 @@ namespace StarfaceOutlookSync.Services
|
|||||||
new FieldDef { Key = "Birthday", Label = "Geburtstag", Get = c => c.Birthday, Set = (c, v) => c.Birthday = v },
|
new FieldDef { Key = "Birthday", Label = "Geburtstag", Get = c => c.Birthday, Set = (c, v) => c.Birthday = v },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fuellt LEERE Felder von target aus den sources auf (erste nicht-leere
|
||||||
|
/// Quelle gewinnt). Vorhandene Werte von target bleiben unveraendert -
|
||||||
|
/// kein Datenverlust. Gibt true zurueck, wenn sich etwas geaendert hat.
|
||||||
|
/// </summary>
|
||||||
|
public static bool FillEmptyInto(UnifiedContact target, IEnumerable<UnifiedContact> sources)
|
||||||
|
{
|
||||||
|
bool changed = false;
|
||||||
|
var list = sources?.ToList() ?? new List<UnifiedContact>();
|
||||||
|
foreach (var f in Fields)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(f.Get(target))) continue;
|
||||||
|
foreach (var s in list)
|
||||||
|
{
|
||||||
|
var v = f.Get(s);
|
||||||
|
if (!string.IsNullOrEmpty(v)) { f.Set(target, v); changed = true; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Anzahl gefuellter Inhaltsfelder - fuer die Wahl des "Gewinners".</summary>
|
||||||
|
public static int FilledFieldCount(UnifiedContact c)
|
||||||
|
{
|
||||||
|
if (c == null) return 0;
|
||||||
|
return Fields.Count(f => !string.IsNullOrEmpty(f.Get(c)));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Liest den Wert eines Feldes per stabilem Schluessel.</summary>
|
/// <summary>Liest den Wert eines Feldes per stabilem Schluessel.</summary>
|
||||||
public static string GetValue(UnifiedContact c, string key)
|
public static string GetValue(UnifiedContact c, string key)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using StarfaceOutlookSync.Models;
|
||||||
|
|
||||||
|
namespace StarfaceOutlookSync.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Fuehrt Dubletten innerhalb EINES Profils zusammen - getrennt je Seite
|
||||||
|
/// (Telefonanlage und Outlook). Regel: ein "Gewinner" bleibt, dessen leere
|
||||||
|
/// Felder aus den Dubletten aufgefuellt werden (kein Datenverlust), die
|
||||||
|
/// uebrigen werden geloescht. Danach werden verwaiste Zuordnungen entfernt,
|
||||||
|
/// damit der naechste (bidirektionale) Sync die Gewinner sauber 1:1 verbindet.
|
||||||
|
/// Nutzt dieselbe Erkennung wie der Sync (ContactMatcher).
|
||||||
|
/// </summary>
|
||||||
|
public class DedupeService
|
||||||
|
{
|
||||||
|
private readonly ProfileManager _profileManager = new ProfileManager();
|
||||||
|
private readonly OutlookContactsService _outlookService = new OutlookContactsService();
|
||||||
|
|
||||||
|
public event Action<string> OnProgress;
|
||||||
|
private void Log(string m) => OnProgress?.Invoke(m);
|
||||||
|
|
||||||
|
public class DupGroup
|
||||||
|
{
|
||||||
|
public UnifiedContact Survivor;
|
||||||
|
public List<UnifiedContact> Duplicates = new List<UnifiedContact>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DedupePlan
|
||||||
|
{
|
||||||
|
public List<DupGroup> RemoteGroups = new List<DupGroup>(); // Telefonanlage
|
||||||
|
public List<DupGroup> OutlookGroups = new List<DupGroup>();
|
||||||
|
public int RemoteDeletions => RemoteGroups.Sum(g => g.Duplicates.Count);
|
||||||
|
public int OutlookDeletions => OutlookGroups.Sum(g => g.Duplicates.Count);
|
||||||
|
public bool HasWork => RemoteGroups.Count > 0 || OutlookGroups.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DedupeReport
|
||||||
|
{
|
||||||
|
public int RemoteMerged, RemoteDeleted, OutlookMerged, OutlookDeleted, Errors;
|
||||||
|
public List<string> Messages = new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Nur analysieren (lesend), fuer die Vorschau.</summary>
|
||||||
|
public async Task<DedupePlan> AnalyzeAsync(SyncProfile profile)
|
||||||
|
{
|
||||||
|
var plan = new DedupePlan();
|
||||||
|
|
||||||
|
var mappings = _profileManager.GetMappings(profile.Id);
|
||||||
|
var mappedStarface = new HashSet<string>(mappings.Where(m => !string.IsNullOrEmpty(m.StarfaceId)).Select(m => m.StarfaceId));
|
||||||
|
var mappedOutlook = new HashSet<string>(mappings.Where(m => !string.IsNullOrEmpty(m.OutlookEntryId)).Select(m => m.OutlookEntryId));
|
||||||
|
|
||||||
|
Log("Lade Outlook-Kontakte...");
|
||||||
|
var outlookContacts = _outlookService.GetContacts(profile.OutlookFolderPath);
|
||||||
|
|
||||||
|
Log($"Verbinde mit {profile.Connection.System}...");
|
||||||
|
using (var backend = ContactBackendFactory.Create(profile.Connection))
|
||||||
|
{
|
||||||
|
backend.OnDebug += Log;
|
||||||
|
if (!await backend.LoginAsync())
|
||||||
|
throw new Exception("Login an der Telefonanlage fehlgeschlagen.");
|
||||||
|
|
||||||
|
Log("Lade Kontakte der Telefonanlage...");
|
||||||
|
var remoteContacts = await backend.GetContactsAsync(profile.AddressBook);
|
||||||
|
await backend.LogoutAsync();
|
||||||
|
|
||||||
|
plan.RemoteGroups = GroupDuplicates(remoteContacts, c => mappedStarface.Contains(c.StarfaceId));
|
||||||
|
plan.OutlookGroups = GroupDuplicates(outlookContacts, c => mappedOutlook.Contains(c.OutlookEntryId));
|
||||||
|
}
|
||||||
|
|
||||||
|
Log($"Analyse fertig: Anlage {plan.RemoteGroups.Count} Gruppe(n)/{plan.RemoteDeletions} zu loeschen, " +
|
||||||
|
$"Outlook {plan.OutlookGroups.Count} Gruppe(n)/{plan.OutlookDeletions} zu loeschen.");
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fuehrt die Zusammenfuehrung wirklich aus. Laedt frisch (unter Sperre
|
||||||
|
/// aufrufen!), gruppiert und wendet an. Gibt einen Bericht zurueck.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<DedupeReport> ExecuteAsync(SyncProfile profile)
|
||||||
|
{
|
||||||
|
var report = new DedupeReport();
|
||||||
|
|
||||||
|
var mappings = _profileManager.GetMappings(profile.Id);
|
||||||
|
var mappedStarface = new HashSet<string>(mappings.Where(m => !string.IsNullOrEmpty(m.StarfaceId)).Select(m => m.StarfaceId));
|
||||||
|
var mappedOutlook = new HashSet<string>(mappings.Where(m => !string.IsNullOrEmpty(m.OutlookEntryId)).Select(m => m.OutlookEntryId));
|
||||||
|
|
||||||
|
var deletedStarfaceIds = new HashSet<string>();
|
||||||
|
var deletedOutlookIds = new HashSet<string>();
|
||||||
|
|
||||||
|
var outlookContacts = _outlookService.GetContacts(profile.OutlookFolderPath);
|
||||||
|
|
||||||
|
using (var backend = ContactBackendFactory.Create(profile.Connection))
|
||||||
|
{
|
||||||
|
backend.OnDebug += Log;
|
||||||
|
if (!await backend.LoginAsync())
|
||||||
|
throw new Exception("Login an der Telefonanlage fehlgeschlagen.");
|
||||||
|
|
||||||
|
var remoteContacts = await backend.GetContactsAsync(profile.AddressBook);
|
||||||
|
|
||||||
|
// --- Telefonanlage ---
|
||||||
|
foreach (var g in GroupDuplicates(remoteContacts, c => mappedStarface.Contains(c.StarfaceId)))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool changed = ContactMerger.FillEmptyInto(g.Survivor, g.Duplicates);
|
||||||
|
bool survivorOk = true;
|
||||||
|
if (changed)
|
||||||
|
survivorOk = await backend.UpdateContactAsync(g.Survivor.StarfaceId, g.Survivor, profile.AddressBook) != null;
|
||||||
|
|
||||||
|
if (!survivorOk)
|
||||||
|
{
|
||||||
|
// Gewinner konnte nicht ergaenzt werden -> Dubletten NICHT loeschen
|
||||||
|
// (sonst gingen die nur dort vorhandenen Felder verloren).
|
||||||
|
report.Errors++;
|
||||||
|
report.Messages.Add($"Anlage: '{g.Survivor.DisplayName}' konnte nicht ergaenzt werden - Gruppe uebersprungen.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var dup in g.Duplicates)
|
||||||
|
{
|
||||||
|
if (await backend.DeleteContactAsync(dup.StarfaceId))
|
||||||
|
{
|
||||||
|
deletedStarfaceIds.Add(dup.StarfaceId);
|
||||||
|
report.RemoteDeleted++;
|
||||||
|
Log($" Anlage: Dublette geloescht -> zusammengefuehrt in '{g.Survivor.DisplayName}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
report.RemoteMerged++;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
report.Errors++;
|
||||||
|
report.Messages.Add($"Anlage '{g.Survivor.DisplayName}': {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await backend.LogoutAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Outlook ---
|
||||||
|
foreach (var g in GroupDuplicates(outlookContacts, c => mappedOutlook.Contains(c.OutlookEntryId)))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool changed = ContactMerger.FillEmptyInto(g.Survivor, g.Duplicates);
|
||||||
|
bool survivorOk = true;
|
||||||
|
if (changed)
|
||||||
|
survivorOk = _outlookService.UpdateContact(g.Survivor.OutlookEntryId, g.Survivor) != null;
|
||||||
|
|
||||||
|
if (!survivorOk)
|
||||||
|
{
|
||||||
|
report.Errors++;
|
||||||
|
report.Messages.Add($"Outlook: '{g.Survivor.DisplayName}' konnte nicht ergaenzt werden - Gruppe uebersprungen.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var dup in g.Duplicates)
|
||||||
|
{
|
||||||
|
if (_outlookService.DeleteContact(dup.OutlookEntryId))
|
||||||
|
{
|
||||||
|
deletedOutlookIds.Add(dup.OutlookEntryId);
|
||||||
|
report.OutlookDeleted++;
|
||||||
|
Log($" Outlook: Dublette geloescht -> zusammengefuehrt in '{g.Survivor.DisplayName}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
report.OutlookMerged++;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
report.Errors++;
|
||||||
|
report.Messages.Add($"Outlook '{g.Survivor.DisplayName}': {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verwaiste Zuordnungen (zeigen auf geloeschte Dubletten) entfernen.
|
||||||
|
// Der naechste Sync verbindet die Gewinner sauber neu.
|
||||||
|
if (deletedStarfaceIds.Count > 0 || deletedOutlookIds.Count > 0)
|
||||||
|
{
|
||||||
|
var kept = mappings
|
||||||
|
.Where(m => !deletedStarfaceIds.Contains(m.StarfaceId) && !deletedOutlookIds.Contains(m.OutlookEntryId))
|
||||||
|
.ToList();
|
||||||
|
_profileManager.SaveMappings(profile.Id, kept);
|
||||||
|
}
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Gruppierung ----
|
||||||
|
|
||||||
|
private static List<DupGroup> GroupDuplicates(List<UnifiedContact> contacts, Func<UnifiedContact, bool> isMapped)
|
||||||
|
{
|
||||||
|
var groups = new List<DupGroup>();
|
||||||
|
if (contacts == null) return groups;
|
||||||
|
var used = new bool[contacts.Count];
|
||||||
|
|
||||||
|
for (int i = 0; i < contacts.Count; i++)
|
||||||
|
{
|
||||||
|
if (used[i]) continue;
|
||||||
|
var members = new List<UnifiedContact> { contacts[i] };
|
||||||
|
used[i] = true;
|
||||||
|
|
||||||
|
for (int j = i + 1; j < contacts.Count; j++)
|
||||||
|
{
|
||||||
|
if (used[j]) continue;
|
||||||
|
if (members.Any(m => ContactMatcher.IsMatch(m, contacts[j])))
|
||||||
|
{
|
||||||
|
members.Add(contacts[j]);
|
||||||
|
used[j] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (members.Count > 1)
|
||||||
|
{
|
||||||
|
var survivor = ChooseSurvivor(members, isMapped);
|
||||||
|
groups.Add(new DupGroup
|
||||||
|
{
|
||||||
|
Survivor = survivor,
|
||||||
|
Duplicates = members.Where(m => !ReferenceEquals(m, survivor)).ToList()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UnifiedContact ChooseSurvivor(List<UnifiedContact> members, Func<UnifiedContact, bool> isMapped)
|
||||||
|
{
|
||||||
|
// 1) einen bereits zugeordneten bevorzugen (Kontinuitaet),
|
||||||
|
// 2) sonst den mit den meisten gefuellten Feldern.
|
||||||
|
return members
|
||||||
|
.OrderByDescending(m => isMapped(m) ? 1 : 0)
|
||||||
|
.ThenByDescending(m => ContactMerger.FilledFieldCount(m))
|
||||||
|
.First();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -115,6 +115,40 @@ namespace StarfaceOutlookSync.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fuehrt eine beliebige exklusive Aktion (z.B. Dubletten zusammenfuehren)
|
||||||
|
/// unter demselben Schutz wie ein Sync aus: lokaler Guard + clientueber-
|
||||||
|
/// greifende Lock-Datei. Gibt false zurueck, wenn gerade ein Sync/eine
|
||||||
|
/// Aktion laeuft oder ein anderer Arbeitsplatz aktiv ist.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> RunExclusiveAsync(Func<Task> action, Action<string> status)
|
||||||
|
{
|
||||||
|
if (Interlocked.CompareExchange(ref _running, 1, 0) != 0)
|
||||||
|
{
|
||||||
|
status?.Invoke("Es laeuft bereits ein Sync / eine Aktion - bitte warten.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SyncLock crossLock = null;
|
||||||
|
var sharedDir = UserSettings.Load().SharedDirectory;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
crossLock = await AcquireCrossClientLock(sharedDir, status);
|
||||||
|
if (crossLock == null)
|
||||||
|
{
|
||||||
|
status?.Invoke("Anderer Arbeitsplatz ist gerade aktiv - abgebrochen.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await action();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
crossLock?.Dispose();
|
||||||
|
Interlocked.Exchange(ref _running, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<SyncLock> AcquireCrossClientLock(string dir, Action<string> status)
|
private async Task<SyncLock> AcquireCrossClientLock(string dir, Action<string> status)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(dir))
|
if (string.IsNullOrWhiteSpace(dir))
|
||||||
|
|||||||
@@ -36,103 +36,6 @@ namespace StarfaceOutlookSync.Services
|
|||||||
m.LastSyncHash = "";
|
m.LastSyncHash = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Findet einen passenden Kontakt in der Kandidatenliste.
|
|
||||||
/// Strenges Matching: Felder die auf einer Seite gefuellt sind muessen
|
|
||||||
/// auf der anderen auch gefuellt (und gleich) sein.
|
|
||||||
/// Ein leeres Feld auf einer Seite und ein gefuelltes auf der anderen
|
|
||||||
/// bedeutet: verschiedene Kontakte.
|
|
||||||
/// </summary>
|
|
||||||
private 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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));
|
|
||||||
|
|
||||||
// Starke Identifikatoren
|
|
||||||
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 vergleichen (die gleiche Nummer steht mal
|
|
||||||
// als geschaeftlich, mal als privat) 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);
|
|
||||||
|
|
||||||
// Widerspruch: beide haben eine E-Mail, aber unterschiedlich -> verschiedene Personen.
|
|
||||||
bool emailContradiction = !string.IsNullOrEmpty(a.Email) && !string.IsNullOrEmpty(b.Email) && !emailMatch;
|
|
||||||
|
|
||||||
// Gleiche E-Mail ist der staerkste Identifikator und reicht allein.
|
|
||||||
if (emailMatch) return true;
|
|
||||||
|
|
||||||
// Gleicher voller Name reicht, solange keine widerspruechliche E-Mail vorliegt.
|
|
||||||
// (Telefon-Umformatierung durch Starface darf einen Namens-Treffer NICHT verhindern.)
|
|
||||||
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 (kein Name, keine Firma, keine E-Mail auf beiden
|
|
||||||
// Seiten): 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>
|
|
||||||
private 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string NormalizePhone(string phone)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(phone)) return "";
|
|
||||||
// Nur Ziffern und + behalten
|
|
||||||
var s = new string(phone.Where(c => char.IsDigit(c) || c == '+').ToArray());
|
|
||||||
// Landesvorwahl vereinheitlichen, damit +49/0049 und 0 gleich sind.
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<SyncResult> SyncProfileAsync(SyncProfile profile)
|
public async Task<SyncResult> SyncProfileAsync(SyncProfile profile)
|
||||||
{
|
{
|
||||||
var result = new SyncResult
|
var result = new SyncResult
|
||||||
@@ -206,7 +109,7 @@ namespace StarfaceOutlookSync.Services
|
|||||||
{
|
{
|
||||||
// Outlook-Kontakt nicht gefunden.
|
// Outlook-Kontakt nicht gefunden.
|
||||||
// Erst pruefen ob er vielleicht nur eine neue EntryID hat
|
// Erst pruefen ob er vielleicht nur eine neue EntryID hat
|
||||||
var reMatch = FindMatch(sc, outlookContacts.Where(c =>
|
var reMatch = ContactMatcher.FindMatch(sc, outlookContacts.Where(c =>
|
||||||
!processedOutlookIds.Contains(c.OutlookEntryId)).ToList());
|
!processedOutlookIds.Contains(c.OutlookEntryId)).ToList());
|
||||||
if (reMatch != null)
|
if (reMatch != null)
|
||||||
{
|
{
|
||||||
@@ -462,7 +365,7 @@ namespace StarfaceOutlookSync.Services
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Duplikat-Check: existiert der Kontakt schon in der Starface?
|
// Duplikat-Check: existiert der Kontakt schon in der Starface?
|
||||||
var match = FindMatch(oc, unmappedStarface);
|
var match = ContactMatcher.FindMatch(oc, unmappedStarface);
|
||||||
if (match != null)
|
if (match != null)
|
||||||
{
|
{
|
||||||
// Existiert schon -> verknuepfen und updaten
|
// Existiert schon -> verknuepfen und updaten
|
||||||
@@ -542,7 +445,7 @@ namespace StarfaceOutlookSync.Services
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Duplikat-Check: existiert der Kontakt schon in Outlook?
|
// Duplikat-Check: existiert der Kontakt schon in Outlook?
|
||||||
var match = FindMatch(sc, unmappedOutlook);
|
var match = ContactMatcher.FindMatch(sc, unmappedOutlook);
|
||||||
if (match != null)
|
if (match != null)
|
||||||
{
|
{
|
||||||
// Existiert schon -> verknuepfen und updaten
|
// Existiert schon -> verknuepfen und updaten
|
||||||
|
|||||||
@@ -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 NotifyIcon _trayIcon;
|
||||||
private ContextMenuStrip _trayMenu;
|
private ContextMenuStrip _trayMenu;
|
||||||
private ListView _profileList;
|
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 StatusStrip _statusBar;
|
||||||
private ToolStripStatusLabel _statusLabel;
|
private ToolStripStatusLabel _statusLabel;
|
||||||
private Timer _autoSyncTimer;
|
private Timer _autoSyncTimer;
|
||||||
@@ -83,8 +83,8 @@ namespace StarfaceOutlookSync.UI
|
|||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
{
|
{
|
||||||
Text = "Starface Kontakt-Sync";
|
Text = "Starface Kontakt-Sync";
|
||||||
Size = new Size(830, 450);
|
Size = new Size(930, 450);
|
||||||
MinimumSize = new Size(830, 350);
|
MinimumSize = new Size(930, 350);
|
||||||
StartPosition = FormStartPosition.CenterScreen;
|
StartPosition = FormStartPosition.CenterScreen;
|
||||||
Font = new Font("Segoe UI", 9);
|
Font = new Font("Segoe UI", 9);
|
||||||
Icon = AppIcon.GetIcon();
|
Icon = AppIcon.GetIcon();
|
||||||
@@ -126,6 +126,9 @@ namespace StarfaceOutlookSync.UI
|
|||||||
_btnSync = new Button { Text = "Synchronisieren", Width = 110, Height = 30 };
|
_btnSync = new Button { Text = "Synchronisieren", Width = 110, Height = 30 };
|
||||||
_btnSync.Click += async (s, e) => await SyncSelectedProfile();
|
_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 = new Button { Text = "Sync Reset", Width = 80, Height = 30 };
|
||||||
_btnReset.Click += (s, e) => ResetSync();
|
_btnReset.Click += (s, e) => ResetSync();
|
||||||
|
|
||||||
@@ -138,7 +141,7 @@ namespace StarfaceOutlookSync.UI
|
|||||||
_btnInfo = new Button { Text = "Info", Width = 50, Height = 30 };
|
_btnInfo = new Button { Text = "Info", Width = 50, Height = 30 };
|
||||||
_btnInfo.Click += (s, e) => ShowAbout();
|
_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
|
||||||
_statusBar = new StatusStrip();
|
_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()
|
private void ExitApplication()
|
||||||
{
|
{
|
||||||
_autoSyncTimer?.Stop();
|
_autoSyncTimer?.Stop();
|
||||||
|
|||||||
Reference in New Issue
Block a user