@page "/settings" @using Microsoft.EntityFrameworkCore @using TrafagSalesExporter.Data @using TrafagSalesExporter.Models @using TrafagSalesExporter.Services @inject IDbContextFactory DbFactory @inject ISharePointUploadService SpService @inject TimerBackgroundService TimerService @inject IHanaQueryService HanaService @inject ISapGatewayService SapGatewayService @inject IConfigTransferService ConfigTransferService @inject IExchangeRateImportService ExchangeRateImportService @inject IJSRuntime JS @inject ISnackbar Snackbar Settings Settings Konfiguration Import/Export Wenn deaktiviert, bleiben Passwörter und Secrets beim Export leer. Beim Import ohne Secrets werden bestehende Secrets auf dem Zielsystem beibehalten. @(_exportingConfig ? "Exportiere..." : "Konfiguration exportieren") @(_importingConfig ? "Importiere..." : "Konfiguration importieren") @* SharePoint Config *@ SharePoint Konfiguration Speichern @if (_testingSp) { @("Teste...") } else { @("SharePoint Verbindung testen") } @if (!string.IsNullOrWhiteSpace(_sharePointTestPreview)) {
Test Preview
@_sharePointTestPreview
}
Quellsysteme Diese Zugangsdaten werden pro Quellsystem als Standard verwendet. Ein Standort kann sie bei Bedarf mit eigenen Overrides überschreiben. Quellsystem hinzufuegen Code Name Anschlussart Zentrale URL User Aktiv Test @context.Code @context.DisplayName @GetConnectionKindLabel(context.ConnectionKind) @GetServiceUrlSummary(context) @GetUsernameSummary(context) @if (context.IsActive) { } else { } @if (!UsesManualImport(context)) { @(_testingSystems.Contains(context.Code) ? "Teste..." : "Testen") } Quellsysteme speichern @(_editingSourceSystem.Id == 0 ? "Quellsystem hinzufuegen" : "Quellsystem bearbeiten") @foreach (var kind in SourceSystemConnectionKinds.All) { @GetConnectionKindLabel(kind) } @if (UsesSapGateway(_editingSourceSystem)) { } Abbrechen Uebernehmen Wechselkurse Diese Kurstabelle wird von der Transformation ConvertCurrency verwendet. Gleiche Waehrung rechnet automatisch mit Faktor 1. Kurs hinzufuegen @(_refreshingExchangeRates ? "Aktualisiere ECB-Kurse..." : "Refresh Kurse") Kurse speichern Von Nach Kurs Gueltig ab Gueltig bis Notiz Aktiv @* Export Settings *@ Export Einstellungen Schreibt zusätzliche technische Fortschrittsmeldungen für HANA- und SAP-Lesevorgänge ins Dashboard und in die Logs. Speichern @* Filename Preview *@ Dateiname Vorschau Sales_{"{TSC}"}_{DateTime.Now:yyyy-MM-dd}.xlsx Beispiel: Sales_TRFR_@(DateTime.Now.ToString("yyyy-MM-dd")).xlsx @code { private SharePointConfig _spConfig = new(); private ExportSettings _exportSettings = new(); private List _sourceSystems = []; private SourceSystemDefinition _editingSourceSystem = new(); private bool _testingSp; private bool _includeSecretsInExport; private bool _exportingConfig; private bool _importingConfig; private bool _refreshingExchangeRates; private string _sharePointTestPreview = string.Empty; private List _exchangeRates = []; private readonly HashSet _testingSystems = []; private bool _sourceSystemDialogVisible; private readonly DialogOptions _sourceSystemDialogOptions = new() { MaxWidth = MaxWidth.Small, FullWidth = true }; protected override async Task OnInitializedAsync() { using var db = await DbFactory.CreateDbContextAsync(); _spConfig = await db.SharePointConfigs.FirstOrDefaultAsync() ?? new SharePointConfig(); _exportSettings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings(); _sourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync(); _exchangeRates = await db.CurrencyExchangeRates .OrderBy(x => x.FromCurrency) .ThenBy(x => x.ToCurrency) .ThenByDescending(x => x.ValidFrom) .ToListAsync(); } private async Task SaveSharePoint() { using var db = await DbFactory.CreateDbContextAsync(); var existing = await db.SharePointConfigs.FirstOrDefaultAsync(); if (existing is null) { db.SharePointConfigs.Add(_spConfig); } else { existing.SiteUrl = _spConfig.SiteUrl; existing.ExportFolder = _spConfig.ExportFolder; existing.CentralExportFolder = _spConfig.CentralExportFolder; existing.TenantId = _spConfig.TenantId; existing.ClientId = _spConfig.ClientId; existing.ClientSecret = _spConfig.ClientSecret; } await db.SaveChangesAsync(); Snackbar.Add("SharePoint Konfiguration gespeichert", Severity.Success); } private async Task TestSharePoint() { _testingSp = true; try { var tenantId = NormalizeConfigValue(_spConfig.TenantId); var clientId = NormalizeConfigValue(_spConfig.ClientId); var clientSecret = NormalizeConfigValue(_spConfig.ClientSecret); var siteUrl = NormalizeConfigValue(_spConfig.SiteUrl); _sharePointTestPreview = BuildSharePointTestPreview(tenantId, clientId, clientSecret, siteUrl); await SpService.TestConnectionAsync( tenantId, clientId, clientSecret, siteUrl); Snackbar.Add("SharePoint Verbindung erfolgreich!", Severity.Success); } catch (Exception ex) { Snackbar.Add($"Verbindung fehlgeschlagen: {ex.Message}", Severity.Error); } finally { _testingSp = false; } } private async Task SaveExportSettings() { using var db = await DbFactory.CreateDbContextAsync(); var existing = await db.ExportSettings.FirstOrDefaultAsync(); if (existing is null) { db.ExportSettings.Add(_exportSettings); } else { existing.DateFilter = _exportSettings.DateFilter; existing.TimerHour = _exportSettings.TimerHour; existing.TimerMinute = _exportSettings.TimerMinute; existing.TimerEnabled = _exportSettings.TimerEnabled; existing.DebugLoggingEnabled = _exportSettings.DebugLoggingEnabled; existing.LocalSiteExportFolder = _exportSettings.LocalSiteExportFolder; existing.LocalConsolidatedExportFolder = _exportSettings.LocalConsolidatedExportFolder; } await db.SaveChangesAsync(); TimerService.Recalculate(); Snackbar.Add("Export Einstellungen gespeichert", Severity.Success); } private void AddSourceSystem() { _editingSourceSystem = new SourceSystemDefinition { Code = string.Empty, DisplayName = string.Empty, ConnectionKind = SourceSystemConnectionKinds.Hana, IsActive = true }; _sourceSystemDialogVisible = true; } private void EditSourceSystem(SourceSystemDefinition definition) { _editingSourceSystem = new SourceSystemDefinition { Id = definition.Id, Code = definition.Code, DisplayName = definition.DisplayName, ConnectionKind = definition.ConnectionKind, IsActive = definition.IsActive, CentralServiceUrl = definition.CentralServiceUrl, CentralUsername = definition.CentralUsername, CentralPassword = definition.CentralPassword }; _sourceSystemDialogVisible = true; } private void SaveSourceSystemEdit() { _editingSourceSystem.Code = NormalizeSourceSystemCode(_editingSourceSystem.Code); _editingSourceSystem.DisplayName = NormalizeConfigValue(_editingSourceSystem.DisplayName); _editingSourceSystem.ConnectionKind = NormalizeConnectionKind(_editingSourceSystem.ConnectionKind); _editingSourceSystem.CentralServiceUrl = NormalizeConfigValue(_editingSourceSystem.CentralServiceUrl); _editingSourceSystem.CentralUsername = NormalizeConfigValue(_editingSourceSystem.CentralUsername); _editingSourceSystem.CentralPassword = _editingSourceSystem.CentralPassword ?? string.Empty; if (string.IsNullOrWhiteSpace(_editingSourceSystem.Code) || string.IsNullOrWhiteSpace(_editingSourceSystem.DisplayName)) { Snackbar.Add("Code und Name fuer das Quellsystem sind Pflicht.", Severity.Warning); return; } if (_sourceSystems.Any(x => x.Id != _editingSourceSystem.Id && x.Code == _editingSourceSystem.Code)) { Snackbar.Add($"Quellsystem-Code doppelt vorhanden: {_editingSourceSystem.Code}", Severity.Warning); return; } if (_editingSourceSystem.Id == 0) { _sourceSystems.Add(_editingSourceSystem); } else { var existing = _sourceSystems.FirstOrDefault(x => x.Id == _editingSourceSystem.Id); if (existing is not null) { existing.Code = _editingSourceSystem.Code; existing.DisplayName = _editingSourceSystem.DisplayName; existing.ConnectionKind = _editingSourceSystem.ConnectionKind; existing.IsActive = _editingSourceSystem.IsActive; existing.CentralServiceUrl = _editingSourceSystem.CentralServiceUrl; existing.CentralUsername = _editingSourceSystem.CentralUsername; existing.CentralPassword = _editingSourceSystem.CentralPassword; } } _sourceSystems = _sourceSystems.OrderBy(x => x.Code).ToList(); _sourceSystemDialogVisible = false; } private void CloseSourceSystemDialog() { _sourceSystemDialogVisible = false; } private void RemoveSourceSystem(SourceSystemDefinition definition) { _sourceSystems.Remove(definition); } private async Task SaveSourceSystems() { var normalized = _sourceSystems .Select(x => new SourceSystemDefinition { Id = x.Id, Code = NormalizeSourceSystemCode(x.Code), DisplayName = NormalizeConfigValue(x.DisplayName), ConnectionKind = NormalizeConnectionKind(x.ConnectionKind), IsActive = x.IsActive, CentralServiceUrl = NormalizeConfigValue(x.CentralServiceUrl), CentralUsername = NormalizeConfigValue(x.CentralUsername), CentralPassword = x.CentralPassword ?? string.Empty }) .Where(x => !string.IsNullOrWhiteSpace(x.Code)) .ToList(); if (normalized.Any(x => string.IsNullOrWhiteSpace(x.DisplayName))) { Snackbar.Add("Jedes Quellsystem braucht einen Anzeigenamen.", Severity.Warning); return; } var duplicates = normalized .GroupBy(x => x.Code) .FirstOrDefault(g => g.Count() > 1); if (duplicates is not null) { Snackbar.Add($"Quellsystem-Code doppelt vorhanden: {duplicates.Key}", Severity.Warning); return; } using var db = await DbFactory.CreateDbContextAsync(); var existing = await db.SourceSystemDefinitions.ToListAsync(); if (existing.Count > 0) db.SourceSystemDefinitions.RemoveRange(existing); db.SourceSystemDefinitions.AddRange(normalized); await db.SaveChangesAsync(); _sourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync(); Snackbar.Add("Quellsysteme gespeichert", Severity.Success); } private void AddExchangeRate() { _exchangeRates.Add(new CurrencyExchangeRate { FromCurrency = "USD", ToCurrency = "EUR", Rate = 1m, ValidFrom = DateTime.Today, IsActive = true }); } private void RemoveExchangeRate(CurrencyExchangeRate rate) { _exchangeRates.Remove(rate); } private async Task SaveExchangeRates() { using var db = await DbFactory.CreateDbContextAsync(); var existingRates = await db.CurrencyExchangeRates.ToListAsync(); if (existingRates.Count > 0) db.CurrencyExchangeRates.RemoveRange(existingRates); db.CurrencyExchangeRates.AddRange(_exchangeRates.Select(rate => new CurrencyExchangeRate { FromCurrency = NormalizeConfigValue(rate.FromCurrency).ToUpperInvariant(), ToCurrency = NormalizeConfigValue(rate.ToCurrency).ToUpperInvariant(), Rate = rate.Rate, ValidFrom = rate.ValidFrom.Date, ValidTo = rate.ValidTo?.Date, Notes = NormalizeConfigValue(rate.Notes), IsActive = rate.IsActive }).Where(rate => !string.IsNullOrWhiteSpace(rate.FromCurrency) && !string.IsNullOrWhiteSpace(rate.ToCurrency) && rate.Rate > 0m)); await db.SaveChangesAsync(); _exchangeRates = await db.CurrencyExchangeRates .OrderBy(x => x.FromCurrency) .ThenBy(x => x.ToCurrency) .ThenByDescending(x => x.ValidFrom) .ToListAsync(); Snackbar.Add("Wechselkurse gespeichert", Severity.Success); } private async Task RefreshEcbRates() { if (_refreshingExchangeRates) return; _refreshingExchangeRates = true; try { var result = await ExchangeRateImportService.RefreshEcbRatesAsync(); _exchangeRates = await LoadExchangeRatesAsync(); Snackbar.Add($"ECB-Kurse aktualisiert: {result.ImportedCount} Kurse vom {result.RateDate:yyyy-MM-dd}.", Severity.Success); } catch (Exception ex) { Snackbar.Add($"ECB-Kursimport fehlgeschlagen: {ex.Message}", Severity.Error); } finally { _refreshingExchangeRates = false; } } private async Task ExportConfiguration() { if (_exportingConfig) return; _exportingConfig = true; try { var json = await ConfigTransferService.ExportJsonAsync(_includeSecretsInExport); var suffix = _includeSecretsInExport ? "with-secrets" : "without-secrets"; var fileName = $"trafag-config-{DateTime.UtcNow:yyyyMMdd-HHmmss}-{suffix}.json"; await JS.InvokeVoidAsync("trafagDownload.saveTextFile", fileName, json, "application/json;charset=utf-8"); Snackbar.Add("Konfiguration exportiert", Severity.Success); } catch (Exception ex) { Snackbar.Add($"Export fehlgeschlagen: {ex.Message}", Severity.Error); } finally { _exportingConfig = false; } } private async Task ImportConfiguration(InputFileChangeEventArgs args) { if (_importingConfig) return; _importingConfig = true; try { var file = args.File; await using var stream = file.OpenReadStream(5 * 1024 * 1024); using var reader = new StreamReader(stream); var json = await reader.ReadToEndAsync(); await ConfigTransferService.ImportJsonAsync(json); using var db = await DbFactory.CreateDbContextAsync(); _spConfig = await db.SharePointConfigs.FirstOrDefaultAsync() ?? new SharePointConfig(); _exportSettings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings(); _sourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync(); _exchangeRates = await db.CurrencyExchangeRates .OrderBy(x => x.FromCurrency) .ThenBy(x => x.ToCurrency) .ThenByDescending(x => x.ValidFrom) .ToListAsync(); TimerService.Recalculate(); Snackbar.Add("Konfiguration importiert", Severity.Success); } catch (Exception ex) { Snackbar.Add($"Import fehlgeschlagen: {ex.Message}", Severity.Error); } finally { _importingConfig = false; } } private async Task TestCentralCredentials(string sourceSystem) { var definition = _sourceSystems.FirstOrDefault(x => string.Equals(x.Code, sourceSystem, StringComparison.OrdinalIgnoreCase)); if (definition is null) { Snackbar.Add($"Quellsystem '{sourceSystem}' nicht gefunden.", Severity.Warning); return; } if (string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase)) { await TestCentralSapCredentials(definition); return; } if (string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.Hana, StringComparison.OrdinalIgnoreCase)) { await TestCentralHanaCredentials(definition); } } private async Task TestCentralHanaCredentials(SourceSystemDefinition definition) { var sourceSystem = definition.Code; if (!_testingSystems.Add(sourceSystem)) return; try { var username = definition.CentralUsername; var password = definition.CentralPassword; if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) { Snackbar.Add($"Für {sourceSystem} sind keine zentralen Zugangsdaten gepflegt.", Severity.Warning); return; } using var db = await DbFactory.CreateDbContextAsync(); var centralServer = await db.HanaServers .Where(s => s.SourceSystem == sourceSystem) .OrderBy(s => s.Id) .FirstOrDefaultAsync(); if (centralServer is null || string.IsNullOrWhiteSpace(centralServer.Host)) { Snackbar.Add($"Keine zentrale HANA-Konfiguration fuer {sourceSystem} gefunden.", Severity.Warning); return; } var testServer = new HanaServer { SourceSystem = sourceSystem, Name = $"{sourceSystem} Central Test", Host = centralServer.Host, Port = centralServer.Port, Username = username.Trim(), Password = password.Trim(), DatabaseName = centralServer.DatabaseName, UseSsl = centralServer.UseSsl, ValidateCertificate = centralServer.ValidateCertificate, AdditionalParams = centralServer.AdditionalParams }; var result = await Task.Run(() => HanaService.TestConnectionDetailed(testServer)); if (result.Success) { Snackbar.Add($"{sourceSystem}: Zentrale HANA-Verbindung erfolgreich.", Severity.Success); } else { Snackbar.Add($"{sourceSystem}: {result.ExceptionType} - {result.ErrorMessage}", Severity.Error); } } finally { _testingSystems.Remove(sourceSystem); } } private async Task TestCentralSapCredentials(SourceSystemDefinition definition) { var sourceSystem = definition.Code; if (!_testingSystems.Add(sourceSystem)) return; try { var username = definition.CentralUsername; var password = definition.CentralPassword; if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) { Snackbar.Add("Für SAP sind keine zentralen Gateway-Zugangsdaten gepflegt.", Severity.Warning); return; } if (string.IsNullOrWhiteSpace(definition.CentralServiceUrl)) { Snackbar.Add($"Fuer {sourceSystem} ist keine zentrale SAP Service URL gepflegt.", Severity.Warning); return; } await SapGatewayService.TestConnectionAsync(definition.CentralServiceUrl, username.Trim(), password.Trim()); Snackbar.Add($"{sourceSystem}: Zentrale SAP Gateway-Verbindung erfolgreich.", Severity.Success); } catch (Exception ex) { Snackbar.Add($"{sourceSystem}: {ex.Message}", Severity.Error); } finally { _testingSystems.Remove(sourceSystem); } } private static string NormalizeSourceSystemCode(string? code) => NormalizeConfigValue(code).ToUpperInvariant(); private static string NormalizeConnectionKind(string? connectionKind) => SourceSystemConnectionKinds.All.Contains(connectionKind ?? string.Empty, StringComparer.OrdinalIgnoreCase) ? (connectionKind ?? string.Empty).Trim().ToUpperInvariant() : SourceSystemConnectionKinds.Hana; private static string GetConnectionKindLabel(string connectionKind) => connectionKind switch { SourceSystemConnectionKinds.Hana => "HANA", SourceSystemConnectionKinds.SapGateway => "SAP Gateway", SourceSystemConnectionKinds.ManualExcel => "Manual Excel", _ => connectionKind }; private static bool UsesManualImport(SourceSystemDefinition definition) => string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.ManualExcel, StringComparison.OrdinalIgnoreCase); private static bool UsesSapGateway(SourceSystemDefinition definition) => string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase); private static string GetServiceUrlSummary(SourceSystemDefinition definition) => string.IsNullOrWhiteSpace(definition.CentralServiceUrl) ? "-" : definition.CentralServiceUrl; private static string GetUsernameSummary(SourceSystemDefinition definition) => string.IsNullOrWhiteSpace(definition.CentralUsername) ? "-" : definition.CentralUsername; private static string NormalizeConfigValue(string? value) => value?.Trim() ?? string.Empty; private static string BuildSharePointTestPreview(string tenantId, string clientId, string clientSecret, string siteUrl) { var maskedSecret = string.IsNullOrEmpty(clientSecret) ? "" : $"{new string('*', Math.Min(clientSecret.Length, 8))} (len={clientSecret.Length})"; return string.Join(Environment.NewLine, [ $"Tenant ID: {tenantId}", $"Client ID: {clientId}", $"Client Secret: {maskedSecret}", $"Site URL: {siteUrl}" ]); } private async Task> LoadExchangeRatesAsync() { using var db = await DbFactory.CreateDbContextAsync(); return await db.CurrencyExchangeRates .OrderBy(x => x.FromCurrency) .ThenBy(x => x.ToCurrency) .ThenByDescending(x => x.ValidFrom) .ToListAsync(); } }