Add field-level 3-way merge for bidirectional conflicts
Bisher wurde bei einem Konflikt (beide Seiten geaendert) der ganze Datensatz ueberschrieben - eine gleichzeitige Aenderung an einem anderen Feld ging verloren (z.B. A aendert Telefon in Outlook, B aendert Mail in Starface -> eine Aenderung weg). Jetzt: - Mapping speichert je Seite einen Snapshot des letzten Sync-Stands (LastOutlook/LastStarface), zusaetzlich zu den Hashes. - Bei beidseitiger Aenderung im Both-Modus wird feldweise gemergt (ContactMerger): unterschiedliche Felder bleiben beide erhalten, nur bei echtem Konflikt am selben Feld gewinnt Outlook. - Echte Feld-Konflikte landen in SyncResult.Conflicts und werden im MainForm per Tray-Meldung angezeigt. - Snapshots werden in allen Baseline-Punkten gesetzt (Phase 1-3) und fuer aeltere Mappings beim naechsten unveraenderten Sync nachgetragen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using StarfaceOutlookSync.Models;
|
||||
|
||||
namespace StarfaceOutlookSync.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Feldweises 3-Wege-Merge zweier Kontaktstaende gegen ihre Baseline.
|
||||
/// Erlaubt es, dass zwei Stellen unterschiedliche Felder desselben Kontakts
|
||||
/// aendern, ohne dass eine Aenderung verloren geht. Nur wenn DASSELBE Feld
|
||||
/// auf beiden Seiten geaendert wurde, greift die Vorrang-Regel.
|
||||
/// </summary>
|
||||
public static class ContactMerger
|
||||
{
|
||||
private class FieldDef
|
||||
{
|
||||
public string Label;
|
||||
public Func<UnifiedContact, string> Get;
|
||||
public Action<UnifiedContact, string> Set;
|
||||
public bool IsPhone;
|
||||
}
|
||||
|
||||
private static readonly FieldDef[] Fields = new[]
|
||||
{
|
||||
new FieldDef { Label = "Vorname", Get = c => c.FirstName, Set = (c, v) => c.FirstName = v },
|
||||
new FieldDef { Label = "Nachname", Get = c => c.LastName, Set = (c, v) => c.LastName = v },
|
||||
new FieldDef { Label = "Firma", Get = c => c.Company, Set = (c, v) => c.Company = v },
|
||||
new FieldDef { Label = "Position", Get = c => c.JobTitle, Set = (c, v) => c.JobTitle = v },
|
||||
new FieldDef { Label = "E-Mail", Get = c => c.Email, Set = (c, v) => c.Email = v },
|
||||
new FieldDef { Label = "E-Mail 2", Get = c => c.EmailSecondary, Set = (c, v) => c.EmailSecondary = v },
|
||||
new FieldDef { Label = "Telefon", Get = c => c.PhoneWork, Set = (c, v) => c.PhoneWork = v, IsPhone = true },
|
||||
new FieldDef { Label = "Mobil", Get = c => c.PhoneMobile, Set = (c, v) => c.PhoneMobile = v, IsPhone = true },
|
||||
new FieldDef { Label = "Telefon privat", Get = c => c.PhoneHome, Set = (c, v) => c.PhoneHome = v, IsPhone = true },
|
||||
new FieldDef { Label = "Fax", Get = c => c.Fax, Set = (c, v) => c.Fax = v, IsPhone = true },
|
||||
new FieldDef { Label = "Strasse", Get = c => c.Street, Set = (c, v) => c.Street = v },
|
||||
new FieldDef { Label = "Ort", Get = c => c.City, Set = (c, v) => c.City = v },
|
||||
new FieldDef { Label = "PLZ", Get = c => c.PostalCode, Set = (c, v) => c.PostalCode = v },
|
||||
new FieldDef { Label = "Bundesland", Get = c => c.State, Set = (c, v) => c.State = v },
|
||||
new FieldDef { Label = "Land", Get = c => c.Country, Set = (c, v) => c.Country = v },
|
||||
new FieldDef { Label = "Webseite", Get = c => c.Website, Set = (c, v) => c.Website = v },
|
||||
new FieldDef { Label = "Notizen", Get = c => c.Notes, Set = (c, v) => c.Notes = v },
|
||||
new FieldDef { Label = "Anrede", Get = c => c.Salutation, Set = (c, v) => c.Salutation = v },
|
||||
new FieldDef { Label = "Titel", Get = c => c.Title, Set = (c, v) => c.Title = v },
|
||||
new FieldDef { Label = "Geburtstag", Get = c => c.Birthday, Set = (c, v) => c.Birthday = v },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Fuehrt den Outlook- und Starface-Stand gegen ihre jeweilige Baseline
|
||||
/// zusammen. outlookWins entscheidet bei echten Feld-Konflikten.
|
||||
/// </summary>
|
||||
public static (UnifiedContact merged, List<FieldConflict> conflicts) Merge(
|
||||
UnifiedContact baseOutlook, UnifiedContact baseStarface,
|
||||
UnifiedContact outlook, UnifiedContact starface,
|
||||
bool outlookWins)
|
||||
{
|
||||
var merged = new UnifiedContact
|
||||
{
|
||||
OutlookEntryId = outlook.OutlookEntryId,
|
||||
StarfaceId = starface.StarfaceId
|
||||
};
|
||||
var conflicts = new List<FieldConflict>();
|
||||
|
||||
foreach (var f in Fields)
|
||||
{
|
||||
string olv = f.Get(outlook) ?? "";
|
||||
string sfv = f.Get(starface) ?? "";
|
||||
string bol = f.Get(baseOutlook) ?? "";
|
||||
string bsf = f.Get(baseStarface) ?? "";
|
||||
|
||||
bool olChanged = !Equal(olv, bol, f.IsPhone);
|
||||
bool sfChanged = !Equal(sfv, bsf, f.IsPhone);
|
||||
|
||||
string chosen;
|
||||
if (olChanged && !sfChanged)
|
||||
{
|
||||
chosen = olv;
|
||||
}
|
||||
else if (sfChanged && !olChanged)
|
||||
{
|
||||
chosen = sfv;
|
||||
}
|
||||
else if (olChanged && sfChanged)
|
||||
{
|
||||
if (Equal(olv, sfv, f.IsPhone))
|
||||
{
|
||||
// Beide auf denselben Wert geaendert -> kein echter Konflikt.
|
||||
chosen = olv;
|
||||
}
|
||||
else
|
||||
{
|
||||
chosen = outlookWins ? olv : sfv;
|
||||
conflicts.Add(new FieldConflict
|
||||
{
|
||||
StarfaceId = starface.StarfaceId,
|
||||
ContactName = outlook.DisplayName,
|
||||
Field = f.Label,
|
||||
OutlookValue = olv,
|
||||
StarfaceValue = sfv,
|
||||
Winner = outlookWins ? "Outlook" : "Starface"
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Keine Seite hat dieses Feld geaendert -> Outlook-Wert behalten.
|
||||
chosen = olv;
|
||||
}
|
||||
|
||||
f.Set(merged, chosen);
|
||||
}
|
||||
|
||||
return (merged, conflicts);
|
||||
}
|
||||
|
||||
private static bool Equal(string a, string b, bool isPhone)
|
||||
{
|
||||
if (isPhone)
|
||||
return NormalizePhone(a) == NormalizePhone(b);
|
||||
return string.Equals(a ?? "", b ?? "", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string NormalizePhone(string phone)
|
||||
{
|
||||
if (string.IsNullOrEmpty(phone)) return "";
|
||||
return new string(phone.Where(c => char.IsDigit(c) || c == '+').ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user