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
20 KiB
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:
-
Helpdesk-Datum-Schutz (identisch zu Ticket 164020) –
UpdateScheduleByGraph()überschreibt Datum bei Helpdesk-Schedules nicht mehr. -
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 mitSeriesMasterId = masterEventIdfür den Mitarbeiter - Lädt nur noch aktive Schedules (
IsActive = true) - Setzt diese auf
IsActive = falseviaSaveSchedule()(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.Deltaliefert 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) |