Files
Christoph Schwörer f045b99a25 Codebasis als Dateien ins Arbeitsrepo statt als Gitlink
QuellCode/CentronERP war nur als Gitlink (Submodul-Referenz auf 79c1142)
getrackt, ohne .gitmodules und ohne erreichbares Remote. Der
Untersuchungsgegenstand der Versuchsreihe war damit nicht reproduzierbar
gesichert: Ein Klon haette ein leeres Verzeichnis erhalten, und die Belege
der 3.287 Anforderungen waeren nicht ueberpruefbar gewesen.

Umstellung:
- Historie nach c:\DEV\CentronERP_git_snapshot_79c1142 ausgelagert
  (vollstaendig lesbar, enthaelt 79c1142 und Vorgaenger 89ccfd6)
- Gitlink aus dem Index entfernt
- Dateiinhalt aufgenommen: 24.557 Dateien, rund 333 MB

Die verschachtelte .gitignore der Codebasis gilt weiter, Build-Artefakte
bleiben ausgeschlossen. Details in Versuche/Versuch_01/_Codebasis-Nachweis.md
2026-08-26 07:43:51 +02:00

20 KiB
Raw Permalink Blame History

Exchange Sync – QS-Protokoll

Erstellt: 2026-03-10 Abgeschlossen: 2026-03-11 Status: ✅ Vollständig implementiert Bearbeiter: Entwicklung (VÖD) Branch: vöd/Exchange_Sync


Geänderte Dateien

Datei Änderungsart
src/backend/Centron.BL/Sales/Calendar/ScheduleBL.cs Bugfixes + Refactoring
src/nexus/CentronNexus/ServiceBoard/Scheduler/SchedulerPage.razor Frontend-Fix Wochenanzeige
src/centron/Centron.WPF.UI/Modules/MyCentron/CentronInspectors/Inspectors/Schedule/OrphanedSeriesScheduleInspector.cs Neu – Inspector für verwaiste Serientermine
src/centron/Centron.WPF.UI/Modules/MyCentron/CentronInspectors/Inspectors/InspectorManager.cs Inspector registriert
src/centron/Centron.WPF.UI/Modules/MyCentron/CentronInspectors/Inspectors/Schedule/TimeChangedByGraphInspector.cs Namespace-Umbenennung (Time → Schedule)
src/centron/Centron.WPF.UI/Modules/MyCentron/CentronInspectors/Inspectors/Schedule/DuplicateMailEntryIdInspector.cs Namespace-Umbenennung (Time → Schedule)

Übersicht

# Ticket Problem Schweregrad Status
1 164020 CenSU überschreibt fremde Outlook-Termine 🔴 Kritisch ✅ Implementiert
2 163184 Doppelter Inhalt in Exchange-Termineinladung 🔴 Kritisch ✅ Implementiert
3 164121 Gelöschte Serientermin-Instanzen bleiben in Nexus 🟠 Hoch ✅ Implementiert
4 160145 Ganztages-Events um +1 Tag verschoben 🟠 Hoch ✅ Implementiert
5 158813 Geplante Zeiten mit falschem Datum nach Sync 🟠 Hoch ✅ Implementiert
6 157036 Fehlerhafte Wochenanzeige in Nexus 🟡 Mittel ✅ Implementiert
7 162826 Outlook-Erinnerungsflut bei synchronisierten Zeiten 🟡 Mittel ✅ Implementiert
8 156991 Doppelte Feiertage / falsches Datum in Nexus 🟡 Mittel ✅ Implementiert
+ Zusatz Gelöschter Serienmaster löscht Instanzen nicht 🟠 Hoch ✅ Implementiert
+ Inspector Manueller Cleanup verwaister Serientermine — ✅ Implementiert

Ticket 164020

Betreff: CenSU überschreibt fremde Outlook-Termine mit alten Zeitdaten

Root Cause

SyncOldSchedule() (Nexus → Outlook) hatte keinen Filter auf den Termintyp. Auch Helpdesk-Zeiterfassungs-Schedules (ObjectType = HelpdeskTimerClass / HelpdeskClass) wurden über diesen Pfad nach Exchange geschrieben – obwohl diese bereits über CreateOrUpdateTimeSchedule() verwaltet werden. Parallel lief UpdateScheduleByGraph() (Outlook → Nexus) und schrieb Outlook-Daten in Helpdesk-Schedules zurück (Datum/Uhrzeit), was zu einem Teufelskreis führte.

Implementierter Fix

SyncOldSchedule() – Helpdesktypen ausschließen:

baseExpression = baseExpression.And(x =>
    x.ObjectType != (int)CentronObjectKindNumeric.HelpdeskTimerClass &&
    x.ObjectType != (int)CentronObjectKindNumeric.HelpdeskClass);

UpdateScheduleByGraph() – Datum-Schutz für Helpdesk-Schedules:

if (!this.IsHelpdeskSchedule(schedule))
{
    // Datum nur für reine Outlook-Termine zurückschreiben
    schedule.DateStart = this.GetDateTimeFromGraphDateTime(graphEvent.Start, localTimeZone);
    schedule.DateEnd   = this.GetDateTimeFromGraphDateTime(graphEvent.End, localTimeZone);
}

Neue Hilfsmethode IsHelpdeskSchedule():

private bool IsHelpdeskSchedule(Schedule schedule)
    => schedule.ObjectType == (int)CentronObjectKindNumeric.HelpdeskTimerClass
    || schedule.ObjectType == (int)CentronObjectKindNumeric.HelpdeskClass;

QS-Testfall

Schritt Erwartetes Ergebnis
1. Helpdeskzeit in Nexus anlegen (z.B. 11.03.2026, 09:00–10:00) Zeit wird angelegt
2. Webservice-Sync abwarten (60s) Zeit erscheint als Termin in Outlook mit korrektem Datum
3. Termin in Outlook manuell bearbeiten (Betreff ändern) —
4. Webservice-Sync abwarten Datum der Nexus-Zeit bleibt 11.03.2026, 09:00–10:00 (kein Überschreiben durch Outlook)
5. Techniker legt weiteren eigenen Outlook-Termin an Exchange-Termin bleibt unverändert – CenSU schreibt nicht in ihn

Ticket 163184

Betreff: Doppelter Inhalt in Exchange-Termineinladung (Body enthält Text 3× und altes Datum)

Root Cause

In CreateOrUpdateTimeSchedule() wurde schedule.Caption = timer.ExternalNote gesetzt, bevor ReplaceVariables(body, schedule) aufgerufen wurde. Das Body-Template enthält @@TerminText@@, welches intern schedule.Caption liest. Dadurch wurde ExternalNote doppelt in den Body eingebettet: einmal über @@TerminText@@ und einmal direkt durch das Template @@WorkOrTime@@.

Implementierter Fix

// Subject-Auflösung: Caption temporär setzen damit @@TerminText@@ im Subject funktioniert
schedule.Caption = timer.ExternalNote;
schedule.Subject = this.ReplaceVariables(subject, schedule);

// Body-Auflösung: Caption leeren damit @@TerminText@@ im Body leer ist
schedule.Caption = string.Empty;
schedule.Caption = this.ReplaceVariables(body, schedule);

QS-Testfall

Schritt Erwartetes Ergebnis
1. Helpdeskzeit mit externem Hinweis anlegen (z.B. "Vor-Ort-Einsatz Drucker") Zeit wird gespeichert
2. Sync abwarten Outlook-Termin wird erstellt
3. Outlook-Termin öffnen, Body prüfen Body enthält "Vor-Ort-Einsatz Drucker" exakt einmal
4. Nochmals Zeit speichern (Update) Termin wird aktualisiert, Body enthält Text weiterhin nur einmal

Ticket 164121

Betreff: Gelöschte Serientermin-Instanzen bleiben in Nexus sichtbar

Root Cause

Microsoft Graph sendet bei gelöschten Serientermin-Instanzen kein @removed-Flag, sondern setzt isCancelled = true. Der bisherige Code prüfte nur @removed → gelöschte Instanzen wurden nie in Nexus als inaktiv markiert.

Implementierter Fix

StoreEvent() – isCancelled gleichwertig zu @removed behandeln:

bool isRemoved = graphEvent.AdditionalData.ContainsKey("@removed")
              || graphEvent.IsCancelled is true;

QS-Testfall

Schritt Erwartetes Ergebnis
1. In Outlook einen Serientermin anlegen (z.B. täglich, 5 Instanzen) Alle 5 Instanzen erscheinen in Nexus nach Sync
2. Eine einzelne Instanz in Outlook löschen ("Nur diesen Termin") —
3. Sync abwarten Die gelöschte Instanz verschwindet aus Nexus
4. Verbleibende Instanzen prüfen Übrige 4 Instanzen bleiben unverändert sichtbar

Ticket 160145

Betreff: Ganztages-Events in Nexus um +1 Tag verschoben

Root Cause

Microsoft Graph liefert Ganztages-Events mit Start.TimeZone = "UTC" und einem exklusiven Enddatum (End = nächster Tag 00:00 UTC). Die bisherige UTC→CET-Konvertierung verschob das Datum: 2025-08-16T00:00 UTC → 2025-08-16T02:00 CEST. Nexus interpretierte den 16.08 als Enddatum → Termin erschien auf dem falschen Tag.

Implementierter Fix

InsertScheduleByGraph() und UpdateScheduleByGraph() – neue ParseAllDayEventDates()-Hilfsmethode:

if (graphEvent.IsAllDay == true)
    (schedule.DateStart, schedule.DateEnd) = ParseAllDayEventDates(graphEvent);
private static (DateTime Start, DateTime End) ParseAllDayEventDates(Event graphEvent)
{
    var start = DateTime.Parse(graphEvent.Start.DateTime).Date;
    var end   = DateTime.Parse(graphEvent.End.DateTime).Date.AddDays(-1); // exklusiv → inklusiv
    return (start, end);
}

Zusätzlich: IANA → Windows Timezone-Mapping in GetDateTimeFromGraphDateTime():

// "Europe/Berlin" wird korrekt auf "W. Europe Standard Time" gemappt
if (TimeZoneInfo.TryConvertIanaIdToWindowsId(graphDateTime.TimeZone, out var windowsId))
    graphTimeZone = TimeZoneInfo.FindSystemTimeZoneById(windowsId);

QS-Testfall

Schritt Erwartetes Ergebnis
1. In Outlook einen ganztägigen Termin am 15.08.2026 anlegen Termin in Outlook korrekt am 15.08
2. Sync abwarten Termin in Nexus erscheint am 15.08.2026 (nicht 16.08)
3. Termin in Outlook auf 10.–12.08.2026 (3 Tage) ändern —
4. Sync abwarten Nexus zeigt Termin vom 10.08. bis 12.08.

Ticket 158813

Betreff: Geplante Zeiten erhalten nach Sync falsches Datum

Root Cause

UpdateScheduleByGraph() überschrieb DateStart/DateEnd von Helpdesk-Schedules mit den aus Exchange zurückgegeben Werten (Round-Trip-Problem). Zusätzlich: IANA-Zeitzonennamen ("Europe/Berlin") wurden nicht als Windows-Zeitzone erkannt → unnötige Konvertierung, mögliche 1h-Abweichung je Sommerzeit.

Implementierter Fix

Beide Ursachen behoben:

  1. Helpdesk-Datum-Schutz (identisch zu Ticket 164020) – UpdateScheduleByGraph() überschreibt Datum bei Helpdesk-Schedules nicht mehr.

  2. Timezone-Mapping (identisch zu Ticket 160145) – IANA → Windows-ID, HasSameRules()-Prüfung verhindert unnötige Konvertierungen bei gleicher Zeitzone.

QS-Testfall

Schritt Erwartetes Ergebnis
1. In Nexus eine Planung anlegen: 02.03.2026, 08:00–09:00 Nexus zeigt 02.03.2026
2. Sync abwarten (Nexus → Outlook) Outlook zeigt Termin am 02.03.2026 korrekt
3. Erneuten Sync abwarten (Outlook → Nexus, Delta-Rückweg) Nexus zeigt weiterhin 02.03.2026 (kein Überschreiben)
4. Prüfen ob Uhrzeit korrekt bleibt 08:00–09:00 unverändert

Ticket 157036

Betreff: Fehlerhafte Wochenanzeige in Nexus (Arbeitswoche zeigt nur Freitag)

Root Cause

Der DxSchedulerWorkWeekView in SchedulerPage.razor hatte kein WorkDays-Attribut. Ohne diese Angabe verwendet DevExpress einen internen Default, der in dieser Version nur den letzten Werktag (Freitag) anzeigte.

Implementierter Fix

SchedulerPage.razor – WorkDays-Attribut ergänzt:

<DxSchedulerWorkWeekView VisibleTime="VisibleTime"
                         WorkTime="_workTime"
                         TimeScale="WeekViewTimeScale"
                         WorkDays="DayOfWeek.Monday | DayOfWeek.Tuesday | DayOfWeek.Wednesday | DayOfWeek.Thursday | DayOfWeek.Friday">

QS-Testfall

Schritt Erwartetes Ergebnis
1. Nexus öffnen → Zeitplanung → Ansicht "Arbeitswoche" wählen Alle 5 Werktage (Mo–Fr) werden angezeigt
2. Termin am Mittwoch prüfen Termin erscheint korrekt auf Mittwoch
3. Ansicht wechseln (Woche, Monat) und zurück zu Arbeitswoche Arbeitswoche zeigt weiterhin Mo–Fr

Ticket 162826

Betreff: Outlook-Erinnerungsflut bei synchronisierten Helpdeskzeiten

Root Cause

UpdateTerminplanungPersonTable() → AddScheduleToExchange() setzte isReminderOn nicht explizit auf false für Helpdesk-Zeiten. Outlook aktiviert standardmäßig eine 15-Minuten-Erinnerung für alle neuen Termine.

Implementierter Fix

UpdateTerminplanungPersonTable() – Reminder für Helpdesk-Schedules deaktivieren:

bool? reminderOn = this.IsHelpdeskSchedule(schedule) ? false : (bool?)null;
var result = await this.AddScheduleToExchange(schedule, schedulePerson, isReminderOn: reminderOn);

null bedeutet: Reminder-Einstellung nicht ändern (Standard-Outlook-Verhalten für normale Termine).

QS-Testfall

Schritt Erwartetes Ergebnis
1. Helpdeskzeit speichern → Sync abwarten Termin in Outlook erscheint
2. Outlook-Termin öffnen → Eigenschaften prüfen Feld "Erinnerung" ist deaktiviert (kein Häkchen)
3. Normalen Nexus-Kalendertermin anlegen → Sync Erinnerung bleibt auf Standard (15 Min.)

Ticket 156991

Betreff: Doppelte Feiertage / falsches Datum in Nexus

Root Cause

A) Datumsversatz: Identisch zu Ticket 160145 (UTC-Konvertierungsfehler bei IsAllDay = true).

B) Duplikate: Wenn derselbe Feiertag in mehreren Exchange-Kalendern (z.B. persönlicher Kalender + Firmenkalender) enthalten ist, liefert Graph Delta denselben Feiertag mit unterschiedlichen MailEntryIDs. Der bisherige Code prüfte nur auf MailEntryID-Duplikate → zwei identische Schedules wurden angelegt.

Implementierter Fix

A) UTC-Fix identisch zu Ticket 160145 über ParseAllDayEventDates().

B) Duplikat-Prüfung in StoreEvent() für Ganztages-Events:

if (graphEvent.IsAllDay == true && graphEvent.Subject is not null)
{
    var (allDayStart, _) = ParseAllDayEventDates(graphEvent);
    var existingAllDay = GetSchedulesByExpression(x =>
        x.Subject == graphEvent.Subject &&
        x.DateStart == allDayStart &&
        x.FullDay == true &&
        x.IsActive);

    if (existingAllDay.Any(s => GetSchedulePersonsByFilter(
            new SchedulePersonsFilter { ScheduleI3D = s.I3D })
        .Any(sp => sp.PersonalI3D == employee.I3D)))
        return; // Bereits vorhanden → kein Duplikat anlegen
}

QS-Testfall

Schritt Erwartetes Ergebnis
1. In Outlook einen Feiertag prüfen (z.B. Ostersonntag 05.04.2026) —
2. Sync abwarten Ostersonntag erscheint in Nexus genau einmal am 05.04.2026
3. Weiteren ganztägigen Termin am selben Tag mit gleichem Betreff anlegen (zweiter Kalender) —
4. Sync abwarten Kein zweiter Eintrag – Duplikat wird erkannt und übersprungen

Zusatz: Serientermin-Kaskade

Problem: Wenn ein kompletter Serienmaster in Exchange gelöscht wird, wurden die zugehörigen Instanz-Schedules in Nexus nicht mitgelöscht. Graph sendet beim Master-Delete nicht immer einzelne @removed-Events für alle Instanzen.

Implementierter Fix

StoreEvent() – Kaskade bei Master-Delete:

if (isRemoved && graphEvent.SeriesMasterId is null) // SeriesMasterId null = dieser Event IST der Master
    await this.DeleteSeriesInstancesByMasterId(graphEvent.Id, employee, cenSU);

Neue Methode DeleteSeriesInstancesByMasterId():

  • Sucht alle SchedulePerson-Einträge mit SeriesMasterId = masterEventId für den Mitarbeiter
  • Lädt nur noch aktive Schedules (IsActive = true)
  • Setzt diese auf IsActive = false via SaveSchedule() (korrekte BL-Kette inkl. Exchange-Löschung)

QS-Testfall

Schritt Erwartetes Ergebnis
1. In Outlook Serientermin anlegen (z.B. täglich, 7 Instanzen) Alle 7 Instanzen erscheinen in Nexus
2. Gesamte Serie in Outlook löschen ("Alle Termine der Serie") —
3. Sync abwarten Alle 7 Nexus-Instanzen werden als inaktiv markiert und verschwinden

Zusatz: Inspector für verwaiste Serientermine

Problem: Historisch bereits entstandene verwaiste Serientermine (vor Einführung des Kaskaden-Fix) müssen manuell bereinigt werden können.

Implementierter Fix

Neuer Inspector "Verwaiste Serientermine (Exchange Sync)" im c-entron WPF-Client unter MyCentron → c-entron Inspektor:

  • Check: Findet alle aktiven Schedules mit SeriesMasterId, für die kein aktiver Serienmaster-Schedule mehr in der DB existiert
  • Repair: Deaktiviert alle gefundenen verwaisten Instanzen per Knopfdruck
  • Kategorie: Zeiten (alphabetisch sortiert)

QS-Testfall

Schritt Erwartetes Ergebnis
1. c-entron WPF öffnen → MyCentron → Inspektor Inspector-Liste erscheint
2. "Verwaiste Serientermine (Exchange Sync)" auswählen und Inspektor ausführen Check läuft durch
3a. Keine verwaisten Daten vorhanden Grünes Häkchen: "Es wurden keine verwaisten Serientermine gefunden."
3b. Verwaiste Daten vorhanden Rotes X mit Anzahl + Reparieren-Button
4. Reparieren klicken Instanzen werden deaktiviert, Check läuft erneut → grünes Häkchen

Technische Grundlagen

Sync-Architektur

ExchangeSyncService (HostedService, Intervall konfigurierbar, default 60s)
 ├── ScheduleBL.SyncByGraphV2()          ← Outlook → Nexus (Graph API Delta)
 └── ScheduleBL.SyncOldSchedule()        ← Nexus → Outlook (Schedules ohne MailEntryID)

HelpdeskTimerBL / ScheduleBL
 ├── CreateOrUpdateTimeSchedule()        ← Zeiterfassung → Outlook-Termin
 └── UpdateTerminplanungPersonTable()    ← Termin gespeichert → Exchange aktualisieren

Schlüssel-Entitäten

Entity Beschreibung
Schedule Termin in c-entron (DateStart, DateEnd, FullDay, Subject, IsActive, ObjectType)
SchedulePerson Verknüpfung Schedule ↔ Mitarbeiter ↔ Exchange (MailEntryID, SeriesMasterId)
CreatedByApp Herkunft: GraphSync = 6, HelpdeskTime, GraphSyncTimeDelete
CenSU CentronSystemUser – Systemkonto für alle Hintergrundprozesse
IsHelpdeskSchedule() Neue Hilfsmethode: prüft ob Schedule an Helpdesk/Zeit gebunden ist

Source of Truth

Schedule-Typ Datum/Zeit Source of Truth
HelpdeskTimerClass / HelpdeskClass Nexus – Exchange darf nie überschreiben
Reine Outlook-Termine (GraphSync) Exchange/Outlook
Ganztages-Events (FullDay = true) Datumswert aus Graph, ohne Zeitzonenkonvertierung

Bekannte Einschränkungen / Nicht umgesetzt

Thema Begründung
Kalenderfilter (welche Exchange-Kalender synchronisiert werden) Konfigurationsthema, kein Code-Fix – muss über Exchange-Einstellungen gesteuert werden
Blocker-Termine (ShowAs = Free) in Nexus anzeigen Kein Kundenauftrag für diese Version
ApplicationSetting für Erinnerungen (An/Aus im UI) Für Helpdesk-Zeiten ist isReminderOn: false als Standard ausreichend

Offene Fragen (für Rollout-Planung)

  • Gibt es Kunden mit Exchange On-Premise (kein Graph API)? → Ja. Diese Kunden nutzen den alten EWS-basierten Agent. Für sie greifen die Graph-spezifischen Fixes nicht (Delta-Token, isCancelled, ParseAllDayEventDates, Kaskaden-Delete, Duplikat-Erkennung). Separate Analyse für On-Premise-Kunden erforderlich – ggf. eigener Bugfix-Zyklus.
  • Welche Exchange-Kalender werden synchronisiert – nur Primärkalender? → Alle Kalender. Der Endpunkt _graphClient.Users[id].CalendarView.Delta liefert Events aus allen Kalendern des Nutzers aggregiert (Primär + abonnierte Kalender wie „Feiertage in Deutschland", Teamkalender etc.). Das erklärt u.a. den Doppel-Feiertage-Bug (Ticket 156991). So gewollt – bleibt so.
  • Sollen Kunden nach dem Update den Inspector manuell ausführen (Kommunikation nötig)? → Ja. In den Release Notes darauf hinweisen, dass Kunden mit Exchange Sync nach dem Update den Inspector „Verwaiste Serientermine (Exchange Sync)" einmalig ausführen sollen, um historisch entstandene verwaiste Serientermine zu bereinigen.
  • Ist der Inspector nur für Admins sichtbar (Rechteprüfung)? → Zugriff wird über das Modul „c-entron Inspektor" gesteuert – keine separate Rechteprüfung im Inspector selbst erforderlich.

Geltungsbereich der Fixes

Fix Exchange Online (Graph API) Exchange On-Premise (alter EWS-Agent)
Ticket 164020 – CenSU überschreibt Helpdesk-Zeiten ✅ ⚠️ Teilweise: SyncOldSchedule()-Filter greift, EWS-Rückweg unklar
Ticket 163184 – Doppelter Body ✅ ✅ (CreateOrUpdateTimeSchedule() ist gemeinsamer Pfad)
Ticket 164121 – Instanz-Delete via isCancelled ✅ ❌ EWS-Agent nutzt anderen Delete-Mechanismus
Ticket 160145 – Ganztages +1 Tag ✅ ❌ Graph-spezifisch
Ticket 158813 – Falsches Datum nach Sync ✅ ⚠️ Helpdesk-Schutz greift, Timezone-Fix ist Graph-spezifisch
Ticket 157036 – Wochenanzeige Nexus ✅ ✅ (Frontend-Fix, kein Exchange-Bezug)
Ticket 162826 – Erinnerungsflut ✅ ✅ (AddScheduleToExchange() gemeinsamer Pfad)
Ticket 156991 – Doppelte Feiertage ✅ ❌ Graph Delta-spezifisch
Zusatz – Serienkaskade ✅ ❌ Graph Delta-spezifisch
Inspector – Cleanup verwaiste Serientermine ✅ ✅ (DB-Level, unabhängig vom Sync-Pfad)