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
This commit is contained in:
Christoph Schwörer
2026-08-26 07:43:51 +02:00
parent 18edae75b6
commit f045b99a25
24664 changed files with 5846716 additions and 1 deletions
@@ -0,0 +1,720 @@
# Automatic Helpdesk Creation Templates
**Feature Branch**: `ral/T-162548-vorlage-autom-ticket-erstellung`
**Implementation Date**: 2026-02-12
**Status**: ✅ Completed
## Overview
This feature implements a comprehensive template system for automatic helpdesk ticket creation from orders. Users can now create, save, and manage multiple preset configurations for ticket creation, with one template designated as the standard default.
## Table of Contents
1. [Features](#features)
2. [User Interface](#user-interface)
3. [Technical Implementation](#technical-implementation)
4. [Database Schema](#database-schema)
5. [Architecture](#architecture)
6. [Usage Guide](#usage-guide)
7. [Configuration](#configuration)
---
## Features
### Core Functionality
- ✅ **Template Management**: Create, save, load, update, and delete ticket creation templates
- ✅ **Standard Template**: Designate one template as the standard (only one at a time)
- ✅ **Auto-Load**: Automatically load the last selected template when opening the UI
- ✅ **Template Persistence**: Selected template is saved and restored across sessions
- ✅ **Field Preservation**: Form fields remain unchanged during template operations
- ✅ **Visual Indicators**: Standard templates are displayed in bold with a checkmark icon
- ✅ **Template Updates**: Automatically update templates when saving settings
### New Fields Added
1. **Category (Kategorie)**: Main category for helpdesk tickets
2. **CreateSeparateTicketsMode**: Choice between Single, Group, or Custom ticket creation
3. **OpenAfterwards**: Whether to open tickets after creation
4. **CreateTicketForAll**: Create tickets for all order positions or only those without tickets
---
## User Interface
### Template Dropdown
**Location**: `Belege → Einstellungen → Bestellung → Automatische Helpdeskfallerstellung → Vorlagen`
**Components**:
- ComboBox with template selection (shows "Keine Vorlage ausgewählt" when none selected)
- Standard templates display with:
- ✓ Checkmark icon
- **Bold text**
**Actions**:
- Three action buttons:
1. **Als Vorlage speichern**: Save current form values as a new template
2. **Als Standard setzen**: Set selected template as the standard
3. **Vorlage löschen**: Delete selected template
### Template Workflow
```
┌─────────────────────────────────────────────────────────────┐
│ 1. User opens UI │
│ → Last selected template is loaded (if exists) │
│ → Or standard template is shown (if no previous selection)│
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 2. User selects a template │
│ → All form fields are populated with template values │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 3. User modifies fields │
│ → Form values change │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 4. User clicks "Speichern" │
│ → ApplicationSettings are saved │
│ → Selected template is updated with new values │
│ → Template selection is persisted │
└─────────────────────────────────────────────────────────────┘
```
### Field Clearing
When "Keine" (none) is selected in the template dropdown:
- All form fields are cleared to default/empty values
- User can manually fill in values
- Clicking "Speichern" saves only to ApplicationSettings (no template update)
---
## Technical Implementation
### Backend Components
#### 1. Database Script
**File**: `ScriptMethod11751.cs`
Creates the `HelpdeskCreationTemplate` table with:
- All template configuration fields
- Standard tracking columns (CreatedByI3D, CreatedDate, ChangedByI3D, ChangedDate, IsDeleted, DeletedByI3D, DeletedDate)
- Indexes on `IsStandard` and `IsDeleted` for performance
```csharp
ScriptHelpers.AddTableIfNotExists("HelpdeskCreationTemplate",
("Name", "nvarchar(255)", false, null),
("IsStandard", "bit", false, "0"),
("TypeI3D", "int", true, null),
("CategoryI3D", "int", true, null),
// ... additional fields
);
```
#### 2. Entity Class
**File**: `HelpdeskCreationTemplate.cs`
```csharp
public class HelpdeskCreationTemplate : BaseEntity
{
public virtual string Name { get; set; }
public virtual bool IsStandard { get; set; }
public virtual int? TypeI3D { get; set; }
public virtual int? CategoryI3D { get; set; }
// ... additional properties
}
```
#### 3. NHibernate Mapping
**File**: `HelpdeskCreationTemplateMaps.cs`
FluentNHibernate mapping with proper nullability and column lengths.
#### 4. Data Transfer Object
**File**: `HelpdeskCreationTemplateDTO.cs`
```csharp
[DataContract]
public class HelpdeskCreationTemplateDTO
{
[DataMember]
public int I3D { get; set; }
[DataMember]
public string Name { get; set; }
[DataMember]
public bool IsStandard { get; set; }
// ... additional properties
}
```
#### 5. Business Logic
**File**: `HelpdeskCreationTemplateBL.cs`
Key methods:
- `GetTemplateByI3D(int i3D)`: Retrieve template by ID
- `GetAllTemplates()`: Get all active templates
- `GetStandardTemplate()`: Get the standard template
- `SaveTemplate(entity, currentUserI3D)`: Create or update template
- `DeleteTemplate(templateI3D, currentUserI3D)`: Soft delete template
- `SetStandardTemplate(templateI3D, currentUserI3D)`: Set as standard (ensures only one)
**Business Rules**:
- Only one template can be marked as standard at a time
- Cannot delete the standard template
- Soft delete pattern (sets `IsDeleted = true`)
#### 6. Web Service Layer
**File**: `HelpdeskCreationTemplateWebServiceBL.cs`
Handles DTO ↔ Entity conversion and delegates to BL.
#### 7. Logic Interfaces
**Files**: `ITicketLogic.cs`, `BLTicketLogic.cs`, `WSTicketLogic.cs`
Added methods:
```csharp
Task<Result<List<HelpdeskCreationTemplateDTO>>> GetAllHelpdeskCreationTemplates();
Task<Result<HelpdeskCreationTemplateDTO>> GetHelpdeskCreationTemplateByI3D(int templateI3D);
Task<Result<HelpdeskCreationTemplateDTO>> GetStandardHelpdeskCreationTemplate();
Task<Result<HelpdeskCreationTemplateDTO>> SaveHelpdeskCreationTemplate(HelpdeskCreationTemplateDTO template);
Task<Result> DeleteHelpdeskCreationTemplate(int templateI3D);
Task<Result> SetStandardHelpdeskCreationTemplate(int templateI3D);
```
### Frontend Components
#### 1. ViewModel
**File**: `AutoHelpdeskCreationOrderSettingsViewModel.cs`
**Properties**:
```csharp
public ObservableCollection<HelpdeskCreationTemplateDTO> Templates { get; set; }
public HelpdeskCreationTemplateDTO SelectedTemplate { get; set; }
private bool _isLoadingTemplate; // Flag to prevent unwanted form reloads
```
**Commands**:
- `SaveTemplateCommand`: Save current form values as a new template
- `DeleteTemplateCommand`: Delete selected template
- `SetStandardTemplateCommand`: Set selected template as standard
**Key Methods**:
- `LoadTemplates()`: Load all templates from database
- `LoadTemplateIntoForm(template)`: Populate form fields from template
- `SaveAsTemplate(name, isStandard)`: Create new template from form values
- `ClearAllFields()`: Clear all form fields when "Keine" is selected
- `SaveSettings(settings)`: Save to ApplicationSettings and update selected template
**Important Implementation Details**:
1. **Field Preservation**: Uses `_isLoadingTemplate` flag to prevent `LoadTemplateIntoForm()` or `ClearAllFields()` from being called during template operations.
2. **Template Updates on Save**: When saving settings with a template selected, automatically updates the template with current form values.
3. **Template Selection Persistence**:
- On load: Retrieves saved template ID from ApplicationSettings and selects it
- On save: Persists currently selected template ID
#### 2. View (XAML)
**File**: `AutoHelpdeskCreationOrderSettingsView.xaml`
**Template Section**:
```xml
<dxlc:LayoutGroup Header="Vorlagen" View="GroupBox" Orientation="Vertical">
<dxlc:LayoutItem Label="Vorlage: ">
<dxe:ComboBoxEdit ItemsSource="{Binding Templates}"
SelectedItem="{Binding SelectedTemplate}"
DisplayMember="Name"
NullText="(Keine Vorlage ausgewählt)">
<dxe:ComboBoxEdit.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<!-- Checkmark icon for standard template -->
<Image Visibility="{Binding IsStandard, Converter={...}}"/>
<!-- Template name (bold if standard) -->
<TextBlock Text="{Binding Name}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding IsStandard}" Value="True">
<Setter Property="FontWeight" Value="Bold"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</DataTemplate>
</dxe:ComboBoxEdit.ItemTemplate>
</dxe:ComboBoxEdit>
</dxlc:LayoutItem>
<!-- Action buttons -->
<dxlc:LayoutItem>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<dx:SimpleButton Content="Als Vorlage speichern"
Command="{Binding SaveTemplateCommand}"/>
<dx:SimpleButton Content="Als Standard setzen"
Command="{Binding SetStandardTemplateCommand}"
IsEnabled="{Binding SelectedTemplate, ...}"/>
<dx:SimpleButton Content="Vorlage löschen"
Command="{Binding DeleteTemplateCommand}"
IsEnabled="{Binding SelectedTemplate, ...}"/>
</StackPanel>
</dxlc:LayoutItem>
</dxlc:LayoutGroup>
```
**New Fields Section**:
```xml
<dxlc:LayoutGroup Header="Aktionen einstellung" View="GroupBox">
<!-- CreateTicketForAll checkbox -->
<dxe:CheckEdit Content="Für jeden Artikel ein Ticket erstellen"
EditValue="{Binding CreateTicketForAll}"/>
<!-- SendEmailToProcessor checkbox -->
<dxe:CheckEdit Content="E-Mail an Bearbeiter"
EditValue="{Binding SendEmailToProcessor}"/>
<!-- OpenAfterwards checkbox -->
<dxe:CheckEdit Content="Tickets nach Erstellung öffnen"
EditValue="{Binding OpenAfterwards}"/>
<!-- CreateSeparateTicketsMode icon selection -->
<dxlc:LayoutItem Label="Ticket einzeln erstellen: ">
<StackPanel Orientation="Horizontal">
<!-- Yes/Single option -->
<dxe:CheckEdit IsChecked="{Binding CreateSeparateTicketsMode,
Converter={StaticResource EnumToBooleanConverter},
ConverterParameter={x:Static createTickets:CreateTicketsMode.Single}}">
<dxe:CheckEdit.Content>
<StackPanel Orientation="Vertical">
<Image Source="{dx:DXImageOffice2013 Image=Apply_32x32.png}"/>
<TextBlock Text="Ja"/>
</StackPanel>
</dxe:CheckEdit.Content>
</dxe:CheckEdit>
<!-- No/Group option -->
<dxe:CheckEdit IsChecked="{Binding CreateSeparateTicketsMode,
Converter={StaticResource EnumToBooleanConverter},
ConverterParameter={x:Static createTickets:CreateTicketsMode.Group}}">
<dxe:CheckEdit.Content>
<StackPanel Orientation="Vertical">
<Image Source="{dx:DXImageOffice2013 Image=Cancel_32x32.png}"/>
<TextBlock Text="Nein"/>
</StackPanel>
</dxe:CheckEdit.Content>
</dxe:CheckEdit>
<!-- Custom option -->
<dxe:CheckEdit IsChecked="{Binding CreateSeparateTicketsMode,
Converter={StaticResource EnumToBooleanConverter},
ConverterParameter={x:Static createTickets:CreateTicketsMode.Custom}}">
<dxe:CheckEdit.Content>
<StackPanel Orientation="Vertical">
<Image Source="{dx:DXImageOffice2013 Image=Properties_32x32.png}"/>
<TextBlock Text="Eigen"/>
</StackPanel>
</dxe:CheckEdit.Content>
</dxe:CheckEdit>
</StackPanel>
</dxlc:LayoutItem>
</dxlc:LayoutGroup>
<dxlc:LayoutGroup Header="Fallbackwerte" View="GroupBox">
<!-- Category dropdown -->
<dxlc:LayoutItem Label="Kategorie: ">
<dxe:ComboBoxEdit ItemsSource="{Binding HelpdeskCategories}"
SelectedItem="{Binding SelectedMainCategory}"
DisplayMember="Name"/>
</dxlc:LayoutItem>
<!-- Existing fields: Type, Bearbeiter, Priorität, etc. -->
</dxlc:LayoutGroup>
```
### ApplicationSettings
#### New Settings Added
| ID | Name | Type | Description |
|---|---|---|---|
| 10441 | `AutomHelpdeskCategoryI3D` | int | Main category I3D for automatic helpdesk creation |
| 10442 | `AutomHelpdeskCreateSeparateTicketsMode` | int | Create separate tickets mode (0=Single, 1=Group, 2=Custom) |
| 10443 | `AutomHelpdeskOpenAfterwards` | bool | Whether to open tickets after creation |
| 10444 | `AutomHelpdeskCreateTicketForAll` | bool | Create tickets for all positions or only those without tickets |
| 10445 | `AutomHelpdeskSelectedTemplateI3D` | int | I3D of selected template (0 = no template) |
#### Files Modified
1. **ApplicationSettingID.cs**: Added enum entries with XML documentation
2. **ApplicationSettingDefinitions.cs**: Added English descriptions
3. **ReceiptSettingsDTO.cs**: Added properties with `[DataMember]` attributes
4. **ReceiptWebServiceBL.cs**: Added load/save logic in four locations:
- Settings load list
- Settings load logic
- Settings update list
- Settings update logic
---
## Database Schema
### Table: `HelpdeskCreationTemplate`
| Column | Type | Nullable | Description |
|--------|------|----------|-------------|
| `I3D` | int | No | Primary key (IDENTITY) |
| `Name` | nvarchar(255) | No | Template name |
| `IsStandard` | bit | No | Whether this is the standard template |
| `TypeI3D` | int | Yes | Helpdesk type I3D |
| `CategoryI3D` | int | Yes | Main category I3D |
| `SubCategory1I3D` | int | Yes | Sub-category 1 I3D |
| `SubCategory2I3D` | int | Yes | Sub-category 2 I3D |
| `PriorityI3D` | int | Yes | Priority I3D |
| `StatusI3D` | int | Yes | Status I3D |
| `ProcessorI3D` | int | Yes | Processor (Bearbeiter) employee I3D |
| `ResponsiblePersonI3D` | int | Yes | Responsible person employee I3D |
| `AdditionalText1` | nvarchar(max) | Yes | Additional text 1 |
| `AdditionalText2` | nvarchar(max) | Yes | Additional text 2 |
| `SendEmailToProcessor` | bit | No | Send email to processor flag |
| `CreateTicketForAll` | bit | No | Create ticket for all positions flag |
| `CreateSeparateTicketsMode` | int | No | Separate tickets mode (0/1/2) |
| `OpenAfterwards` | bit | No | Open after creation flag |
| `OnlyInternal` | bit | No | Only internal visibility flag |
| `CreatedByI3D` | int | Yes | Created by user I3D |
| `CreatedDate` | datetime2(2) | Yes | Creation timestamp |
| `ChangedByI3D` | int | Yes | Last modified by user I3D |
| `ChangedDate` | datetime2(2) | Yes | Last modification timestamp |
| `IsDeleted` | bit | No | Soft delete flag |
| `DeletedByI3D` | int | Yes | Deleted by user I3D |
| `DeletedDate` | datetime2(2) | Yes | Deletion timestamp |
**Indexes**:
- Primary key on `I3D`
- Index on `IsStandard` (performance optimization for finding standard template)
- Index on `IsDeleted` (performance optimization for filtering active templates)
---
## Architecture
### Layered Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ WPF UI Layer │
│ AutoHelpdeskCreationOrderSettingsView (XAML) │
│ AutoHelpdeskCreationOrderSettingsViewModel (C#) │
└─────────────────────────────────────────────────────────────┘
↓
ClassContainer.Instance
↓
┌─────────────────────────────────────────────────────────────┐
│ Logic Interface Layer │
│ ITicketLogic (Interface) │
│ ├─ BLTicketLogic (SQL Connection) │
│ └─ WSTicketLogic (REST API Connection) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Web Service Business Logic │
│ HelpdeskCreationTemplateWebServiceBL │
│ - DTO ↔ Entity conversion │
│ - Delegates to BL │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Business Logic Layer │
│ HelpdeskCreationTemplateBL │
│ - Business rules enforcement │
│ - Data validation │
│ - Standard template management │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Data Access Layer │
│ DAOSession / NHibernate │
│ - HelpdeskCreationTemplateMaps (FluentNHibernate) │
│ - HelpdeskCreationTemplate (Entity) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Database │
│ SQL Server 2019+ │
│ - HelpdeskCreationTemplate table │
└─────────────────────────────────────────────────────────────┘
```
### Design Patterns Used
1. **MVVM (Model-View-ViewModel)**: Separation of UI and business logic
2. **Result Pattern**: `Result<T>` for error handling
3. **Repository Pattern**: DAO layer with NHibernate
4. **DTO Pattern**: Data transfer between layers
5. **Command Pattern**: DelegateCommand for UI actions
6. **Soft Delete Pattern**: `IsDeleted` flag instead of physical deletion
7. **Singleton Pattern**: ClassContainer for dependency injection
8. **Observer Pattern**: INotifyPropertyChanged for data binding
---
## Usage Guide
### Creating a New Template
1. Fill in all desired values in the form
2. Click **"Als Vorlage speichern"**
3. Enter a template name
4. Choose whether to set it as standard
5. Click OK
6. Template is saved and automatically selected
### Loading a Template
1. Select template from dropdown
2. All form fields populate with template values
3. Modify fields as needed
4. Click **"Speichern"** to save changes (updates both ApplicationSettings AND the template)
### Setting a Template as Standard
1. Select template from dropdown
2. Click **"Als Standard setzen"**
3. Template is marked as standard (only one template can be standard)
4. Standard template displays with checkmark icon and bold text
### Deleting a Template
1. Select template from dropdown
2. Click **"Vorlage löschen"**
3. Confirm deletion
4. Template is soft-deleted (marked as `IsDeleted = true`)
5. **Note**: Cannot delete the standard template
### Updating a Template
1. Select template from dropdown
2. Modify form fields
3. Click **"Speichern"** (main save button)
4. Template is automatically updated with new values
### Clearing Fields
1. Select **"Keine"** from the dropdown
2. All form fields are cleared
3. Manually enter values
4. Click **"Speichern"** to save only to ApplicationSettings (no template)
---
## Configuration
### Dialog Settings
All dialogs use the codebase standard pattern:
```csharp
// Input dialog
var result = await CentronApplication.Instance.DialogManager.ShowInputDialog(
title,
new InputDialogOptions { ... }
);
// Confirmation dialog
await CentronApplication.Instance.DialogManager.ShowConfirmationDialog(
message,
title
);
// Choice dialog
var choice = await CentronApplication.Instance.DialogManager.ShowDialog(
message,
title,
"Button1",
"Button2"
);
```
### Enum Values
#### CreateTicketsMode
```csharp
public enum CreateTicketsMode
{
Single = 0, // Create separate ticket for each position
Group = 1, // Create one ticket for all positions
Custom = 2 // User selects which positions get tickets
}
```
---
## Testing Scenarios
### Test Case 1: First Time User
- **Given**: User opens settings for the first time
- **When**: UI loads
- **Then**: No template selected, fields show ApplicationSettings values
### Test Case 2: Saved Template Exists
- **Given**: User previously selected template ID 5
- **When**: UI loads
- **Then**: Template ID 5 is selected in dropdown, fields show ApplicationSettings values
### Test Case 3: Saved Template Deleted
- **Given**: User previously selected template ID 5, but it was deleted
- **When**: UI loads
- **Then**: Dropdown shows "Keine", fields show ApplicationSettings values
### Test Case 4: Create Template
- **Given**: User fills form with values
- **When**: User clicks "Als Vorlage speichern" and enters name
- **Then**: New template created, automatically selected, fields unchanged
### Test Case 5: Update Template
- **Given**: User has template selected, modifies fields
- **When**: User clicks "Speichern"
- **Then**: ApplicationSettings saved, template updated, fields unchanged, template stays selected
### Test Case 6: Delete Template
- **Given**: User has template selected
- **When**: User clicks "Vorlage löschen" and confirms
- **Then**: Template soft-deleted, dropdown shows "Keine", fields unchanged
### Test Case 7: Set Standard
- **Given**: User has template selected (not standard)
- **When**: User clicks "Als Standard setzen"
- **Then**: Template marked as standard, displays bold with checkmark, fields unchanged
### Test Case 8: Clear Fields
- **Given**: User has template selected with values
- **When**: User selects "Keine" from dropdown
- **Then**: All fields cleared, ready for manual entry
---
## Known Limitations
1. **Standard Template Deletion**: Cannot delete a template marked as standard (must set another template as standard first)
2. **Single Standard**: Only one template can be marked as standard at a time
3. **Soft Delete**: Deleted templates remain in database with `IsDeleted = true`
---
## Future Enhancements
Potential improvements for future versions:
1. **Template Export/Import**: Export templates to file for sharing between systems
2. **Template Versioning**: Track template changes over time
3. **User-Specific Templates**: Allow users to have private templates
4. **Template Categories**: Organize templates into categories
5. **Template Permissions**: Control who can create/edit/delete templates
6. **Bulk Template Operations**: Apply template to multiple orders at once
7. **Template Statistics**: Track how often each template is used
---
## Migration Notes
### Upgrading from Previous Version
1. **Database**: ScriptMethod11751 runs automatically during update
2. **ApplicationSettings**: New settings are auto-created on first access
3. **Existing Data**: All existing automatic ticket creation settings remain intact
4. **Templates**: Start with empty template list (users create as needed)
### Rollback Considerations
If rollback is needed:
- ApplicationSettings remain (no data loss)
- HelpdeskCreationTemplate table can be safely dropped
- No impact on existing ticket creation functionality
---
## Support and Troubleshooting
### Common Issues
**Issue**: Template name disappears after saving
**Solution**: Fixed - template now updates in-place without losing reference
**Issue**: Fields clear when setting template as standard
**Solution**: Fixed - `_isLoadingTemplate` flag prevents unwanted clearing
**Issue**: Cannot update template
**Solution**: Fixed - main "Speichern" button now updates selected template
**Issue**: Template not loading on UI open
**Solution**: Verify ApplicationSettingID 10445 contains valid template I3D
### Debug Tips
1. Check `HelpdeskCreationTemplate` table for template data
2. Verify `ApplicationSettings` table for setting ID 10445
3. Check `_isLoadingTemplate` flag state during operations
4. Verify `Templates` ObservableCollection contains templates
5. Check `SelectedTemplate` property value
---
## Code Review Checklist
- ✅ Result<T> pattern used consistently
- ✅ Soft delete pattern implemented
- ✅ Standard tracking columns present
- ✅ UTF-8 with BOM encoding for C# files
- ✅ German localization for UI text
- ✅ English descriptions for ApplicationSettings
- ✅ NHibernate mappings correct
- ✅ Business logic in BL layer (not UI)
- ✅ DTO conversion in WebServiceBL
- ✅ Dialog patterns follow codebase standards
- ✅ MVVM pattern properly implemented
- ✅ Commands use DelegateCommand
- ✅ ObservableCollection for data binding
- ✅ Proper disposal of ClassContainer instances
- ✅ Async/await used correctly
- ✅ Error handling with Result pattern
- ✅ No hardcoded strings (use LocalizedStrings)
---
## Related Documentation
- [ApplicationSettings Pattern](../patterns/application-settings.md)
- [Dialog Manager Usage](../patterns/dialog-manager.md)
- [MVVM Guidelines](../patterns/mvvm-guidelines.md)
- [Database Script Creation](../database/script-creation.md)
- [NHibernate Best Practices](../data-access/nhibernate.md)
---
## Version History
| Version | Date | Changes |
|---------|------|---------|
| 1.0.0 | 2026-02-12 | Initial implementation with full template system |
---
## Contributors
- **Developer**: Implementation of template system and UI
- **Code Review**: Verified patterns and architecture
- **Testing**: User acceptance testing
---
**End of Documentation**
@@ -0,0 +1,429 @@
# 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](#ticket-164020) | CenSU überschreibt fremde Outlook-Termine | 🔴 Kritisch | ✅ Implementiert |
| 2 | [163184](#ticket-163184) | Doppelter Inhalt in Exchange-Termineinladung | 🔴 Kritisch | ✅ Implementiert |
| 3 | [164121](#ticket-164121) | Gelöschte Serientermin-Instanzen bleiben in Nexus | 🟠 Hoch | ✅ Implementiert |
| 4 | [160145](#ticket-160145) | Ganztages-Events um +1 Tag verschoben | 🟠 Hoch | ✅ Implementiert |
| 5 | [158813](#ticket-158813) | Geplante Zeiten mit falschem Datum nach Sync | 🟠 Hoch | ✅ Implementiert |
| 6 | [157036](#ticket-157036) | Fehlerhafte Wochenanzeige in Nexus | 🟡 Mittel | ✅ Implementiert |
| 7 | [162826](#ticket-162826) | Outlook-Erinnerungsflut bei synchronisierten Zeiten | 🟡 Mittel | ✅ Implementiert |
| 8 | [156991](#ticket-156991) | Doppelte Feiertage / falsches Datum in Nexus | 🟡 Mittel | ✅ Implementiert |
| + | [Zusatz](#zusatz-serientermin-kaskade) | Gelöschter Serienmaster löscht Instanzen nicht | 🟠 Hoch | ✅ Implementiert |
| + | [Inspector](#zusatz-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:**
```csharp
baseExpression = baseExpression.And(x =>
x.ObjectType != (int)CentronObjectKindNumeric.HelpdeskTimerClass &&
x.ObjectType != (int)CentronObjectKindNumeric.HelpdeskClass);
```
**`UpdateScheduleByGraph()` – Datum-Schutz für Helpdesk-Schedules:**
```csharp
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()`:**
```csharp
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
```csharp
// 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:**
```csharp
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:**
```csharp
if (graphEvent.IsAllDay == true)
(schedule.DateStart, schedule.DateEnd) = ParseAllDayEventDates(graphEvent);
```
```csharp
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()`:**
```csharp
// "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:**
```razor
<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:**
```csharp
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 `MailEntryID`s. 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:**
```csharp
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:**
```csharp
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)
- [x] **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.
- [x] **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.
- [x] **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.
- [x] **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) |