# Implementierungs-Leitfaden: Asset Management & Scheduling Modules > **Zielgruppe**: C# Entwickler, WPF/MVVM Architekten > **Schwerpunkt**: Praktische Umsetzung der neuen Module > **Basis**: USE_CASES_NEW.md + USE_CASES_NEW_GUI_MAPPING.md --- ## 🎯 Schnelleinstieg ### Verzeichnis-Struktur nach Fertigstellung ``` src/ β”œβ”€β”€ centron/Centron.WPF.UI/Modules/ β”‚ β”œβ”€β”€ Administration/ β”‚ β”‚ β”œβ”€β”€ AssetManagement/ [NEU] β”‚ β”‚ β”‚ β”œβ”€β”€ AssetManagementAppModuleController.cs β”‚ β”‚ β”‚ β”œβ”€β”€ AssetManagementView.xaml β”‚ β”‚ β”‚ β”œβ”€β”€ ViewModels/ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ AssetInventoryViewModel.cs β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ AssetDetailsViewModel.cs β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ AssetLifecycleViewModel.cs β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ AssetBatchImportViewModel.cs β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ PatchManagementViewModel.cs β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ SnmpMonitoringViewModel.cs β”‚ β”‚ β”‚ β”‚ └── LicenseManagementViewModel.cs β”‚ β”‚ β”‚ └── Views/ β”‚ β”‚ β”‚ β”œβ”€β”€ AssetInventoryView.xaml β”‚ β”‚ β”‚ β”œβ”€β”€ AssetDetailsView.xaml β”‚ β”‚ β”‚ β”œβ”€β”€ AssetLifecycleView.xaml β”‚ β”‚ β”‚ β”œβ”€β”€ PatchManagementView.xaml β”‚ β”‚ β”‚ β”œβ”€β”€ SnmpMonitoringView.xaml β”‚ β”‚ β”‚ └── LicenseManagementView.xaml β”‚ β”‚ β”‚ └── Calendar/ β”‚ β”œβ”€β”€ AppointmentScheduling/ [NEU/ERWEITERT] β”‚ β”‚ β”œβ”€β”€ AppointmentSchedulingAppModuleController.cs β”‚ β”‚ β”œβ”€β”€ AppointmentSchedulingView.xaml β”‚ β”‚ β”œβ”€β”€ ViewModels/ β”‚ β”‚ β”‚ β”œβ”€β”€ AppointmentRequestViewModel.cs β”‚ β”‚ β”‚ β”œβ”€β”€ AppointmentProposalViewModel.cs β”‚ β”‚ β”‚ β”œβ”€β”€ AppointmentConfirmationViewModel.cs β”‚ β”‚ β”‚ β”œβ”€β”€ TechnicianRoutePlannerViewModel.cs β”‚ β”‚ β”‚ β”œβ”€β”€ CapacityPlanningViewModel.cs β”‚ β”‚ β”‚ β”œβ”€β”€ SLAMonitoringViewModel.cs β”‚ β”‚ β”‚ └── WizardPages/ β”‚ β”‚ β”‚ β”œβ”€β”€ AppointmentRequestWizardPageViewModel.cs β”‚ β”‚ β”‚ β”œβ”€β”€ AppointmentProposalWizardPageViewModel.cs β”‚ β”‚ β”‚ β”œβ”€β”€ AppointmentConfirmationWizardPageViewModel.cs β”‚ β”‚ └── Views/ β”‚ β”‚ β”œβ”€β”€ AppointmentRequestView.xaml β”‚ β”‚ β”œβ”€β”€ AppointmentSlotSelectionView.xaml β”‚ β”‚ β”œβ”€β”€ RouteOptimizationView.xaml β”‚ β”‚ β”œβ”€β”€ CapacityPlanningView.xaml β”‚ β”‚ β”œβ”€β”€ SLAMonitoringView.xaml β”‚ β”‚ └── Wizard/ β”‚ β”‚ β”œβ”€β”€ AppointmentBookingWizardView.xaml β”‚ β”‚ β”‚ └── RouteOptimization/ [NEU] β”‚ β”œβ”€β”€ RouteOptimizationAppModuleController.cs β”‚ β”œβ”€β”€ RouteOptimizationView.xaml β”‚ └── ViewModels/ β”‚ β”œβ”€β”€ TechnicianRoutePlannerViewModel.cs β”‚ β”œβ”€β”€ ZoneManagementViewModel.cs β”‚ └── RouteMapViewModel.cs β”‚ └── backend/ β”œβ”€β”€ Centron.BL/ β”‚ β”œβ”€β”€ Sales/CustomerAssets/ [ERWEITERN] β”‚ β”‚ β”œβ”€β”€ AssetBL.cs (erweitern um 1.1-1.5) β”‚ β”‚ β”œβ”€β”€ AssetArticleBL.cs β”‚ β”‚ β”œβ”€β”€ AssetPatchBL.cs [NEU] β”‚ β”‚ β”œβ”€β”€ AssetSnmpMonitoringBL.cs [NEU] β”‚ β”‚ β”œβ”€β”€ AssetLicenseBL.cs [NEU] β”‚ β”‚ └── AssetDepreciationBL.cs [NEU] β”‚ β”‚ β”‚ β”œβ”€β”€ AppointmentRequests/ [ERWEITERN] β”‚ β”‚ β”œβ”€β”€ AppointmentRequestBL.cs (erweitern um 17.1) β”‚ β”‚ β”œβ”€β”€ AppointmentProposalBL.cs [NEU] β”‚ β”‚ β”œβ”€β”€ TechnicianRouteBL.cs [NEU] β”‚ β”‚ β”œβ”€β”€ CapacityPlanningBL.cs [NEU] β”‚ β”‚ └── SLAAgreementBL.cs [NEU] β”‚ β”‚ β”‚ └── MyCentron/Schedulings/ [ERWEITERN] β”‚ β”œβ”€β”€ SchedulingBL.cs (erweitern um KapazitΓ€t) β”‚ β”œβ”€β”€ ScheduleOptimizationBL.cs [NEU] β”‚ └── ScheduleAlertingBL.cs [NEU] β”‚ β”œβ”€β”€ Centron.Interfaces/ β”‚ β”œβ”€β”€ Administration/ β”‚ β”‚ β”œβ”€β”€ AssetManagement/ [NEU] β”‚ β”‚ β”‚ β”œβ”€β”€ IAssetManagementLogic.cs β”‚ β”‚ β”‚ β”œβ”€β”€ IAssetPatchLogic.cs β”‚ β”‚ β”‚ β”œβ”€β”€ IAssetSnmpMonitoringLogic.cs β”‚ β”‚ β”‚ └── IAssetLicenseLogic.cs β”‚ β”‚ β”‚ β”‚ β”‚ └── Scheduling/ [NEU] β”‚ β”‚ β”œβ”€β”€ IAppointmentSchedulingLogic.cs β”‚ β”‚ β”œβ”€β”€ ITechnicianRoutingLogic.cs β”‚ β”‚ β”œβ”€β”€ ICapacityPlanningLogic.cs β”‚ β”‚ └── ISLAAgreementLogic.cs β”‚ β”‚ β”‚ └── WebServiceBL/ [ERWEITERN] β”‚ β”œβ”€β”€ AssetManagementWebServiceBL.cs [NEU] β”‚ └── SchedulingWebServiceBL.cs [NEU] β”‚ β”œβ”€β”€ Centron.Entities/Entities/ β”‚ β”œβ”€β”€ Administration/MasterData/ β”‚ β”‚ β”œβ”€β”€ AssetCondition.cs (existiert) β”‚ β”‚ β”œβ”€β”€ AssetManagementDevice.cs [NEU - optional] β”‚ β”‚ └── AssetManagementPatch.cs [NEU - optional] β”‚ β”‚ β”‚ β”œβ”€β”€ AppointmentRequests/ [EXISTIERT] β”‚ β”‚ β”œβ”€β”€ AppointmentRequest.cs β”‚ β”‚ β”œβ”€β”€ AppointmentProposal.cs β”‚ β”‚ └── AppointmentRequestFilter.cs β”‚ β”‚ β”‚ β”œβ”€β”€ Sales/CustomerAssets/ [EXISTIERT] β”‚ β”‚ β”œβ”€β”€ Asset.cs β”‚ β”‚ └── AssetBase.cs β”‚ β”‚ β”‚ └── MyCentron/Schedulings/ [EXISTIERT] β”‚ β”œβ”€β”€ Scheduling.cs β”‚ └── SchedulingFilter.cs β”‚ └── Centron.DAO/ └── Repositories/ β”œβ”€β”€ Sales/Customers/Assets/ [EXISTIERT] └── AppointmentRequests/ [EXISTIERT] ``` --- ## πŸ—οΈ ViewModel-Template fΓΌr Asset Management ```csharp // File: src/centron/Centron.WPF.UI/Modules/Administration/AssetManagement/ViewModels/AssetInventoryViewModel.cs // UTF-8 with BOM encoding required! using Centron.Interfaces.BL; using Centron.Common; using Prism.Mvvm; using Prism.Commands; using System; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using DevExpress.Mvvm; using Centron.Data.Entities.Sales.CustomerAssets; using Centron.WPF.UI.Common; using Centron.WPF.UI.Services.Logics; namespace Centron.WPF.UI.Modules.Administration.AssetManagement.ViewModels { /// /// ViewModel fΓΌr Asset-Inventarverwaltung (UC 1.1 - 1.5) /// /// Use Cases: /// - UC 1.1: Neue IT-GerΓ€te erfassen /// - UC 1.2: GerΓ€te-Details anzeigen und bearbeiten /// - UC 1.3: GerΓ€te-BestΓ€nde nach Abteilung verwalten /// - UC 1.4: GerΓ€te-Lebenszyklusmanagement /// - UC 1.5: Batch-Import von GerΓ€teinventaren /// /// ViewModel Properties β†’ Use Cases: /// DeviceName β†’ UC 1.1 TextEdit /// SelectedDeviceType β†’ UC 1.1 ComboBoxEdit /// SelectedDepartmentI3D β†’ UC 1.3 Filter /// SelectedStatus β†’ UC 1.4 Lifecycle /// public class AssetInventoryViewModel : BindableBase { #region Dependencies private readonly IAssetManagementLogic _assetLogic; private readonly IMessageDialogService _messageService; #endregion #region Fields private string _deviceName; private int _selectedDeviceType; private string _manufacturer; private string _modelNumber; private string _serialNumber; private string _assetTag; private int _selectedDepartmentI3D; private int _selectedLocationI3D; private int _selectedStatus; private bool _isLoading; #endregion #region Properties /// /// UC 1.1: GerΓ€tname eingeben /// public string DeviceName { get => _deviceName; set => this.SetProperty(ref _deviceName, value, nameof(DeviceName)); } /// /// UC 1.1: GerΓ€ttyp auswΓ€hlen /// public int SelectedDeviceType { get => _selectedDeviceType; set => this.SetProperty(ref _selectedDeviceType, value, nameof(SelectedDeviceType)); } /// /// UC 1.1: Hersteller angeben /// public string Manufacturer { get => _manufacturer; set => this.SetProperty(ref _manufacturer, value, nameof(Manufacturer)); } /// /// UC 1.1: Modellnummer eingeben /// public string ModelNumber { get => _modelNumber; set => this.SetProperty(ref _modelNumber, value, nameof(ModelNumber)); } /// /// UC 1.1: Seriennummer (eindeutig!) /// public string SerialNumber { get => _serialNumber; set => this.SetProperty(ref _serialNumber, value, nameof(SerialNumber)); } /// /// UC 1.1: Asset-Tag (auto-generated) /// public string AssetTag { get => _assetTag; set => this.SetProperty(ref _assetTag, value, nameof(AssetTag)); } /// /// UC 1.3: Abteilung filtern /// public int SelectedDepartmentI3D { get => _selectedDepartmentI3D; set => this.SetProperty(ref _selectedDepartmentI3D, value, nameof(SelectedDepartmentI3D)); } /// /// UC 1.1: Standort zuordnen /// public int SelectedLocationI3D { get => _selectedLocationI3D; set => this.SetProperty(ref _selectedLocationI3D, value, nameof(SelectedLocationI3D)); } /// /// UC 1.4: Status / Lifecycle Stage /// public int SelectedStatus { get => _selectedStatus; set => this.SetProperty(ref _selectedStatus, value, nameof(SelectedStatus)); } /// /// UI-Status: Loading indicator /// public bool IsLoading { get => _isLoading; set => this.SetProperty(ref _isLoading, value, nameof(IsLoading)); } /// /// UC 1.3: Assets-Grid Datenquelle /// public ObservableCollection Assets { get; set; } /// /// UC 1.3: Abteilungs-Dropdown Datenquelle /// public ObservableCollection Departments { get; set; } /// /// UC 1.1: Locations-Dropdown /// public ObservableCollection Locations { get; set; } #endregion #region Commands /// /// UC 1.1: Neues GerΓ€t erstellen /// public ICommand CreateNewAssetCommand { get; private set; } /// /// UC 1.1: Asset-Tag auto-generieren /// public ICommand GenerateAssetTagCommand { get; private set; } /// /// UC 1.1: GerΓ€t speichern /// public ICommand SaveAssetCommand { get; private set; } /// /// UC 1.5: Batch-Import starten /// public ICommand OpenBatchImportCommand { get; private set; } /// /// UC 1.3: Filter anwenden /// public ICommand FilterCommand { get; private set; } #endregion #region Constructor public AssetInventoryViewModel() { // Dependency Injection (from ClassContainer or direct DI) _assetLogic = ClassContainer.Instance .GetInstance(); _messageService = ClassContainer.Instance .GetInstance(); // Collections this.Assets = new ObservableCollection(); this.Departments = new ObservableCollection(); this.Locations = new ObservableCollection(); // Initialize Commands this.CreateNewAssetCommand = new DelegateCommand(this.CreateNewAsset); this.GenerateAssetTagCommand = new DelegateCommand(this.GenerateAssetTag); this.SaveAssetCommand = new DelegateCommand(this.SaveAsset, this.CanSaveAsset); this.OpenBatchImportCommand = new DelegateCommand(this.OpenBatchImport); this.FilterCommand = new DelegateCommand(this.ApplyFilter); // Load initial data this.LoadInitialData(); } #endregion #region Methods /// /// UC 1.1: Neues Asset-Objekt initialisieren /// private void CreateNewAsset() { // Clear form for new entry this.DeviceName = string.Empty; this.Manufacturer = string.Empty; this.ModelNumber = string.Empty; this.SerialNumber = string.Empty; this.SelectedDeviceType = 0; this.SelectedLocationI3D = 0; this.SelectedStatus = 1; // Active this._messageService.ShowMessage("Neues GerΓ€t bereit zur Eingabe", "Info"); } /// /// UC 1.1: Asset-Tag auto-generieren /// private void GenerateAssetTag() { // Pattern: AST-YYYYMMDD-XXX string prefix = "AST-" + DateTime.Now.ToString("yyyyMMdd"); string randomPart = new Random().Next(100, 999).ToString(); this.AssetTag = $"{prefix}-{randomPart}"; } /// /// UC 1.1: GerΓ€t speichern /// private void SaveAsset() { try { this.IsLoading = true; // Validation if (string.IsNullOrWhiteSpace(this.DeviceName)) { this._messageService.ShowError("GerΓ€tname erforderlich", "Validierung"); return; } if (string.IsNullOrWhiteSpace(this.SerialNumber)) { this._messageService.ShowError("Seriennummer erforderlich", "Validierung"); return; } // Call BL var result = this._assetLogic.CreateNewAsset(new CreateAssetRequest { DeviceName = this.DeviceName, DeviceType = (AssetDeviceType)this.SelectedDeviceType, Manufacturer = this.Manufacturer, ModelNumber = this.ModelNumber, SerialNumber = this.SerialNumber, AssetTag = this.AssetTag, DepartmentI3D = this.SelectedDepartmentI3D, LocationI3D = this.SelectedLocationI3D, Status = (AssetStatus)this.SelectedStatus }); if (result.Status == ResultStatus.Success) { this._messageService.ShowMessage($"GerΓ€t gespeichert: {result.Data.I3D}", "Erfolg"); this.CreateNewAsset(); // Reset form this.ApplyFilter(); // Refresh list } else { this._messageService.ShowError(result.Error, "Fehler"); } } catch (Exception ex) { this._messageService.ShowError($"Fehler beim Speichern: {ex.Message}", "Fehler"); } finally { this.IsLoading = false; } } /// /// UC 1.1: CanExecute fΓΌr SaveCommand /// private bool CanSaveAsset() { return !string.IsNullOrWhiteSpace(this.DeviceName) && !string.IsNullOrWhiteSpace(this.SerialNumber); } /// /// UC 1.5: Batch-Import ΓΆffnen /// private void OpenBatchImport() { // Open wizard view var wizard = new AssetBatchImportWizardView(); // ... present wizard dialog } /// /// UC 1.3: Filter anwenden /// private async void ApplyFilter() { try { this.IsLoading = true; var result = await this._assetLogic.GetAssetsByDepartmentAsync( this.SelectedDepartmentI3D, (AssetStatus?)this.SelectedStatus ); if (result.Status == ResultStatus.Success) { this.Assets.Clear(); foreach (var asset in result.Data) { this.Assets.Add(new AssetListItemViewModel { I3D = asset.I3D, DeviceName = asset.DeviceName, AssetTag = asset.AssetTag, Status = asset.Status.ToString() }); } } } finally { this.IsLoading = false; } } /// /// Load initial data (departments, locations, etc.) /// private async void LoadInitialData() { try { // Load departments var deptResult = await this._assetLogic.GetDepartmentsAsync(); if (deptResult.Status == ResultStatus.Success) { this.Departments.Clear(); foreach (var dept in deptResult.Data) { this.Departments.Add(new DepartmentViewModel { I3D = dept.I3D, Name = dept.Name }); } } } catch (Exception ex) { this._messageService.ShowError($"Fehler beim Laden: {ex.Message}", "Fehler"); } } #endregion } } ``` --- ## πŸ—οΈ ViewModel-Template fΓΌr Scheduling ```csharp // File: src/centron/Centron.WPF.UI/Modules/Calendar/AppointmentScheduling/ViewModels/AppointmentRequestViewModel.cs using Prism.Mvvm; using Prism.Commands; using System; using System.Collections.ObjectModel; using Centron.Interfaces.BL; using Centron.Common; namespace Centron.WPF.UI.Modules.Calendar.AppointmentScheduling.ViewModels { /// /// ViewModel fΓΌr Terminanfragen (UC 17.1) /// Use Cases: /// - UC 17.1.1: Kunde fordert Termin an /// - UC 17.1.2: System schlΓ€gt verfΓΌgbare Slots vor /// - UC 17.1.3: Kunde wΓ€hlt Termin-Slot /// - UC 17.1.4: TerminbestΓ€tigung an Kunde /// - UC 17.1.5: Termine verschieben/absagen /// public class AppointmentRequestViewModel : BindableBase { #region Dependencies private readonly IAppointmentSchedulingLogic _appointmentLogic; #endregion #region Fields private int _ticketI3D; private int _selectedServiceType; private DateTime _preferredDateFrom; private DateTime _preferredDateTo; private int _estimatedDurationInHours; private string _locationAddress; private string _customerNotes; private string _appointmentStatus; #endregion #region Properties /// /// UC 17.1.1: Hidden - aus Ticket Context /// public int TicketI3D { get => _ticketI3D; set => this.SetProperty(ref _ticketI3D, value, nameof(TicketI3D)); } /// /// UC 17.1.1: Service-Typ (Installation/Wartung/Reparatur) /// public int SelectedServiceType { get => _selectedServiceType; set => this.SetProperty(ref _selectedServiceType, value, nameof(SelectedServiceType)); } /// /// UC 17.1.1: GewΓΌnschtes Startdatum /// public DateTime PreferredDateFrom { get => _preferredDateFrom; set => this.SetProperty(ref _preferredDateFrom, value, nameof(PreferredDateFrom)); } /// /// UC 17.1.1: GewΓΌnschtes Enddatum /// public DateTime PreferredDateTo { get => _preferredDateTo; set => this.SetProperty(ref _preferredDateTo, value, nameof(PreferredDateTo)); } /// /// UC 17.1.1: GeschΓ€tzte Dauer in Stunden /// public int EstimatedDurationInHours { get => _estimatedDurationInHours; set => this.SetProperty(ref _estimatedDurationInHours, value, nameof(EstimatedDurationInHours)); } /// /// UC 17.1.1: Ort des Termins /// public string LocationAddress { get => _locationAddress; set => this.SetProperty(ref _locationAddress, value, nameof(LocationAddress)); } /// /// UC 17.1.1: Kundennotizen /// public string CustomerNotes { get => _customerNotes; set => this.SetProperty(ref _customerNotes, value, nameof(CustomerNotes)); } /// /// UC 17.1.2: VerfΓΌgbare Termine /// public ObservableCollection ProposedSlots { get; set; } /// /// Aktueller Status der Anfrage /// public string AppointmentStatus { get => _appointmentStatus; set => this.SetProperty(ref _appointmentStatus, value, nameof(AppointmentStatus)); } #endregion #region Commands public ICommand RequestAppointmentCommand { get; private set; } public ICommand SelectSlotCommand { get; private set; } public ICommand ConfirmAppointmentCommand { get; private set; } #endregion #region Constructor public AppointmentRequestViewModel() { this._appointmentLogic = ClassContainer.Instance .GetInstance(); this.ProposedSlots = new ObservableCollection(); // Initialize Commands this.RequestAppointmentCommand = new DelegateCommand(this.RequestAppointment); this.SelectSlotCommand = new DelegateCommand(this.SelectSlot); this.ConfirmAppointmentCommand = new DelegateCommand(this.ConfirmAppointment); // Default values this.PreferredDateFrom = DateTime.Now; this.PreferredDateTo = DateTime.Now.AddDays(7); this.EstimatedDurationInHours = 2; this.AppointmentStatus = "Bereit fΓΌr Anfrage"; } #endregion #region Methods /// /// UC 17.1.1: Anfrage einreichen und Slots vorschlagen /// private async void RequestAppointment() { try { // UC 17.1.2: Get available slots var result = await this._appointmentLogic.GetAvailableAppointmentSlotsAsync( this.TicketI3D, this.PreferredDateFrom, this.PreferredDateTo, this.EstimatedDurationInHours ); if (result.Status == ResultStatus.Success) { this.ProposedSlots.Clear(); foreach (var slot in result.Data) { this.ProposedSlots.Add(new AppointmentSlotViewModel { SlotDateTime = slot.StartTime, TechnicianName = slot.TechnicianName, TravelTime = $"{slot.TravelTimeMinutes} Min", ProposalI3D = slot.I3D }); } this.AppointmentStatus = $"βœ“ {this.ProposedSlots.Count} Slots verfΓΌgbar"; } } catch (Exception ex) { this.AppointmentStatus = $"βœ— Fehler: {ex.Message}"; } } /// /// UC 17.1.3: Slot auswΓ€hlen /// private void SelectSlot(AppointmentSlotViewModel slot) { if (slot != null) { this.AppointmentStatus = $"Slot ausgewΓ€hlt: {slot.SlotDateTime:dd.MM.yyyy HH:mm}"; // Store selected slot for confirmation } } /// /// UC 17.1.4: Termin bestΓ€tigen /// private async void ConfirmAppointment() { try { // TODO: Call BookAppointment logic this.AppointmentStatus = "βœ“ Termin bestΓ€tigt!"; } catch (Exception ex) { this.AppointmentStatus = $"βœ— Fehler: {ex.Message}"; } } #endregion } } ``` --- ## πŸ“‹ REST API Endpoints Template ```csharp // File: src/webservice/Centron.WebServices.Core/RestService/CentronRestService.cs // Add these methods to existing service: [WebInvoke(Method = "POST", UriTemplate = "AssetManagement/GetDeviceDetails")] [Authenticate] public Response GetAssetDeviceDetails(Request request) { // UC 1.2: Get asset details try { var result = this.classContainer .WithInstance((IAssetManagementLogic logic) => logic.GetAssetDetails(request.Data)) .ThrowIfError(); return Response.Success(result.Data); } catch (Exception ex) { return Response.Error(ex.Message); } } [WebInvoke(Method = "POST", UriTemplate = "AssetManagement/ImportBatch")] [Authenticate] public Response ImportAssetBatch(Request request) { // UC 1.5: Batch import try { var result = this.classContainer .WithInstance((IAssetManagementLogic logic) => logic.ImportAssetsBatch( request.Data.FileContent, request.Data.Format)) .ThrowIfError(); return Response.Success(result.Data); } catch (Exception ex) { return Response.Error(ex.Message); } } [WebInvoke(Method = "POST", UriTemplate = "Scheduling/GetAvailableAppointments")] [Authenticate] public Response> GetAvailableAppointments(Request request) { // UC 17.1.2: Get available slots try { var result = this.classContainer .WithInstance((IAppointmentSchedulingLogic logic) => logic.GetAvailableAppointmentSlots( request.Data.TicketI3D, request.Data.PreferredDateFrom, request.Data.PreferredDateTo, request.Data.DurationHours)) .ThrowIfError(); return Response.Success(result.Data); } catch (Exception ex) { return Response.Error(ex.Message); } } [WebInvoke(Method = "POST", UriTemplate = "Scheduling/BookAppointment")] [Authenticate] public Response BookAppointment(Request request) { // UC 17.1.3: Book appointment try { var result = this.classContainer .WithInstance((IAppointmentSchedulingLogic logic) => logic.BookAppointment(request.Data)) .ThrowIfError(); return Response.Success(result.Data); } catch (Exception ex) { return Response.Error(ex.Message); } } [WebInvoke(Method = "POST", UriTemplate = "Scheduling/OptimizeRoute")] [Authenticate] public Response OptimizeRoute(Request request) { // UC 17.2: Route optimization try { var result = this.classContainer .WithInstance((ITechnicianRoutingLogic logic) => logic.OptimizeRoute( request.Data.TechnicianI3D, request.Data.Date)) .ThrowIfError(); return Response.Success(result.Data); } catch (Exception ex) { return Response.Error(ex.Message); } } [WebInvoke(Method = "POST", UriTemplate = "Scheduling/GetSLAStatus")] [Authenticate] public Response GetSLAStatus(Request request) { // UC 17.4: Get SLA status try { var result = this.classContainer .WithInstance((ISLAAgreementLogic logic) => logic.GetSLAStatus(request.Data)) .ThrowIfError(); return Response.Success(result.Data); } catch (Exception ex) { return Response.Error(ex.Message); } } ``` --- ## πŸ§ͺ Unit Test Template ```csharp // File: tests/Centron.Tests/BL/AssetManagementLogicTests.cs [TestFixture] public class AssetManagementLogicTests { private AssetManagementLogic _logic; private Mock _assetBLMock; private Mock _sessionMock; [SetUp] public void Setup() { _sessionMock = new Mock(); _assetBLMock = new Mock(); _logic = new AssetManagementLogic(_sessionMock.Object, _assetBLMock.Object); } /// /// UC 1.1: Test creating new asset /// [Test] public void CreateNewAsset_WithValidData_ReturnsSuccess() { // Arrange var request = new CreateAssetRequest { DeviceName = "Test-Laptop", SerialNumber = "SN-123456", Manufacturer = "Dell" }; var expectedAsset = new Asset { I3D = 1, DeviceName = "Test-Laptop", SerialNumber = "SN-123456" }; _assetBLMock .Setup(bl => bl.CreateNewAsset(It.IsAny())) .Returns(expectedAsset); // Act var result = _logic.CreateNewAsset(request); // Assert Assert.That(result.Status, Is.EqualTo(ResultStatus.Success)); Assert.That(result.Data.I3D, Is.EqualTo(1)); Assert.That(result.Data.DeviceName, Is.EqualTo("Test-Laptop")); } /// /// UC 1.1: Test validation - missing serial number /// [Test] public void CreateNewAsset_WithoutSerialNumber_ReturnsError() { // Arrange var request = new CreateAssetRequest { DeviceName = "Test-Laptop", SerialNumber = "" // Missing! }; // Act var result = _logic.CreateNewAsset(request); // Assert Assert.That(result.Status, Is.EqualTo(ResultStatus.Error)); Assert.That(result.Error, Contains.Substring("Seriennummer")); } } ``` --- ## πŸ“¦ Deployment Checklist - [ ] **Code-Review** durchfΓΌhren - [ ] **Unit Tests** schreiben und ausfΓΌhren - [ ] **Integration Tests** mit echtem DB durchfΓΌhren - [ ] **Code Analysis** (SonarQube/StyleCop) ausfΓΌhren - [ ] **Performance Tests** durchfΓΌhren - [ ] **UI-Tests** durchfΓΌhren (manual) - [ ] **Ribbon-Integration** testen - [ ] **Localization** ΓΌberprΓΌfen (Deutsche Strings) - [ ] **Documentation** finalisieren - [ ] **Release Notes** schreiben - [ ] **Module Registration** in main app durchfΓΌhren - [ ] **Build Pipeline** anpassen --- ## πŸ”— NΓ€chste Schritte 1. **Code-Struktur erstellen** (Verzeichnisse, leere Dateien) 2. **Templates ausfΓΌllen** (Controller, ViewModel, View, BL) 3. **Database-Access** via DAO implementieren 4. **UI-Binding** in XAML durchfΓΌhren 5. **Commands** und Event-Handler implementieren 6. **Unit Tests** schreiben 7. **Integration Tests** durchfΓΌhren 8. **User Acceptance Testing** organisieren 9. **Deployment** durchfΓΌhren --- **Status**: βœ… Implementierungs-Leitfaden abgeschlossen | Templates & Checklisten bereit