From 2a56ba53bad201f9bd64461dd9920cebc3f6c8a8 Mon Sep 17 00:00:00 2001 From: metacube Date: Fri, 17 Apr 2026 13:56:41 +0200 Subject: [PATCH] umfangreiches refactoring --- .../Components/Pages/Dashboard.razor | 116 +-- .../Components/Pages/Logs.razor | 42 +- .../Components/Pages/ManagementCockpit.razor | 17 +- .../Components/Pages/Settings.razor | 292 +----- .../Components/Pages/Standorte.razor | 662 ++----------- .../Components/Pages/Transformations.razor | 17 +- TrafagSalesExporter/Program.cs | 9 + .../Services/DashboardPageService.cs | 144 +++ ...DatabaseInitializationService.SchemaSql.cs | 152 +++ .../Services/DatabaseInitializationService.cs | 882 +----------------- .../DatabaseSchemaMaintenanceService.cs | 440 +++++++++ .../Services/DatabaseSeedService.cs | 225 +++++ .../IDatabaseSchemaMaintenanceService.cs | 8 + .../Services/IDatabaseSeedService.cs | 8 + .../Services/LogsPageService.cs | 69 ++ .../Services/ManagementCockpitPageService.cs | 56 ++ .../Services/SettingsPageService.cs | 324 +++++++ .../Services/StandortePageService.cs | 522 +++++++++++ .../Services/StandorteSapEditorService.cs | 240 +++++ .../Services/TransformationsPageService.cs | 54 ++ .../DatabaseInitializationServiceTests.cs | 27 +- 21 files changed, 2401 insertions(+), 1905 deletions(-) create mode 100644 TrafagSalesExporter/Services/DashboardPageService.cs create mode 100644 TrafagSalesExporter/Services/DatabaseInitializationService.SchemaSql.cs create mode 100644 TrafagSalesExporter/Services/DatabaseSchemaMaintenanceService.cs create mode 100644 TrafagSalesExporter/Services/DatabaseSeedService.cs create mode 100644 TrafagSalesExporter/Services/IDatabaseSchemaMaintenanceService.cs create mode 100644 TrafagSalesExporter/Services/IDatabaseSeedService.cs create mode 100644 TrafagSalesExporter/Services/LogsPageService.cs create mode 100644 TrafagSalesExporter/Services/ManagementCockpitPageService.cs create mode 100644 TrafagSalesExporter/Services/SettingsPageService.cs create mode 100644 TrafagSalesExporter/Services/StandortePageService.cs create mode 100644 TrafagSalesExporter/Services/StandorteSapEditorService.cs create mode 100644 TrafagSalesExporter/Services/TransformationsPageService.cs diff --git a/TrafagSalesExporter/Components/Pages/Dashboard.razor b/TrafagSalesExporter/Components/Pages/Dashboard.razor index 97d5af5..6618acb 100644 --- a/TrafagSalesExporter/Components/Pages/Dashboard.razor +++ b/TrafagSalesExporter/Components/Pages/Dashboard.razor @@ -1,9 +1,7 @@ @page "/" -@using Microsoft.EntityFrameworkCore @using System.Diagnostics -@using TrafagSalesExporter.Data @using TrafagSalesExporter.Services -@inject IDbContextFactory DbFactory +@inject IDashboardPageService DashboardPageActions @inject ExportOrchestrationService Orchestrator @inject TimerBackgroundService TimerService @inject ISnackbar Snackbar @@ -170,49 +168,9 @@ private async Task LoadDataAsync() { _loading = true; - using var db = await DbFactory.CreateDbContextAsync(); - - var sites = await db.Sites.Include(s => s.HanaServer).Where(s => s.IsActive).ToListAsync(); - var sourceSystems = await db.SourceSystemDefinitions.AsNoTracking().ToListAsync(); - var logs = await db.ExportLogs - .GroupBy(l => l.SiteId) - .Select(g => g.OrderByDescending(l => l.Timestamp).First()) - .ToListAsync(); - var appLogs = await db.AppEventLogs - .Where(l => l.SiteId != null) - .OrderByDescending(l => l.Timestamp) - .Take(1000) - .ToListAsync(); - var latestAppLogsBySite = appLogs - .GroupBy(l => l.SiteId!.Value) - .ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.Timestamp).First()); - - _dashboardRows = sites.Select(s => - { - var log = logs.FirstOrDefault(l => l.SiteId == s.Id); - latestAppLogsBySite.TryGetValue(s.Id, out var appLog); - var sourceSystem = sourceSystems.FirstOrDefault(x => string.Equals(x.Code, s.SourceSystem, StringComparison.OrdinalIgnoreCase)); - return new DashboardRow - { - SiteId = s.Id, - Land = s.Land, - TSC = s.TSC, - Schema = s.Schema, - ServerName = string.Equals(sourceSystem?.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase) - ? ResolveDashboardSapServiceUrl(s, sourceSystems) - : s.HanaServer?.Name ?? "", - LastStatus = log?.Status ?? "", - RowCount = log?.RowCount ?? 0, - LastRun = log?.Timestamp, - DurationSeconds = log?.DurationSeconds ?? 0, - ErrorMessage = log?.ErrorMessage ?? "", - FilePath = log?.FilePath ?? "", - LiveMessage = appLog is null ? string.Empty : $"{appLog.Category}: {appLog.Message}", - LiveDetails = appLog?.Details ?? "" - }; - }).ToList(); - - _consolidatedRows = BuildConsolidatedRows(settings: await db.ExportSettings.FirstOrDefaultAsync() ?? new()); + var state = await DashboardPageActions.LoadAsync(); + _dashboardRows = state.DashboardRows; + _consolidatedRows = state.ConsolidatedRows; _anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || Orchestrator.IsConsolidatedExporting(); _loading = false; @@ -321,15 +279,6 @@ OpenFile(row.FilePath); } - private static string ResolveDashboardSapServiceUrl(Site site, List sourceSystems) - { - if (!string.IsNullOrWhiteSpace(site.SapServiceUrl)) - return site.SapServiceUrl; - - var sourceSystem = sourceSystems.FirstOrDefault(x => string.Equals(x.Code, site.SourceSystem, StringComparison.OrdinalIgnoreCase)); - return string.IsNullOrWhiteSpace(sourceSystem?.CentralServiceUrl) ? "SAP Gateway" : sourceSystem.CentralServiceUrl; - } - private void OpenFile(string filePath) { if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath)) @@ -417,63 +366,6 @@ return Task.CompletedTask; } - private static List BuildConsolidatedRows(ExportSettings settings) - { - var outputDirectory = ResolveConsolidatedOutputDirectory(settings); - if (!Directory.Exists(outputDirectory)) - return []; - - return Directory.GetFiles(outputDirectory, "Sales_All_*.xlsx") - .Select(path => new FileInfo(path)) - .OrderByDescending(file => file.LastWriteTime) - .Take(1) - .Select(file => new ConsolidatedDashboardRow - { - Label = "Konsolidierter Export", - FilePath = file.FullName, - DisplayPath = file.FullName, - LastModified = file.LastWriteTime - }) - .ToList(); - } - - private static string ResolveConsolidatedOutputDirectory(ExportSettings settings) - { - if (!string.IsNullOrWhiteSpace(settings.LocalConsolidatedExportFolder)) - return settings.LocalConsolidatedExportFolder.Trim(); - - if (!string.IsNullOrWhiteSpace(settings.LocalSiteExportFolder)) - return settings.LocalSiteExportFolder.Trim(); - - return Path.Combine(AppContext.BaseDirectory, "output"); - } - - private class DashboardRow - { - public int SiteId { get; set; } - public string Land { get; set; } = ""; - public string TSC { get; set; } = ""; - public string Schema { get; set; } = ""; - public string ServerName { get; set; } = ""; - public string LastStatus { get; set; } = ""; - public int RowCount { get; set; } - public DateTime? LastRun { get; set; } - public double DurationSeconds { get; set; } - public string ErrorMessage { get; set; } = ""; - public string FilePath { get; set; } = ""; - public string LiveMessage { get; set; } = ""; - public string LiveDetails { get; set; } = ""; - public bool HasOpenableFile => !string.IsNullOrWhiteSpace(FilePath) && File.Exists(FilePath); - } - - private class ConsolidatedDashboardRow - { - public string Label { get; set; } = ""; - public string FilePath { get; set; } = ""; - public string DisplayPath { get; set; } = ""; - public DateTime? LastModified { get; set; } - public bool HasOpenableFile => !string.IsNullOrWhiteSpace(FilePath) && File.Exists(FilePath); - } } @code { diff --git a/TrafagSalesExporter/Components/Pages/Logs.razor b/TrafagSalesExporter/Components/Pages/Logs.razor index 0659994..06867e1 100644 --- a/TrafagSalesExporter/Components/Pages/Logs.razor +++ b/TrafagSalesExporter/Components/Pages/Logs.razor @@ -1,7 +1,6 @@ @page "/logs" -@using Microsoft.EntityFrameworkCore -@using TrafagSalesExporter.Data -@inject IDbContextFactory DbFactory +@using TrafagSalesExporter.Services +@inject ILogsPageService LogsPageActions @inject ISnackbar Snackbar @inject IDialogService DialogService @inject TrafagSalesExporter.Services.IUiTextService UiText @@ -117,37 +116,16 @@ protected override async Task OnInitializedAsync() { - using var db = await DbFactory.CreateDbContextAsync(); - _availableLands = await db.ExportLogs.Select(l => l.Land).Distinct().OrderBy(l => l).ToListAsync(); await LoadLogsAsync(); } private async Task LoadLogsAsync() { _loading = true; - using var db = await DbFactory.CreateDbContextAsync(); - IQueryable query = db.ExportLogs.OrderByDescending(l => l.Timestamp); - - if (!string.IsNullOrEmpty(_filterLand)) - query = query.Where(l => l.Land == _filterLand); - - if (!string.IsNullOrEmpty(_filterStatus)) - query = query.Where(l => l.Status == _filterStatus); - - if (_filterDate.HasValue) - query = query.Where(l => l.Timestamp.Date == _filterDate.Value.Date); - - _logs = await query.Take(500).ToListAsync(); - - IQueryable appLogQuery = db.AppEventLogs.OrderByDescending(l => l.Timestamp); - - if (!string.IsNullOrEmpty(_filterLand)) - appLogQuery = appLogQuery.Where(l => l.Land == _filterLand); - - if (_filterDate.HasValue) - appLogQuery = appLogQuery.Where(l => l.Timestamp.Date == _filterDate.Value.Date); - - _appLogs = await appLogQuery.Take(500).ToListAsync(); + var state = await LogsPageActions.LoadAsync(_filterLand, _filterStatus, _filterDate); + _availableLands = state.AvailableLands; + _logs = state.Logs; + _appLogs = state.AppLogs; _loading = false; } @@ -165,13 +143,9 @@ if (result != true) return; - using var db = await DbFactory.CreateDbContextAsync(); - var cutoff = DateTime.Now.AddDays(-90); - var oldLogs = await db.ExportLogs.Where(l => l.Timestamp < cutoff).ToListAsync(); - db.ExportLogs.RemoveRange(oldLogs); - var count = await db.SaveChangesAsync(); + var deletedCount = await LogsPageActions.DeleteOldLogsAsync(90); await LoadLogsAsync(); - Snackbar.Add(string.Format(T("{0} alte Logs geloescht", "{0} old logs deleted"), oldLogs.Count), Severity.Info); + Snackbar.Add(string.Format(T("{0} alte Logs geloescht", "{0} old logs deleted"), deletedCount), Severity.Info); } } diff --git a/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor b/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor index 9fb496f..548dfb0 100644 --- a/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor +++ b/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor @@ -1,7 +1,7 @@ @page "/management-cockpit" @using TrafagSalesExporter.Models @using TrafagSalesExporter.Services -@inject IManagementCockpitService CockpitService +@inject IManagementCockpitPageService CockpitPageService @inject ISnackbar Snackbar @inject IUiTextService UiText @@ -335,8 +335,11 @@ protected override async Task OnInitializedAsync() { - await ReloadFiles(); - await ReloadCentralYears(); + var state = await CockpitPageService.InitializeAsync(_selectedFilePath, _selectedCentralYear); + _files = state.Files; + _centralYears = state.CentralYears; + _selectedFilePath = state.SelectedFilePath; + _selectedCentralYear = state.SelectedCentralYear; } private async Task ReloadFiles() @@ -344,7 +347,7 @@ _loadingFiles = true; try { - _files = await CockpitService.GetAvailableFilesAsync(); + _files = await CockpitPageService.LoadFilesAsync(); _selectedFilePath ??= _files.FirstOrDefault()?.Path; } finally @@ -355,7 +358,7 @@ private async Task ReloadCentralYears() { - _centralYears = await CockpitService.GetAvailableCentralYearsAsync(); + _centralYears = await CockpitPageService.LoadCentralYearsAsync(); if (_selectedCentralYear == 0) _selectedCentralYear = _centralYears.LastOrDefault(); } @@ -368,7 +371,7 @@ _analyzing = true; try { - _result = await CockpitService.AnalyzeAsync(_selectedFilePath); + _result = await CockpitPageService.AnalyzeAsync(_selectedFilePath); } catch (Exception ex) { @@ -388,7 +391,7 @@ _analyzingCentral = true; try { - _centralResult = await CockpitService.AnalyzeCentralAsync(_selectedCentralYear, _selectedCentralMonth); + _centralResult = await CockpitPageService.AnalyzeCentralAsync(_selectedCentralYear, _selectedCentralMonth); } catch (Exception ex) { diff --git a/TrafagSalesExporter/Components/Pages/Settings.razor b/TrafagSalesExporter/Components/Pages/Settings.razor index abc1455..3da78d6 100644 --- a/TrafagSalesExporter/Components/Pages/Settings.razor +++ b/TrafagSalesExporter/Components/Pages/Settings.razor @@ -1,15 +1,7 @@ @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 ISettingsPageService SettingsPageActions @inject IJSRuntime JS @inject ISnackbar Snackbar @@ -328,35 +320,16 @@ 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(); + var state = await SettingsPageActions.LoadAsync(); + _spConfig = state.SharePointConfig; + _exportSettings = state.ExportSettings; + _sourceSystems = state.SourceSystems; + _exchangeRates = state.ExchangeRates; } 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(); + await SettingsPageActions.SaveSharePointAsync(_spConfig); Snackbar.Add("SharePoint Konfiguration gespeichert", Severity.Success); } @@ -365,15 +338,7 @@ _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); + _sharePointTestPreview = await SettingsPageActions.BuildSharePointTestPreviewAsync(_spConfig); Snackbar.Add("SharePoint Verbindung erfolgreich!", Severity.Success); } catch (Exception ex) @@ -388,24 +353,7 @@ 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(); + await SettingsPageActions.SaveExportSettingsAsync(_exportSettings); Snackbar.Add("Export Einstellungen gespeichert", Severity.Success); } @@ -493,46 +441,15 @@ 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))) + try { - Snackbar.Add("Jedes Quellsystem braucht einen Anzeigenamen.", Severity.Warning); - return; + _sourceSystems = await SettingsPageActions.SaveSourceSystemsAsync(_sourceSystems); + Snackbar.Add("Quellsysteme gespeichert", Severity.Success); } - - var duplicates = normalized - .GroupBy(x => x.Code) - .FirstOrDefault(g => g.Count() > 1); - if (duplicates is not null) + catch (Exception ex) { - Snackbar.Add($"Quellsystem-Code doppelt vorhanden: {duplicates.Key}", Severity.Warning); - return; + Snackbar.Add(ex.Message, Severity.Warning); } - - 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() @@ -554,32 +471,7 @@ 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(); - + _exchangeRates = await SettingsPageActions.SaveExchangeRatesAsync(_exchangeRates); Snackbar.Add("Wechselkurse gespeichert", Severity.Success); } @@ -591,8 +483,8 @@ _refreshingExchangeRates = true; try { - var result = await ExchangeRateImportService.RefreshEcbRatesAsync(); - _exchangeRates = await LoadExchangeRatesAsync(); + var result = await SettingsPageActions.RefreshEcbRatesAsync(); + _exchangeRates = result.ExchangeRates; Snackbar.Add($"ECB-Kurse aktualisiert: {result.ImportedCount} Kurse vom {result.RateDate:yyyy-MM-dd}.", Severity.Success); } catch (Exception ex) @@ -613,7 +505,7 @@ _exportingConfig = true; try { - var json = await ConfigTransferService.ExportJsonAsync(_includeSecretsInExport); + var json = await SettingsPageActions.ExportConfigurationAsync(_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"); @@ -641,18 +533,11 @@ 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(); + var state = await SettingsPageActions.ImportConfigurationAsync(json); + _spConfig = state.SharePointConfig; + _exportSettings = state.ExportSettings; + _sourceSystems = state.SourceSystems; + _exchangeRates = state.ExchangeRates; Snackbar.Add("Konfiguration importiert", Severity.Success); } catch (Exception ex) @@ -674,70 +559,13 @@ 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); - } + var result = await SettingsPageActions.TestCentralCredentialsAsync(definition); + Snackbar.Add(result.Message, result.Success ? Severity.Success : result.Warning ? Severity.Warning : Severity.Error); } finally { @@ -745,48 +573,9 @@ } } - private async Task TestCentralSapCredentials(SourceSystemDefinition definition) - { - var sourceSystem = definition.Code; - if (!_testingSystems.Add(sourceSystem)) - return; + private static string NormalizeSourceSystemCode(string? code) => Services.SettingsPageService.NormalizeSourceSystemCode(code); - 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 NormalizeConnectionKind(string? connectionKind) => Services.SettingsPageService.NormalizeConnectionKind(connectionKind); private static string GetConnectionKindLabel(string connectionKind) => connectionKind switch { @@ -808,31 +597,6 @@ 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(); - } + private static string NormalizeConfigValue(string? value) => Services.SettingsPageService.NormalizeConfigValue(value); } diff --git a/TrafagSalesExporter/Components/Pages/Standorte.razor b/TrafagSalesExporter/Components/Pages/Standorte.razor index 794b698..613ca4c 100644 --- a/TrafagSalesExporter/Components/Pages/Standorte.razor +++ b/TrafagSalesExporter/Components/Pages/Standorte.razor @@ -1,16 +1,11 @@ @page "/standorte" @using Microsoft.AspNetCore.Components.Forms -@using Microsoft.EntityFrameworkCore @using System.Text.Json @using System.Reflection -@using TrafagSalesExporter.Data @using TrafagSalesExporter.Models @using TrafagSalesExporter.Services -@inject IDbContextFactory DbFactory -@inject IHanaQueryService HanaService -@inject ISapGatewayService SapGatewayService -@inject ISharePointUploadService SharePointService -@inject IAppEventLogService AppEventLogService +@inject IStandortePageService StandortePageService +@inject IStandorteSapEditorService SapEditorService @inject ISnackbar Snackbar @inject IDialogService DialogService @@ -466,16 +461,10 @@ private async Task LoadDataAsync() { - using var db = await DbFactory.CreateDbContextAsync(); - _sourceSystemDefinitions = await db.SourceSystemDefinitions - .OrderBy(x => x.Code) - .ToListAsync(); - _servers = await db.HanaServers - .Where(s => GetHanaSourceSystemCodes().Contains(s.SourceSystem)) - .OrderBy(s => s.SourceSystem) - .ThenBy(s => s.Name) - .ToListAsync(); - _sites = await db.Sites.Include(s => s.HanaServer).OrderBy(s => s.Land).ToListAsync(); + var state = await StandortePageService.LoadAsync(); + _sourceSystemDefinitions = state.SourceSystems; + _servers = state.Servers; + _sites = state.Sites; } private void EditServer(HanaServer server) @@ -492,61 +481,10 @@ _savingServer = true; try { - _editingServer.SourceSystem = string.IsNullOrWhiteSpace(_editingServer.SourceSystem) - ? GetHanaSourceSystemCodes().FirstOrDefault() ?? string.Empty - : _editingServer.SourceSystem.Trim().ToUpperInvariant(); - _editingServer.Name = string.IsNullOrWhiteSpace(_editingServer.Name) ? _editingServer.SourceSystem : _editingServer.Name.Trim(); - _editingServer.Host = _editingServer.Host.Trim(); - _editingServer.DatabaseName = _editingServer.DatabaseName.Trim(); - _editingServer.AdditionalParams = _editingServer.AdditionalParams.Trim(); - _editingServer.Username = string.Empty; - _editingServer.Password = string.Empty; - using var db = await DbFactory.CreateDbContextAsync(); - if (_editingServer.Id == 0) - { - var existingForSourceSystem = await db.HanaServers - .OrderBy(x => x.Id) - .FirstOrDefaultAsync(x => x.SourceSystem == _editingServer.SourceSystem); - - if (existingForSourceSystem is null) - { - db.HanaServers.Add(_editingServer); - } - else - { - existingForSourceSystem.Name = _editingServer.Name; - existingForSourceSystem.Host = _editingServer.Host; - existingForSourceSystem.Port = _editingServer.Port; - existingForSourceSystem.Username = string.Empty; - existingForSourceSystem.Password = string.Empty; - existingForSourceSystem.DatabaseName = _editingServer.DatabaseName; - existingForSourceSystem.UseSsl = _editingServer.UseSsl; - existingForSourceSystem.ValidateCertificate = _editingServer.ValidateCertificate; - existingForSourceSystem.AdditionalParams = _editingServer.AdditionalParams; - } - } - else - { - var existing = await db.HanaServers.FindAsync(_editingServer.Id); - if (existing is not null) - { - existing.SourceSystem = _editingServer.SourceSystem; - existing.Name = _editingServer.Name; - existing.Host = _editingServer.Host; - existing.Port = _editingServer.Port; - existing.Username = string.Empty; - existing.Password = string.Empty; - existing.DatabaseName = _editingServer.DatabaseName; - existing.UseSsl = _editingServer.UseSsl; - existing.ValidateCertificate = _editingServer.ValidateCertificate; - existing.AdditionalParams = _editingServer.AdditionalParams; - } - } - - await db.SaveChangesAsync(); - _serverDialogVisible = false; - await LoadDataAsync(); - Snackbar.Add("Server gespeichert", Severity.Success); + await StandortePageService.SaveServerAsync(_editingServer, GetHanaSourceSystemCodes()); + _serverDialogVisible = false; + await LoadDataAsync(); + Snackbar.Add("Server gespeichert", Severity.Success); } finally { @@ -571,27 +509,7 @@ try { - using var db = await DbFactory.CreateDbContextAsync(); - var linkedSites = await db.Sites - .Where(s => s.HanaServerId == server.Id) - .OrderBy(s => s.Land) - .Select(s => $"{s.Land} ({s.TSC})") - .ToListAsync(); - - if (linkedSites.Count > 0) - { - Snackbar.Add( - $"Server kann nicht gelöscht werden. Noch verknüpfte Standorte: {string.Join(", ", linkedSites)}", - Severity.Warning); - return; - } - - var entity = await db.HanaServers.FindAsync(server.Id); - if (entity is not null) - { - db.HanaServers.Remove(entity); - await db.SaveChangesAsync(); - } + await StandortePageService.DeleteServerAsync(server); } catch (Exception ex) { @@ -605,50 +523,19 @@ private async Task TestServerConnection(HanaServer server) { - using var db = await DbFactory.CreateDbContextAsync(); - var sourceDefinition = await db.SourceSystemDefinitions - .OrderBy(x => x.Id) - .FirstOrDefaultAsync(x => x.Code == server.SourceSystem); - - if (sourceDefinition is null) + try { - Snackbar.Add($"Quellsystem '{server.SourceSystem}' nicht gefunden.", Severity.Warning); - return; + var result = await StandortePageService.TestServerConnectionAsync(server); + _connectionStatus[server.Id] = result; + Snackbar.Add( + result.Success + ? $"Verbindung zu '{server.Name}' erfolgreich." + : $"{server.Name}: {result.ExceptionType} - {result.ErrorMessage}", + result.Success ? Severity.Success : Severity.Error); } - - if (string.IsNullOrWhiteSpace(sourceDefinition.CentralUsername) || string.IsNullOrWhiteSpace(sourceDefinition.CentralPassword)) + catch (Exception ex) { - Snackbar.Add($"Fuer {server.SourceSystem} sind keine zentralen Zugangsdaten im Quellsystem gepflegt.", Severity.Warning); - return; - } - - var testServer = new HanaServer - { - Id = server.Id, - SourceSystem = server.SourceSystem, - Name = server.Name, - Host = server.Host, - Port = server.Port, - Username = sourceDefinition.CentralUsername.Trim(), - Password = sourceDefinition.CentralPassword, - DatabaseName = server.DatabaseName, - UseSsl = server.UseSsl, - ValidateCertificate = server.ValidateCertificate, - AdditionalParams = server.AdditionalParams - }; - - await AppEventLogService.WriteAsync("HANA", "Server-Test aus UI gestartet", - details: testServer.GetConnectionStringPreview()); - var result = await Task.Run(() => HanaService.TestConnectionDetailed(testServer)); - _connectionStatus[server.Id] = result; - - if (result.Success) - { - Snackbar.Add($"Verbindung zu '{server.Name}' erfolgreich.", Severity.Success); - } - else - { - Snackbar.Add($"{server.Name}: {result.ExceptionType} - {result.ErrorMessage}", Severity.Error); + Snackbar.Add(ex.Message, Severity.Warning); } } @@ -682,33 +569,18 @@ private void EditSite(Site site) { - _editingSite = new Site - { - Id = site.Id, - HanaServerId = site.HanaServerId, - Schema = site.Schema, - TSC = site.TSC, - Land = site.Land, - SourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) - ? GetAvailableSourceSystems().FirstOrDefault()?.Code ?? "SAP" - : site.SourceSystem, - UsernameOverride = site.UsernameOverride, - PasswordOverride = site.PasswordOverride, - LocalExportFolderOverride = site.LocalExportFolderOverride, - ManualImportFilePath = site.ManualImportFilePath, - ManualImportLastUploadedAtUtc = site.ManualImportLastUploadedAtUtc, - SapServiceUrl = site.SapServiceUrl, - SapEntitySet = site.SapEntitySet, - SapEntitySetsCache = site.SapEntitySetsCache, - SapEntitySetsRefreshedAtUtc = site.SapEntitySetsRefreshedAtUtc, - IsActive = site.IsActive - }; + _ = EditSiteAsync(site); + } + + private async Task EditSiteAsync(Site site) + { + var editorState = await StandortePageService.LoadSiteEditorAsync(site, GetAvailableSourceSystems()); + _editingSite = editorState.Site; _availableSchemas = []; - _sapEntitySetsCache = ParseSapEntitySets(site.SapEntitySetsCache); - using var db = DbFactory.CreateDbContext(); - _sapSources = db.SapSourceDefinitions.Where(s => s.SiteId == site.Id).OrderBy(s => s.SortOrder).ThenBy(s => s.Id).ToList(); - _sapJoins = db.SapJoinDefinitions.Where(j => j.SiteId == site.Id).OrderBy(j => j.SortOrder).ThenBy(j => j.Id).ToList(); - _sapMappings = db.SapFieldMappings.Where(m => m.SiteId == site.Id).OrderBy(m => m.SortOrder).ThenBy(m => m.Id).ToList(); + _sapEntitySetsCache = editorState.SapEntitySets; + _sapSources = editorState.SapSources; + _sapJoins = editorState.SapJoins; + _sapMappings = editorState.SapMappings; _sapAvailableSourceExpressions = BuildSourceExpressionsFromMappings(); _sapSourceFieldMap = BuildSourceFieldMapFromJoins(); _siteDialogVisible = true; @@ -722,40 +594,7 @@ _savingSite = true; try { - using var db = await DbFactory.CreateDbContextAsync(); - var serverId = UsesHanaConnection() ? await ResolveCentralHanaServerIdAsync(db, _editingSite.SourceSystem) : (int?)null; - _editingSite.HanaServerId = serverId; - _editingSite.SapEntitySetsCache = SerializeSapEntitySets(_sapEntitySetsCache); - - if (_editingSite.Id == 0) - { - db.Sites.Add(_editingSite); - } - else - { - var existing = await db.Sites.FindAsync(_editingSite.Id); - if (existing is not null) - { - existing.HanaServerId = serverId; - existing.Schema = _editingSite.Schema; - existing.TSC = _editingSite.TSC; - existing.Land = _editingSite.Land; - existing.SourceSystem = _editingSite.SourceSystem; - existing.UsernameOverride = _editingSite.UsernameOverride; - existing.PasswordOverride = _editingSite.PasswordOverride; - existing.LocalExportFolderOverride = _editingSite.LocalExportFolderOverride; - existing.ManualImportFilePath = _editingSite.ManualImportFilePath; - existing.ManualImportLastUploadedAtUtc = _editingSite.ManualImportLastUploadedAtUtc; - existing.SapServiceUrl = _editingSite.SapServiceUrl; - existing.SapEntitySet = _editingSite.SapEntitySet; - existing.SapEntitySetsCache = _editingSite.SapEntitySetsCache; - existing.SapEntitySetsRefreshedAtUtc = _editingSite.SapEntitySetsRefreshedAtUtc; - existing.IsActive = _editingSite.IsActive; - } - } - - await db.SaveChangesAsync(); - await SaveSapConfigurationAsync(db, _editingSite.Id); + await StandortePageService.SaveSiteAsync(_editingSite, UsesHanaConnection(), IsSapSite(), _sapSources, _sapJoins, _sapMappings, _sapEntitySetsCache); _siteDialogVisible = false; await LoadDataAsync(); Snackbar.Add("Standort gespeichert", Severity.Success); @@ -779,22 +618,7 @@ if (result != true) return; - using var db = await DbFactory.CreateDbContextAsync(); - var entity = await db.Sites.FindAsync(site.Id); - if (entity is not null) - { - var sources = await db.SapSourceDefinitions.Where(s => s.SiteId == site.Id).ToListAsync(); - var joins = await db.SapJoinDefinitions.Where(j => j.SiteId == site.Id).ToListAsync(); - var mappings = await db.SapFieldMappings.Where(m => m.SiteId == site.Id).ToListAsync(); - var centralRows = await db.CentralSalesRecords.Where(r => r.SiteId == site.Id).ToListAsync(); - if (sources.Count > 0) db.SapSourceDefinitions.RemoveRange(sources); - if (joins.Count > 0) db.SapJoinDefinitions.RemoveRange(joins); - if (mappings.Count > 0) db.SapFieldMappings.RemoveRange(mappings); - if (centralRows.Count > 0) db.CentralSalesRecords.RemoveRange(centralRows); - db.Sites.Remove(entity); - await db.SaveChangesAsync(); - } - + await StandortePageService.DeleteSiteAsync(site); await LoadDataAsync(); Snackbar.Add("Standort gelöscht", Severity.Info); } @@ -825,26 +649,6 @@ }; } - private async Task ResolveCentralHanaServerIdAsync(AppDbContext db, string sourceSystem) - { - _editingSite.UsernameOverride = _editingSite.UsernameOverride.Trim(); - _editingSite.PasswordOverride = _editingSite.PasswordOverride.Trim(); - _editingSite.LocalExportFolderOverride = _editingSite.LocalExportFolderOverride.Trim(); - _editingSite.ManualImportFilePath = _editingSite.ManualImportFilePath.Trim(); - _editingSite.SapServiceUrl = _editingSite.SapServiceUrl.Trim(); - _editingSite.SapEntitySet = _editingSite.SapEntitySet.Trim(); - - var normalizedSourceSystem = string.IsNullOrWhiteSpace(sourceSystem) ? string.Empty : sourceSystem.Trim().ToUpperInvariant(); - var centralServer = await db.HanaServers - .OrderBy(x => x.Id) - .FirstOrDefaultAsync(x => x.SourceSystem == normalizedSourceSystem); - - if (centralServer is null || string.IsNullOrWhiteSpace(centralServer.Host)) - throw new InvalidOperationException($"Fuer Quellsystem '{normalizedSourceSystem}' ist keine gueltige zentrale HANA-Konfiguration vorhanden."); - - return centralServer.Id; - } - private Task OnSchemaSelected(string schema) { _editingSite.Schema = schema; @@ -939,52 +743,7 @@ _loadingSchemas = true; try { - using var db = await DbFactory.CreateDbContextAsync(); - var sourceDefinition = await db.SourceSystemDefinitions - .OrderBy(x => x.Id) - .FirstOrDefaultAsync(x => x.Code == _editingSite.SourceSystem); - - if (sourceDefinition is null) - throw new InvalidOperationException($"Quellsystem '{_editingSite.SourceSystem}' nicht gefunden."); - - var centralServer = await db.HanaServers - .OrderBy(x => x.Id) - .FirstOrDefaultAsync(x => x.SourceSystem == _editingSite.SourceSystem); - - if (centralServer is null || string.IsNullOrWhiteSpace(centralServer.Host)) - throw new InvalidOperationException($"Fuer {_editingSite.SourceSystem} ist keine gueltige zentrale HANA-Konfiguration vorhanden."); - - var username = string.IsNullOrWhiteSpace(_editingSite.UsernameOverride) - ? sourceDefinition.CentralUsername ?? string.Empty - : _editingSite.UsernameOverride; - var password = string.IsNullOrWhiteSpace(_editingSite.PasswordOverride) - ? sourceDefinition.CentralPassword ?? string.Empty - : _editingSite.PasswordOverride; - - if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) - throw new InvalidOperationException($"Fuer {_editingSite.SourceSystem} sind weder zentrale Zugangsdaten noch Standort-Overrides gesetzt."); - - var lookupServer = new HanaServer - { - Id = centralServer.Id, - SourceSystem = centralServer.SourceSystem, - Name = centralServer.Name, - Host = centralServer.Host, - Port = centralServer.Port, - Username = username.Trim(), - Password = password, - DatabaseName = centralServer.DatabaseName, - UseSsl = centralServer.UseSsl, - ValidateCertificate = centralServer.ValidateCertificate, - AdditionalParams = centralServer.AdditionalParams - }; - - var schemas = await Task.Run(() => HanaService.GetAvailableSchemas(lookupServer)); - _availableSchemas = schemas - .Where(x => !string.IsNullOrWhiteSpace(x)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) - .ToList(); + _availableSchemas = await StandortePageService.LoadAvailableSchemasAsync(_editingSite); if (_availableSchemas.Count == 0) { @@ -1018,31 +777,10 @@ _refreshingSapEntitySets = true; try { - using var db = await DbFactory.CreateDbContextAsync(); - var sourceDefinition = await db.SourceSystemDefinitions - .OrderBy(x => x.Id) - .FirstOrDefaultAsync(x => x.Code == _editingSite.SourceSystem); - var serviceUrl = string.IsNullOrWhiteSpace(_editingSite.SapServiceUrl) - ? sourceDefinition?.CentralServiceUrl ?? string.Empty - : _editingSite.SapServiceUrl; - if (string.IsNullOrWhiteSpace(serviceUrl)) - throw new InvalidOperationException("Es ist weder eine zentrale SAP Service URL noch ein Standort-Override gesetzt."); - var username = string.IsNullOrWhiteSpace(_editingSite.UsernameOverride) - ? sourceDefinition?.CentralUsername ?? string.Empty - : _editingSite.UsernameOverride; - var password = string.IsNullOrWhiteSpace(_editingSite.PasswordOverride) - ? sourceDefinition?.CentralPassword ?? string.Empty - : _editingSite.PasswordOverride; - - if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) - throw new InvalidOperationException("Für SAP sind weder zentrale Zugangsdaten noch Standort-Overrides gesetzt."); - - await AppEventLogService.WriteAsync("SAP", "Refresh aus UI gestartet", siteId: _editingSite.Id, land: _editingSite.Land, - details: serviceUrl); - var entitySets = await SapGatewayService.GetEntitySetsAsync(serviceUrl, username.Trim(), password.Trim()); - _sapEntitySetsCache = entitySets; - _editingSite.SapEntitySetsCache = SerializeSapEntitySets(entitySets); - _editingSite.SapEntitySetsRefreshedAtUtc = DateTime.UtcNow; + var result = await StandortePageService.RefreshSapEntitySetsAsync(_editingSite); + _sapEntitySetsCache = result.EntitySets; + _editingSite.SapEntitySetsCache = SerializeSapEntitySets(result.EntitySets); + _editingSite.SapEntitySetsRefreshedAtUtc = result.RefreshedAtUtc; if (!string.IsNullOrWhiteSpace(_editingSite.SapEntitySet) && !_sapEntitySetsCache.Contains(_editingSite.SapEntitySet, StringComparer.OrdinalIgnoreCase)) @@ -1050,15 +788,11 @@ _editingSite.SapEntitySet = string.Empty; } - Snackbar.Add($"{entitySets.Count} SAP Entity Sets geladen.", Severity.Success); - await AppEventLogService.WriteAsync("SAP", "Refresh aus UI erfolgreich", siteId: _editingSite.Id, land: _editingSite.Land, - details: $"EntitySets={entitySets.Count}"); + Snackbar.Add($"{result.EntitySets.Count} SAP Entity Sets geladen.", Severity.Success); } catch (Exception ex) { Snackbar.Add(ex.Message, Severity.Error); - await AppEventLogService.WriteAsync("SAP", "Refresh aus UI fehlgeschlagen", "Error", siteId: _editingSite.Id, land: _editingSite.Land, - details: ex.ToString()); } finally { @@ -1119,12 +853,10 @@ _editingSite.ManualImportFilePath = targetPath; _editingSite.ManualImportLastUploadedAtUtc = DateTime.UtcNow; Snackbar.Add("Excel-Datei hochgeladen.", Severity.Success); - await AppEventLogService.WriteAsync("ManualImport", "Excel-Datei hochgeladen", siteId: _editingSite.Id, land: _editingSite.Land, details: targetPath); } catch (Exception ex) { Snackbar.Add($"Upload fehlgeschlagen: {ex.Message}", Severity.Error); - await AppEventLogService.WriteAsync("ManualImport", "Excel-Upload fehlgeschlagen", "Error", siteId: _editingSite.Id, land: _editingSite.Land, details: ex.ToString()); } finally { @@ -1136,55 +868,12 @@ { try { - _editingSite.ManualImportFilePath = _editingSite.ManualImportFilePath.Trim(); - - if (string.IsNullOrWhiteSpace(_editingSite.ManualImportFilePath)) - throw new InvalidOperationException("Bitte zuerst einen Dateipfad eintragen."); - - if (!string.Equals(Path.GetExtension(_editingSite.ManualImportFilePath), ".xlsx", StringComparison.OrdinalIgnoreCase)) - throw new InvalidOperationException("Bitte eine Excel-Datei mit Endung .xlsx angeben."); - - if (File.Exists(_editingSite.ManualImportFilePath)) - { - _editingSite.ManualImportLastUploadedAtUtc = File.GetLastWriteTimeUtc(_editingSite.ManualImportFilePath); - } - else if (LooksLikeSharePointReference(_editingSite.ManualImportFilePath)) - { - using var db = await DbFactory.CreateDbContextAsync(); - var spConfig = await db.SharePointConfigs.FirstOrDefaultAsync(); - if (spConfig is null || - string.IsNullOrWhiteSpace(spConfig.TenantId) || - string.IsNullOrWhiteSpace(spConfig.ClientId) || - string.IsNullOrWhiteSpace(spConfig.ClientSecret) || - string.IsNullOrWhiteSpace(spConfig.SiteUrl)) - { - throw new InvalidOperationException("Fuer SharePoint-Pruefung fehlt eine vollstaendige SharePoint-Konfiguration in Settings."); - } - - var tempPath = await SharePointService.DownloadToTempFileAsync( - spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret, spConfig.SiteUrl, _editingSite.ManualImportFilePath); - try - { - _editingSite.ManualImportLastUploadedAtUtc = File.GetLastWriteTimeUtc(tempPath); - } - finally - { - if (File.Exists(tempPath)) - File.Delete(tempPath); - } - } - else - { - throw new InvalidOperationException($"Datei nicht gefunden oder nicht erreichbar: {_editingSite.ManualImportFilePath}"); - } - + _editingSite.ManualImportLastUploadedAtUtc = await StandortePageService.ValidateManualImportPathAsync(_editingSite.ManualImportFilePath); Snackbar.Add("Dateipfad ist gueltig und die Excel-Datei ist erreichbar.", Severity.Success); - await AppEventLogService.WriteAsync("ManualImport", "Dateipfad erfolgreich geprueft", siteId: _editingSite.Id, land: _editingSite.Land, details: _editingSite.ManualImportFilePath); } catch (Exception ex) { Snackbar.Add($"Pfadpruefung fehlgeschlagen: {ex.Message}", Severity.Error); - await AppEventLogService.WriteAsync("ManualImport", "Dateipfadpruefung fehlgeschlagen", "Error", siteId: _editingSite.Id, land: _editingSite.Land, details: ex.ToString()); } } @@ -1206,182 +895,48 @@ private static string SerializeSapEntitySets(List entitySets) => JsonSerializer.Serialize(entitySets); - private static bool LooksLikeSharePointReference(string path) - => path.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || - path.StartsWith("https://", StringComparison.OrdinalIgnoreCase) || - path.StartsWith("/Shared Documents/", StringComparison.OrdinalIgnoreCase) || - path.StartsWith("Shared Documents/", StringComparison.OrdinalIgnoreCase); - private void AddSapSource() { - _sapSources.Add(new SapSourceDefinition - { - Alias = $"SRC{_sapSources.Count + 1}", - EntitySet = _sapEntitySetsCache.FirstOrDefault() ?? string.Empty, - IsActive = true, - IsPrimary = _sapSources.Count == 0, - SortOrder = _sapSources.Count - }); + SapEditorService.AddSapSource(_sapSources, _sapEntitySetsCache); } private void RemoveSapSource(SapSourceDefinition source) { - _sapSources.Remove(source); + SapEditorService.RemoveSapSource(_sapSources, source); } private void AddSapJoin() { - _sapJoins.Add(new SapJoinDefinition - { - JoinType = "Left", - IsActive = true, - SortOrder = _sapJoins.Count - }); + SapEditorService.AddSapJoin(_sapJoins); } private void AutoMatchSapJoins() { - var activeSources = _sapSources - .Where(s => s.IsActive && !string.IsNullOrWhiteSpace(s.Alias)) - .OrderBy(s => s.SortOrder) - .ThenBy(s => s.Id) - .ToList(); - - if (activeSources.Count < 2) - { - Snackbar.Add("Für Auto-Match werden mindestens zwei aktive SAP-Quellen benötigt.", Severity.Warning); - return; - } - - if (_sapSourceFieldMap.Count == 0) - { - Snackbar.Add("Bitte zuerst 'Felder aus Quellen laden' ausführen.", Severity.Warning); - return; - } - - var primary = activeSources.FirstOrDefault(s => s.IsPrimary) ?? activeSources.First(); - var createdOrUpdated = 0; - - foreach (var source in activeSources.Where(s => !string.Equals(s.Alias, primary.Alias, StringComparison.OrdinalIgnoreCase))) - { - if (!_sapSourceFieldMap.TryGetValue(primary.Alias, out var leftFields) || leftFields.Count == 0) - continue; - if (!_sapSourceFieldMap.TryGetValue(source.Alias, out var rightFields) || rightFields.Count == 0) - continue; - - var matchingFields = leftFields - .Intersect(rightFields, StringComparer.OrdinalIgnoreCase) - .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) - .ToList(); - - if (matchingFields.Count == 0) - continue; - - var existingJoin = _sapJoins.FirstOrDefault(j => - string.Equals(j.LeftAlias, primary.Alias, StringComparison.OrdinalIgnoreCase) && - string.Equals(j.RightAlias, source.Alias, StringComparison.OrdinalIgnoreCase)); - - var keyList = string.Join(',', matchingFields); - if (existingJoin is null) - { - _sapJoins.Add(new SapJoinDefinition - { - LeftAlias = primary.Alias, - RightAlias = source.Alias, - LeftKeys = keyList, - RightKeys = keyList, - JoinType = "Left", - IsActive = true, - SortOrder = _sapJoins.Count - }); - } - else - { - existingJoin.LeftKeys = keyList; - existingJoin.RightKeys = keyList; - existingJoin.JoinType = "Left"; - existingJoin.IsActive = true; - } - - createdOrUpdated++; - } - - if (createdOrUpdated == 0) - { - Snackbar.Add("Kein passender Join-Vorschlag gefunden.", Severity.Info); - return; - } - - NormalizeSapConfigCollections(); - Snackbar.Add($"{createdOrUpdated} Join-Vorschläge gesetzt.", Severity.Success); + var result = SapEditorService.AutoMatchSapJoins(_sapSources, _sapJoins, _sapSourceFieldMap); + SapEditorService.NormalizeSapConfigCollections(_sapSources, _sapJoins, _sapMappings); + Snackbar.Add(result.Message, result.Success ? Severity.Success : result.Warning ? Severity.Warning : Severity.Info); } private void RemoveSapJoin(SapJoinDefinition join) { - _sapJoins.Remove(join); + SapEditorService.RemoveSapJoin(_sapJoins, join); } private void AddSapMapping() { - _sapMappings.Add(new SapFieldMapping - { - TargetField = _salesRecordFields.First(), - SourceExpression = _sapAvailableSourceExpressions.FirstOrDefault() ?? "=SAP", - IsActive = true, - SortOrder = _sapMappings.Count - }); + SapEditorService.AddSapMapping(_sapMappings, _salesRecordFields, _sapAvailableSourceExpressions); } private void RemoveSapMapping(SapFieldMapping mapping) { - _sapMappings.Remove(mapping); + SapEditorService.RemoveSapMapping(_sapMappings, mapping); } private IEnumerable GetSapAliases() - => _sapSources.Where(s => !string.IsNullOrWhiteSpace(s.Alias)).Select(s => s.Alias).Distinct(StringComparer.OrdinalIgnoreCase); - - private async Task SaveSapConfigurationAsync(AppDbContext db, int siteId) - { - var oldSources = await db.SapSourceDefinitions.Where(s => s.SiteId == siteId).ToListAsync(); - var oldJoins = await db.SapJoinDefinitions.Where(j => j.SiteId == siteId).ToListAsync(); - var oldMappings = await db.SapFieldMappings.Where(m => m.SiteId == siteId).ToListAsync(); - if (oldSources.Count > 0) db.SapSourceDefinitions.RemoveRange(oldSources); - if (oldJoins.Count > 0) db.SapJoinDefinitions.RemoveRange(oldJoins); - if (oldMappings.Count > 0) db.SapFieldMappings.RemoveRange(oldMappings); - - if (IsSapSite()) - { - NormalizeSapConfigCollections(); - foreach (var source in _sapSources) - source.SiteId = siteId; - foreach (var join in _sapJoins) - join.SiteId = siteId; - foreach (var mapping in _sapMappings) - mapping.SiteId = siteId; - db.SapSourceDefinitions.AddRange(_sapSources); - db.SapJoinDefinitions.AddRange(_sapJoins); - db.SapFieldMappings.AddRange(_sapMappings); - } - - await db.SaveChangesAsync(); - } + => SapEditorService.GetSapAliases(_sapSources); private void NormalizeSapConfigCollections() - { - for (var i = 0; i < _sapSources.Count; i++) - _sapSources[i].SortOrder = i; - for (var i = 0; i < _sapJoins.Count; i++) - _sapJoins[i].SortOrder = i; - for (var i = 0; i < _sapMappings.Count; i++) - _sapMappings[i].SortOrder = i; - - var selectedPrimaryIndex = _sapSources.FindIndex(s => s.IsPrimary); - var primarySource = selectedPrimaryIndex >= 0 ? _sapSources[selectedPrimaryIndex] : _sapSources.FirstOrDefault(); - foreach (var source in _sapSources) - source.IsPrimary = primarySource is not null && ReferenceEquals(source, primarySource); - if (_sapSources.Count > 0 && _sapSources.All(s => !s.IsPrimary)) - _sapSources[0].IsPrimary = true; - } + => SapEditorService.NormalizeSapConfigCollections(_sapSources, _sapJoins, _sapMappings); private async Task RefreshSapSourceFields() { @@ -1400,51 +955,9 @@ if (activeSources.Count == 0) throw new InvalidOperationException("Es gibt keine aktiven SAP-Quellen mit Alias und Entity Set."); - using var db = await DbFactory.CreateDbContextAsync(); - var sourceDefinition = await db.SourceSystemDefinitions - .OrderBy(x => x.Id) - .FirstOrDefaultAsync(x => x.Code == _editingSite.SourceSystem); - var serviceUrl = string.IsNullOrWhiteSpace(_editingSite.SapServiceUrl) - ? sourceDefinition?.CentralServiceUrl ?? string.Empty - : _editingSite.SapServiceUrl; - if (string.IsNullOrWhiteSpace(serviceUrl)) - throw new InvalidOperationException("Es ist weder eine zentrale SAP Service URL noch ein Standort-Override gesetzt."); - var username = string.IsNullOrWhiteSpace(_editingSite.UsernameOverride) - ? sourceDefinition?.CentralUsername ?? string.Empty - : _editingSite.UsernameOverride; - var password = string.IsNullOrWhiteSpace(_editingSite.PasswordOverride) - ? sourceDefinition?.CentralPassword ?? string.Empty - : _editingSite.PasswordOverride; - - if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) - throw new InvalidOperationException("Für SAP sind weder zentrale Zugangsdaten noch Standort-Overrides gesetzt."); - - var expressions = new List { "=SAP" }; - var sourceFieldMap = new Dictionary>(StringComparer.OrdinalIgnoreCase); - foreach (var source in activeSources) - { - var fieldNames = await SapGatewayService.GetEntityFieldNamesAsync(serviceUrl, source.EntitySet, username.Trim(), password.Trim()); - sourceFieldMap[source.Alias] = fieldNames; - expressions.AddRange(fieldNames.Select(field => $"{source.Alias}.{field}")); - } - - _sapAvailableSourceExpressions = expressions - .Where(x => !string.IsNullOrWhiteSpace(x)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) - .ToList(); - _sapSourceFieldMap = sourceFieldMap; - - foreach (var current in BuildSourceExpressionsFromMappings()) - { - if (!_sapAvailableSourceExpressions.Contains(current, StringComparer.OrdinalIgnoreCase)) - _sapAvailableSourceExpressions.Add(current); - } - - _sapAvailableSourceExpressions = _sapAvailableSourceExpressions - .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) - .ToList(); + var result = await StandortePageService.RefreshSapSourceFieldsAsync(_editingSite, activeSources, _sapMappings); + _sapAvailableSourceExpressions = result.SourceExpressions; + _sapSourceFieldMap = result.SourceFieldMap; Snackbar.Add($"{_sapAvailableSourceExpressions.Count} Source Expressions geladen.", Severity.Success); } @@ -1459,73 +972,16 @@ } private IEnumerable GetAvailableSourceExpressions(string? currentValue) - { - var expressions = new List(_sapAvailableSourceExpressions); - if (!string.IsNullOrWhiteSpace(currentValue) && !expressions.Contains(currentValue, StringComparer.OrdinalIgnoreCase)) - expressions.Insert(0, currentValue); - - return expressions; - } + => SapEditorService.GetAvailableSourceExpressions(_sapAvailableSourceExpressions, currentValue); private List BuildSourceExpressionsFromMappings() - => _sapMappings - .Select(m => m.SourceExpression) - .Where(x => !string.IsNullOrWhiteSpace(x)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) - .ToList(); + => SapEditorService.BuildSourceExpressionsFromMappings(_sapMappings); private Dictionary> BuildSourceFieldMapFromJoins() - { - var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); - - foreach (var join in _sapJoins) - { - AddJoinKeysToFieldMap(result, join.LeftAlias, join.LeftKeys); - AddJoinKeysToFieldMap(result, join.RightAlias, join.RightKeys); - } - - return result; - } - - private static void AddJoinKeysToFieldMap(Dictionary> target, string alias, string keys) - { - if (string.IsNullOrWhiteSpace(alias)) - return; - - if (!target.TryGetValue(alias, out var fields)) - { - fields = []; - target[alias] = fields; - } - - foreach (var key in GetSelectedJoinKeys(keys)) - { - if (!fields.Contains(key, StringComparer.OrdinalIgnoreCase)) - fields.Add(key); - } - - fields.Sort(StringComparer.OrdinalIgnoreCase); - } + => SapEditorService.BuildSourceFieldMapFromJoins(_sapJoins); private IEnumerable GetAvailableJoinFields(string? alias, string? currentKeys) - { - var values = new List(); - if (!string.IsNullOrWhiteSpace(alias) && _sapSourceFieldMap.TryGetValue(alias, out var fields)) - values.AddRange(fields); - - foreach (var key in GetSelectedJoinKeys(currentKeys)) - { - if (!values.Contains(key, StringComparer.OrdinalIgnoreCase)) - values.Add(key); - } - - return values - .Where(x => !string.IsNullOrWhiteSpace(x)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) - .ToList(); - } + => SapEditorService.GetAvailableJoinFields(_sapSourceFieldMap, alias, currentKeys); private static HashSet GetSelectedJoinKeys(string? keys) => keys? diff --git a/TrafagSalesExporter/Components/Pages/Transformations.razor b/TrafagSalesExporter/Components/Pages/Transformations.razor index a4436a7..5b23ab2 100644 --- a/TrafagSalesExporter/Components/Pages/Transformations.razor +++ b/TrafagSalesExporter/Components/Pages/Transformations.razor @@ -1,10 +1,8 @@ @page "/transformations" -@using Microsoft.EntityFrameworkCore @using System.Reflection -@using TrafagSalesExporter.Data @using TrafagSalesExporter.Models @using TrafagSalesExporter.Services -@inject IDbContextFactory DbFactory +@inject ITransformationsPageService TransformationsPageActions @inject ITransformationCatalog TransformationCatalog @inject ISnackbar Snackbar @inject IUiTextService UiText @@ -199,9 +197,9 @@ private async Task LoadAsync() { - using var db = await DbFactory.CreateDbContextAsync(); - _sourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync(); - _rules = await db.FieldTransformationRules.OrderBy(r => r.SortOrder).ThenBy(r => r.Id).ToListAsync(); + var state = await TransformationsPageActions.LoadAsync(); + _sourceSystems = state.SourceSystems; + _rules = state.Rules; foreach (var rule in _rules) { @@ -235,12 +233,7 @@ private async Task SaveAllAsync() { - using var db = await DbFactory.CreateDbContextAsync(); - db.FieldTransformationRules.RemoveRange(db.FieldTransformationRules); - await db.SaveChangesAsync(); - - db.FieldTransformationRules.AddRange(_rules); - await db.SaveChangesAsync(); + _rules = await TransformationsPageActions.SaveAllAsync(_rules); Snackbar.Add(T("Transformationsregeln gespeichert.", "Transformation rules saved."), Severity.Success); await LoadAsync(); diff --git a/TrafagSalesExporter/Program.cs b/TrafagSalesExporter/Program.cs index a4b2cb7..01d56ad 100644 --- a/TrafagSalesExporter/Program.cs +++ b/TrafagSalesExporter/Program.cs @@ -41,7 +41,16 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/TrafagSalesExporter/Services/DashboardPageService.cs b/TrafagSalesExporter/Services/DashboardPageService.cs new file mode 100644 index 0000000..cd60a41 --- /dev/null +++ b/TrafagSalesExporter/Services/DashboardPageService.cs @@ -0,0 +1,144 @@ +using Microsoft.EntityFrameworkCore; +using TrafagSalesExporter.Data; +using TrafagSalesExporter.Models; + +namespace TrafagSalesExporter.Services; + +public interface IDashboardPageService +{ + Task LoadAsync(); +} + +public sealed class DashboardPageService : IDashboardPageService +{ + private readonly IDbContextFactory _dbFactory; + + public DashboardPageService(IDbContextFactory dbFactory) + { + _dbFactory = dbFactory; + } + + public async Task LoadAsync() + { + await using var db = await _dbFactory.CreateDbContextAsync(); + + var sites = await db.Sites.Include(s => s.HanaServer).Where(s => s.IsActive).ToListAsync(); + var sourceSystems = await db.SourceSystemDefinitions.AsNoTracking().ToListAsync(); + var logs = await db.ExportLogs + .GroupBy(l => l.SiteId) + .Select(g => g.OrderByDescending(l => l.Timestamp).First()) + .ToListAsync(); + var appLogs = await db.AppEventLogs + .Where(l => l.SiteId != null) + .OrderByDescending(l => l.Timestamp) + .Take(1000) + .ToListAsync(); + var latestAppLogsBySite = appLogs + .GroupBy(l => l.SiteId!.Value) + .ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.Timestamp).First()); + + var rows = sites.Select(s => + { + var log = logs.FirstOrDefault(l => l.SiteId == s.Id); + latestAppLogsBySite.TryGetValue(s.Id, out var appLog); + var sourceSystem = sourceSystems.FirstOrDefault(x => string.Equals(x.Code, s.SourceSystem, StringComparison.OrdinalIgnoreCase)); + return new DashboardRow + { + SiteId = s.Id, + Land = s.Land, + TSC = s.TSC, + Schema = s.Schema, + ServerName = string.Equals(sourceSystem?.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase) + ? ResolveDashboardSapServiceUrl(s, sourceSystems) + : s.HanaServer?.Name ?? string.Empty, + LastStatus = log?.Status ?? string.Empty, + RowCount = log?.RowCount ?? 0, + LastRun = log?.Timestamp, + DurationSeconds = log?.DurationSeconds ?? 0, + ErrorMessage = log?.ErrorMessage ?? string.Empty, + FilePath = log?.FilePath ?? string.Empty, + LiveMessage = appLog is null ? string.Empty : $"{appLog.Category}: {appLog.Message}", + LiveDetails = appLog?.Details ?? string.Empty + }; + }).ToList(); + + return new DashboardPageState + { + DashboardRows = rows, + ConsolidatedRows = BuildConsolidatedRows(await db.ExportSettings.FirstOrDefaultAsync() ?? new()) + }; + } + + private static string ResolveDashboardSapServiceUrl(Site site, List sourceSystems) + { + if (!string.IsNullOrWhiteSpace(site.SapServiceUrl)) + return site.SapServiceUrl; + + var sourceSystem = sourceSystems.FirstOrDefault(x => string.Equals(x.Code, site.SourceSystem, StringComparison.OrdinalIgnoreCase)); + return string.IsNullOrWhiteSpace(sourceSystem?.CentralServiceUrl) ? "SAP Gateway" : sourceSystem.CentralServiceUrl; + } + + private static List BuildConsolidatedRows(ExportSettings settings) + { + var outputDirectory = ResolveConsolidatedOutputDirectory(settings); + if (!Directory.Exists(outputDirectory)) + return []; + + return Directory.GetFiles(outputDirectory, "Sales_All_*.xlsx") + .Select(path => new FileInfo(path)) + .OrderByDescending(file => file.LastWriteTime) + .Take(1) + .Select(file => new ConsolidatedDashboardRow + { + Label = "Konsolidierter Export", + FilePath = file.FullName, + DisplayPath = file.FullName, + LastModified = file.LastWriteTime + }) + .ToList(); + } + + private static string ResolveConsolidatedOutputDirectory(ExportSettings settings) + { + if (!string.IsNullOrWhiteSpace(settings.LocalConsolidatedExportFolder)) + return settings.LocalConsolidatedExportFolder.Trim(); + + if (!string.IsNullOrWhiteSpace(settings.LocalSiteExportFolder)) + return settings.LocalSiteExportFolder.Trim(); + + return Path.Combine(AppContext.BaseDirectory, "output"); + } +} + +public sealed class DashboardPageState +{ + public List DashboardRows { get; set; } = []; + public List ConsolidatedRows { get; set; } = []; +} + +public sealed class DashboardRow +{ + public int SiteId { get; set; } + public string Land { get; set; } = string.Empty; + public string TSC { get; set; } = string.Empty; + public string Schema { get; set; } = string.Empty; + public string ServerName { get; set; } = string.Empty; + public string LastStatus { get; set; } = string.Empty; + public int RowCount { get; set; } + public DateTime? LastRun { get; set; } + public double DurationSeconds { get; set; } + public string ErrorMessage { get; set; } = string.Empty; + public string FilePath { get; set; } = string.Empty; + public string LiveMessage { get; set; } = string.Empty; + public string LiveDetails { get; set; } = string.Empty; + public bool HasOpenableFile => !string.IsNullOrWhiteSpace(FilePath) && File.Exists(FilePath); +} + +public sealed class ConsolidatedDashboardRow +{ + public string Label { get; set; } = string.Empty; + public string FilePath { get; set; } = string.Empty; + public string DisplayPath { get; set; } = string.Empty; + public DateTime? LastModified { get; set; } + public bool HasOpenableFile => !string.IsNullOrWhiteSpace(FilePath) && File.Exists(FilePath); +} diff --git a/TrafagSalesExporter/Services/DatabaseInitializationService.SchemaSql.cs b/TrafagSalesExporter/Services/DatabaseInitializationService.SchemaSql.cs new file mode 100644 index 0000000..40af817 --- /dev/null +++ b/TrafagSalesExporter/Services/DatabaseInitializationService.SchemaSql.cs @@ -0,0 +1,152 @@ +namespace TrafagSalesExporter.Services; + +internal static class DatabaseSchemaSql +{ + internal static string GetExportLogsCreateSql() => @" +CREATE TABLE ExportLogs ( + Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + Timestamp TEXT NOT NULL, + SiteId INTEGER NOT NULL, + Land TEXT NOT NULL, + TSC TEXT NOT NULL, + Status TEXT NOT NULL, + RowCount INTEGER NOT NULL, + ErrorMessage TEXT NULL, + FileName TEXT NOT NULL DEFAULT '', + FilePath TEXT NOT NULL DEFAULT '', + DurationSeconds REAL NOT NULL, + FOREIGN KEY (SiteId) REFERENCES Sites (Id) +);"; + + internal static string GetExportSettingsCreateSql() => @" +CREATE TABLE ExportSettings ( + Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + DateFilter TEXT NOT NULL, + TimerHour INTEGER NOT NULL, + TimerMinute INTEGER NOT NULL, + TimerEnabled INTEGER NOT NULL, + DebugLoggingEnabled INTEGER NOT NULL DEFAULT 0, + LocalSiteExportFolder TEXT NOT NULL DEFAULT '', + LocalConsolidatedExportFolder TEXT NOT NULL DEFAULT '' +);"; + + internal static string GetHanaServersCreateSql() => @" +CREATE TABLE HanaServers ( + Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + SourceSystem TEXT NOT NULL, + Name TEXT NOT NULL, + Host TEXT NOT NULL, + Port INTEGER NOT NULL, + DatabaseName TEXT NOT NULL DEFAULT '', + UseSsl INTEGER NOT NULL DEFAULT 0, + ValidateCertificate INTEGER NOT NULL DEFAULT 0, + AdditionalParams TEXT NOT NULL DEFAULT '' +);"; + + internal static string GetSitesCreateSql() => @" +CREATE TABLE Sites ( + Id INTEGER NOT NULL CONSTRAINT PK_Sites PRIMARY KEY AUTOINCREMENT, + HanaServerId INTEGER NULL, + Schema TEXT NOT NULL, + TSC TEXT NOT NULL, + Land TEXT NOT NULL, + SourceSystem TEXT NOT NULL DEFAULT 'SAP', + UsernameOverride TEXT NOT NULL DEFAULT '', + PasswordOverride TEXT NOT NULL DEFAULT '', + LocalExportFolderOverride TEXT NOT NULL DEFAULT '', + ManualImportFilePath TEXT NOT NULL DEFAULT '', + ManualImportLastUploadedAtUtc TEXT NULL, + SapServiceUrl TEXT NOT NULL DEFAULT '', + SapEntitySet TEXT NOT NULL DEFAULT '', + SapEntitySetsCache TEXT NOT NULL DEFAULT '', + SapEntitySetsRefreshedAtUtc TEXT NULL, + IsActive INTEGER NOT NULL, + CONSTRAINT FK_Sites_HanaServers_HanaServerId FOREIGN KEY (HanaServerId) REFERENCES HanaServers (Id) +);"; + + internal static string GetAppEventLogsCreateSql() => @" +CREATE TABLE AppEventLogs ( + Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + Timestamp TEXT NOT NULL, + Level TEXT NOT NULL, + Category TEXT NOT NULL, + SiteId INTEGER NULL, + Land TEXT NOT NULL, + Message TEXT NOT NULL, + Details TEXT NOT NULL, + FOREIGN KEY (SiteId) REFERENCES Sites (Id) +);"; + + internal static string GetCentralSalesRecordsCreateSql() => @" +CREATE TABLE CentralSalesRecords ( + Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + StoredAtUtc TEXT NOT NULL, + SiteId INTEGER NOT NULL, + SourceSystem TEXT NOT NULL, + ExtractionDate TEXT NOT NULL, + Tsc TEXT NOT NULL, + InvoiceNumber TEXT NOT NULL, + PositionOnInvoice INTEGER NOT NULL, + Material TEXT NOT NULL, + Name TEXT NOT NULL, + ProductGroup TEXT NOT NULL, + Quantity TEXT NOT NULL, + SupplierNumber TEXT NOT NULL, + SupplierName TEXT NOT NULL, + SupplierCountry TEXT NOT NULL, + CustomerNumber TEXT NOT NULL, + CustomerName TEXT NOT NULL, + CustomerCountry TEXT NOT NULL, + CustomerIndustry TEXT NOT NULL, + StandardCost TEXT NOT NULL, + StandardCostCurrency TEXT NOT NULL, + PurchaseOrderNumber TEXT NOT NULL, + SalesPriceValue TEXT NOT NULL, + SalesCurrency TEXT NOT NULL, + Incoterms2020 TEXT NOT NULL, + SalesResponsibleEmployee TEXT NOT NULL, + InvoiceDate TEXT NULL, + OrderDate TEXT NULL, + Land TEXT NOT NULL, + DocumentType TEXT NOT NULL, + FOREIGN KEY (SiteId) REFERENCES Sites (Id) +);"; + + internal static string GetSapSourceDefinitionsCreateSql() => @" +CREATE TABLE SapSourceDefinitions ( + Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + SiteId INTEGER NOT NULL, + Alias TEXT NOT NULL, + EntitySet TEXT NOT NULL, + IsPrimary INTEGER NOT NULL DEFAULT 0, + IsActive INTEGER NOT NULL DEFAULT 1, + SortOrder INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (SiteId) REFERENCES Sites (Id) +);"; + + internal static string GetSapJoinDefinitionsCreateSql() => @" +CREATE TABLE SapJoinDefinitions ( + Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + SiteId INTEGER NOT NULL, + LeftAlias TEXT NOT NULL, + RightAlias TEXT NOT NULL, + LeftKeys TEXT NOT NULL, + RightKeys TEXT NOT NULL, + JoinType TEXT NOT NULL DEFAULT 'Left', + IsActive INTEGER NOT NULL DEFAULT 1, + SortOrder INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (SiteId) REFERENCES Sites (Id) +);"; + + internal static string GetSapFieldMappingsCreateSql() => @" +CREATE TABLE SapFieldMappings ( + Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + SiteId INTEGER NOT NULL, + TargetField TEXT NOT NULL, + SourceExpression TEXT NOT NULL, + IsRequired INTEGER NOT NULL DEFAULT 0, + IsActive INTEGER NOT NULL DEFAULT 1, + SortOrder INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (SiteId) REFERENCES Sites (Id) +);"; +} diff --git a/TrafagSalesExporter/Services/DatabaseInitializationService.cs b/TrafagSalesExporter/Services/DatabaseInitializationService.cs index 545025d..242d960 100644 --- a/TrafagSalesExporter/Services/DatabaseInitializationService.cs +++ b/TrafagSalesExporter/Services/DatabaseInitializationService.cs @@ -1,17 +1,23 @@ using System.Data; using Microsoft.EntityFrameworkCore; using TrafagSalesExporter.Data; -using TrafagSalesExporter.Models; namespace TrafagSalesExporter.Services; -public class DatabaseInitializationService : IDatabaseInitializationService +public partial class DatabaseInitializationService : IDatabaseInitializationService { private readonly IDbContextFactory _dbFactory; + private readonly IDatabaseSchemaMaintenanceService _schemaMaintenanceService; + private readonly IDatabaseSeedService _seedService; - public DatabaseInitializationService(IDbContextFactory dbFactory) + public DatabaseInitializationService( + IDbContextFactory dbFactory, + IDatabaseSchemaMaintenanceService schemaMaintenanceService, + IDatabaseSeedService seedService) { _dbFactory = dbFactory; + _schemaMaintenanceService = schemaMaintenanceService; + _seedService = seedService; } public async Task InitializeAsync() @@ -19,9 +25,8 @@ public class DatabaseInitializationService : IDatabaseInitializationService using var db = await _dbFactory.CreateDbContextAsync(); await db.Database.EnsureCreatedAsync(); ConfigureSqlite(db); - EnsureSchema(db); - SeedIfEmpty(db); - EnsureRecommendedTransformationRules(db); + _schemaMaintenanceService.EnsureSchema(db); + _seedService.SeedDefaults(db); } private static void ConfigureSqlite(AppDbContext db) @@ -42,869 +47,4 @@ public class DatabaseInitializationService : IDatabaseInitializationService timeout.ExecuteNonQuery(); } } - - private static void EnsureSchema(AppDbContext db) - { - EnsureSitesTableSupportsOptionalHanaServer(db); - EnsureExportSettingsTableSupportsCurrentSchema(db); - EnsureHanaServersTableSupportsCurrentSchema(db); - RepairBrokenForeignKeys(db); - AddColumnIfMissing(db, "HanaServers", "SourceSystem", "TEXT NOT NULL DEFAULT ''"); - AddColumnIfMissing(db, "HanaServers", "DatabaseName", "TEXT NOT NULL DEFAULT ''"); - AddColumnIfMissing(db, "HanaServers", "UseSsl", "INTEGER NOT NULL DEFAULT 0"); - AddColumnIfMissing(db, "HanaServers", "ValidateCertificate", "INTEGER NOT NULL DEFAULT 0"); - AddColumnIfMissing(db, "HanaServers", "AdditionalParams", "TEXT NOT NULL DEFAULT ''"); - AddColumnIfMissing(db, "Sites", "SourceSystem", "TEXT NOT NULL DEFAULT 'SAP'"); - AddColumnIfMissing(db, "Sites", "UsernameOverride", "TEXT NOT NULL DEFAULT ''"); - AddColumnIfMissing(db, "Sites", "PasswordOverride", "TEXT NOT NULL DEFAULT ''"); - AddColumnIfMissing(db, "Sites", "LocalExportFolderOverride", "TEXT NOT NULL DEFAULT ''"); - AddColumnIfMissing(db, "Sites", "ManualImportFilePath", "TEXT NOT NULL DEFAULT ''"); - AddColumnIfMissing(db, "Sites", "ManualImportLastUploadedAtUtc", "TEXT NULL"); - AddColumnIfMissing(db, "Sites", "SapServiceUrl", "TEXT NOT NULL DEFAULT ''"); - AddColumnIfMissing(db, "Sites", "SapEntitySet", "TEXT NOT NULL DEFAULT ''"); - AddColumnIfMissing(db, "Sites", "SapEntitySetsCache", "TEXT NOT NULL DEFAULT ''"); - AddColumnIfMissing(db, "Sites", "SapEntitySetsRefreshedAtUtc", "TEXT NULL"); - AddColumnIfMissing(db, "ExportSettings", "DebugLoggingEnabled", "INTEGER NOT NULL DEFAULT 0"); - AddColumnIfMissing(db, "ExportSettings", "LocalSiteExportFolder", "TEXT NOT NULL DEFAULT ''"); - AddColumnIfMissing(db, "ExportSettings", "LocalConsolidatedExportFolder", "TEXT NOT NULL DEFAULT ''"); - AddColumnIfMissing(db, "SharePointConfigs", "CentralExportFolder", "TEXT NOT NULL DEFAULT ''"); - AddColumnIfMissing(db, "ExportLogs", "FilePath", "TEXT NOT NULL DEFAULT ''"); - EnsureTransformationTable(db); - AddColumnIfMissing(db, "FieldTransformationRules", "RuleScope", "TEXT NOT NULL DEFAULT 'Value'"); - EnsureCurrencyExchangeRateTable(db); - EnsureSourceSystemDefinitionTable(db); - AddColumnIfMissing(db, "SourceSystemDefinitions", "CentralServiceUrl", "TEXT NOT NULL DEFAULT ''"); - EnsureSapSourceTable(db); - EnsureSapJoinTable(db); - EnsureSapFieldMappingTable(db); - EnsureCentralSalesRecordTable(db); - EnsureAppEventLogTable(db); - EnsureSourceSystemDefinitions(db); - EnsureCentralHanaServerRecords(db); - } - - private static void EnsureExportSettingsTableSupportsCurrentSchema(AppDbContext db) - { - var conn = db.Database.GetDbConnection(); - if (conn.State != ConnectionState.Open) - conn.Open(); - - var columns = GetTableColumns(conn, transaction: null, "ExportSettings"); - if (columns.Count == 0) - return; - - var legacyColumns = new[] - { - "SapUsername", - "SapPassword", - "Bi1Username", - "Bi1Password", - "SageUsername", - "SagePassword" - }; - - if (!legacyColumns.Any(columns.Contains)) - return; - - RebuildTable(conn, "ExportSettings", GetExportSettingsCreateSql()); - } - - private static void EnsureHanaServersTableSupportsCurrentSchema(AppDbContext db) - { - var conn = db.Database.GetDbConnection(); - if (conn.State != ConnectionState.Open) - conn.Open(); - - var columns = GetTableColumns(conn, transaction: null, "HanaServers"); - if (columns.Count == 0) - return; - - if (!columns.Contains("Username") && !columns.Contains("Password")) - return; - - RebuildTable(conn, "HanaServers", GetHanaServersCreateSql()); - } - - private static void EnsureSitesTableSupportsOptionalHanaServer(AppDbContext db) - { - var conn = db.Database.GetDbConnection(); - if (conn.State != ConnectionState.Open) - conn.Open(); - - var hanaServerIdIsRequired = false; - { - using var pragma = conn.CreateCommand(); - pragma.CommandText = "PRAGMA table_info(Sites)"; - using var reader = pragma.ExecuteReader(); - - while (reader.Read()) - { - if (string.Equals(reader["name"]?.ToString(), "HanaServerId", StringComparison.OrdinalIgnoreCase)) - { - hanaServerIdIsRequired = Convert.ToInt32(reader["notnull"]) == 1; - break; - } - } - } - - if (!hanaServerIdIsRequired) - return; - - using var disableFk = conn.CreateCommand(); - disableFk.CommandText = "PRAGMA foreign_keys = OFF;"; - disableFk.ExecuteNonQuery(); - - using var transaction = conn.BeginTransaction(); - - using (var rename = conn.CreateCommand()) - { - rename.Transaction = transaction; - rename.CommandText = "ALTER TABLE Sites RENAME TO Sites_old;"; - rename.ExecuteNonQuery(); - } - - using (var create = conn.CreateCommand()) - { - create.Transaction = transaction; - create.CommandText = GetSitesCreateSql(); - create.ExecuteNonQuery(); - } - - using (var copy = conn.CreateCommand()) - { - copy.Transaction = transaction; - copy.CommandText = @" -INSERT INTO Sites ( - Id, HanaServerId, Schema, TSC, Land, SourceSystem, - UsernameOverride, PasswordOverride, LocalExportFolderOverride, ManualImportFilePath, - ManualImportLastUploadedAtUtc, SapServiceUrl, SapEntitySet, SapEntitySetsCache, - SapEntitySetsRefreshedAtUtc, IsActive -) -SELECT - Id, HanaServerId, Schema, TSC, Land, - COALESCE(SourceSystem, 'SAP'), - COALESCE(UsernameOverride, ''), - COALESCE(PasswordOverride, ''), - COALESCE(LocalExportFolderOverride, ''), - COALESCE(ManualImportFilePath, ''), - ManualImportLastUploadedAtUtc, - COALESCE(SapServiceUrl, ''), - COALESCE(SapEntitySet, ''), - COALESCE(SapEntitySetsCache, ''), - SapEntitySetsRefreshedAtUtc, - IsActive -FROM Sites_old;"; - copy.ExecuteNonQuery(); - } - - using (var drop = conn.CreateCommand()) - { - drop.Transaction = transaction; - drop.CommandText = "DROP TABLE Sites_old;"; - drop.ExecuteNonQuery(); - } - - transaction.Commit(); - - using var enableFk = conn.CreateCommand(); - enableFk.CommandText = "PRAGMA foreign_keys = ON;"; - enableFk.ExecuteNonQuery(); - } - - private static void RepairBrokenForeignKeys(AppDbContext db) - { - var conn = db.Database.GetDbConnection(); - if (conn.State != ConnectionState.Open) - conn.Open(); - - var siteDependentTables = new[] - { - ("ExportLogs", GetExportLogsCreateSql()), - ("AppEventLogs", GetAppEventLogsCreateSql()), - ("CentralSalesRecords", GetCentralSalesRecordsCreateSql()), - ("SapSourceDefinitions", GetSapSourceDefinitionsCreateSql()), - ("SapJoinDefinitions", GetSapJoinDefinitionsCreateSql()), - ("SapFieldMappings", GetSapFieldMappingsCreateSql()) - }; - - foreach (var (tableName, createSql) in siteDependentTables) - { - if (TableReferences(conn, tableName, "Sites_old")) - RebuildTable(conn, tableName, createSql); - } - - if (TableReferences(conn, "Sites", "HanaServers_repair_old")) - RebuildTable(conn, "Sites", GetSitesCreateSql()); - } - - private static bool TableReferences(System.Data.Common.DbConnection connection, string tableName, string referencedTableName) - { - using var command = connection.CreateCommand(); - command.CommandText = "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = $tableName;"; - - var parameter = command.CreateParameter(); - parameter.ParameterName = "$tableName"; - parameter.Value = tableName; - command.Parameters.Add(parameter); - - var sql = command.ExecuteScalar()?.ToString() ?? string.Empty; - return sql.Contains(referencedTableName, StringComparison.OrdinalIgnoreCase); - } - - private static void RebuildTable(System.Data.Common.DbConnection connection, string tableName, string createSql) - { - using var disableFk = connection.CreateCommand(); - disableFk.CommandText = "PRAGMA foreign_keys = OFF;"; - disableFk.ExecuteNonQuery(); - - using var transaction = connection.BeginTransaction(); - - var tempTableName = $"{tableName}_repair_old"; - - using (var rename = connection.CreateCommand()) - { - rename.Transaction = transaction; - rename.CommandText = $"ALTER TABLE {tableName} RENAME TO {tempTableName};"; - rename.ExecuteNonQuery(); - } - - using (var create = connection.CreateCommand()) - { - create.Transaction = transaction; - create.CommandText = createSql; - create.ExecuteNonQuery(); - } - - var columns = GetSharedColumns(connection, transaction, tableName, tempTableName); - if (columns.Count > 0) - { - var columnList = string.Join(", ", columns); - - using var copy = connection.CreateCommand(); - copy.Transaction = transaction; - copy.CommandText = $"INSERT INTO {tableName} ({columnList}) SELECT {columnList} FROM {tempTableName};"; - copy.ExecuteNonQuery(); - } - - using (var drop = connection.CreateCommand()) - { - drop.Transaction = transaction; - drop.CommandText = $"DROP TABLE {tempTableName};"; - drop.ExecuteNonQuery(); - } - - transaction.Commit(); - - using var enableFk = connection.CreateCommand(); - enableFk.CommandText = "PRAGMA foreign_keys = ON;"; - enableFk.ExecuteNonQuery(); - } - - private static List GetSharedColumns(System.Data.Common.DbConnection connection, System.Data.Common.DbTransaction? transaction, string newTableName, string oldTableName) - { - var newColumns = GetTableColumns(connection, transaction, newTableName); - var oldColumns = GetTableColumns(connection, transaction, oldTableName); - - return newColumns.Where(oldColumns.Contains).ToList(); - } - - private static HashSet GetTableColumns(System.Data.Common.DbConnection connection, System.Data.Common.DbTransaction? transaction, string tableName) - { - var columns = new HashSet(StringComparer.OrdinalIgnoreCase); - - using var command = connection.CreateCommand(); - command.Transaction = transaction; - command.CommandText = $"PRAGMA table_info({tableName})"; - - using var reader = command.ExecuteReader(); - while (reader.Read()) - { - var name = reader["name"]?.ToString(); - if (!string.IsNullOrWhiteSpace(name)) - columns.Add(name); - } - - return columns; - } - - private static string GetExportLogsCreateSql() => @" -CREATE TABLE ExportLogs ( - Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - Timestamp TEXT NOT NULL, - SiteId INTEGER NOT NULL, - Land TEXT NOT NULL, - TSC TEXT NOT NULL, - Status TEXT NOT NULL, - RowCount INTEGER NOT NULL, - ErrorMessage TEXT NULL, - FileName TEXT NOT NULL DEFAULT '', - FilePath TEXT NOT NULL DEFAULT '', - DurationSeconds REAL NOT NULL, - FOREIGN KEY (SiteId) REFERENCES Sites (Id) -);"; - - private static string GetExportSettingsCreateSql() => @" -CREATE TABLE ExportSettings ( - Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - DateFilter TEXT NOT NULL, - TimerHour INTEGER NOT NULL, - TimerMinute INTEGER NOT NULL, - TimerEnabled INTEGER NOT NULL, - DebugLoggingEnabled INTEGER NOT NULL DEFAULT 0, - LocalSiteExportFolder TEXT NOT NULL DEFAULT '', - LocalConsolidatedExportFolder TEXT NOT NULL DEFAULT '' -);"; - - private static string GetHanaServersCreateSql() => @" -CREATE TABLE HanaServers ( - Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - SourceSystem TEXT NOT NULL, - Name TEXT NOT NULL, - Host TEXT NOT NULL, - Port INTEGER NOT NULL, - DatabaseName TEXT NOT NULL DEFAULT '', - UseSsl INTEGER NOT NULL DEFAULT 0, - ValidateCertificate INTEGER NOT NULL DEFAULT 0, - AdditionalParams TEXT NOT NULL DEFAULT '' -);"; - - private static string GetSitesCreateSql() => @" -CREATE TABLE Sites ( - Id INTEGER NOT NULL CONSTRAINT PK_Sites PRIMARY KEY AUTOINCREMENT, - HanaServerId INTEGER NULL, - Schema TEXT NOT NULL, - TSC TEXT NOT NULL, - Land TEXT NOT NULL, - SourceSystem TEXT NOT NULL DEFAULT 'SAP', - UsernameOverride TEXT NOT NULL DEFAULT '', - PasswordOverride TEXT NOT NULL DEFAULT '', - LocalExportFolderOverride TEXT NOT NULL DEFAULT '', - ManualImportFilePath TEXT NOT NULL DEFAULT '', - ManualImportLastUploadedAtUtc TEXT NULL, - SapServiceUrl TEXT NOT NULL DEFAULT '', - SapEntitySet TEXT NOT NULL DEFAULT '', - SapEntitySetsCache TEXT NOT NULL DEFAULT '', - SapEntitySetsRefreshedAtUtc TEXT NULL, - IsActive INTEGER NOT NULL, - CONSTRAINT FK_Sites_HanaServers_HanaServerId FOREIGN KEY (HanaServerId) REFERENCES HanaServers (Id) -);"; - - private static string GetAppEventLogsCreateSql() => @" -CREATE TABLE AppEventLogs ( - Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - Timestamp TEXT NOT NULL, - Level TEXT NOT NULL, - Category TEXT NOT NULL, - SiteId INTEGER NULL, - Land TEXT NOT NULL, - Message TEXT NOT NULL, - Details TEXT NOT NULL, - FOREIGN KEY (SiteId) REFERENCES Sites (Id) -);"; - - private static string GetCentralSalesRecordsCreateSql() => @" -CREATE TABLE CentralSalesRecords ( - Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - StoredAtUtc TEXT NOT NULL, - SiteId INTEGER NOT NULL, - SourceSystem TEXT NOT NULL, - ExtractionDate TEXT NOT NULL, - Tsc TEXT NOT NULL, - InvoiceNumber TEXT NOT NULL, - PositionOnInvoice INTEGER NOT NULL, - Material TEXT NOT NULL, - Name TEXT NOT NULL, - ProductGroup TEXT NOT NULL, - Quantity TEXT NOT NULL, - SupplierNumber TEXT NOT NULL, - SupplierName TEXT NOT NULL, - SupplierCountry TEXT NOT NULL, - CustomerNumber TEXT NOT NULL, - CustomerName TEXT NOT NULL, - CustomerCountry TEXT NOT NULL, - CustomerIndustry TEXT NOT NULL, - StandardCost TEXT NOT NULL, - StandardCostCurrency TEXT NOT NULL, - PurchaseOrderNumber TEXT NOT NULL, - SalesPriceValue TEXT NOT NULL, - SalesCurrency TEXT NOT NULL, - Incoterms2020 TEXT NOT NULL, - SalesResponsibleEmployee TEXT NOT NULL, - InvoiceDate TEXT NULL, - OrderDate TEXT NULL, - Land TEXT NOT NULL, - DocumentType TEXT NOT NULL, - FOREIGN KEY (SiteId) REFERENCES Sites (Id) -);"; - - private static string GetSapSourceDefinitionsCreateSql() => @" -CREATE TABLE SapSourceDefinitions ( - Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - SiteId INTEGER NOT NULL, - Alias TEXT NOT NULL, - EntitySet TEXT NOT NULL, - IsPrimary INTEGER NOT NULL DEFAULT 0, - IsActive INTEGER NOT NULL DEFAULT 1, - SortOrder INTEGER NOT NULL DEFAULT 0, - FOREIGN KEY (SiteId) REFERENCES Sites (Id) -);"; - - private static string GetSapJoinDefinitionsCreateSql() => @" -CREATE TABLE SapJoinDefinitions ( - Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - SiteId INTEGER NOT NULL, - LeftAlias TEXT NOT NULL, - RightAlias TEXT NOT NULL, - LeftKeys TEXT NOT NULL, - RightKeys TEXT NOT NULL, - JoinType TEXT NOT NULL DEFAULT 'Left', - IsActive INTEGER NOT NULL DEFAULT 1, - SortOrder INTEGER NOT NULL DEFAULT 0, - FOREIGN KEY (SiteId) REFERENCES Sites (Id) -);"; - - private static string GetSapFieldMappingsCreateSql() => @" -CREATE TABLE SapFieldMappings ( - Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - SiteId INTEGER NOT NULL, - TargetField TEXT NOT NULL, - SourceExpression TEXT NOT NULL, - IsRequired INTEGER NOT NULL DEFAULT 0, - IsActive INTEGER NOT NULL DEFAULT 1, - SortOrder INTEGER NOT NULL DEFAULT 0, - FOREIGN KEY (SiteId) REFERENCES Sites (Id) -);"; - - private static void AddColumnIfMissing(AppDbContext db, string table, string column, string type) - { - var conn = db.Database.GetDbConnection(); - if (conn.State != ConnectionState.Open) - conn.Open(); - - var exists = false; - using (var cmd = conn.CreateCommand()) - { - cmd.CommandText = $"PRAGMA table_info({table})"; - using var reader = cmd.ExecuteReader(); - while (reader.Read()) - { - if (string.Equals(reader["name"]?.ToString(), column, StringComparison.OrdinalIgnoreCase)) - { - exists = true; - break; - } - } - } - - if (!exists) - { - using var alter = conn.CreateCommand(); - alter.CommandText = $"ALTER TABLE {table} ADD COLUMN {column} {type}"; - alter.ExecuteNonQuery(); - } - } - - private static void EnsureTransformationTable(AppDbContext db) - { - var conn = db.Database.GetDbConnection(); - if (conn.State != ConnectionState.Open) - conn.Open(); - - using var cmd = conn.CreateCommand(); - cmd.CommandText = @" -CREATE TABLE IF NOT EXISTS FieldTransformationRules ( - Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - SourceSystem TEXT NOT NULL DEFAULT 'SAP', - SourceField TEXT NOT NULL, - TargetField TEXT NOT NULL, - TransformationType TEXT NOT NULL, - RuleScope TEXT NOT NULL DEFAULT 'Value', - Argument TEXT NOT NULL DEFAULT '', - SortOrder INTEGER NOT NULL DEFAULT 0, - IsActive INTEGER NOT NULL DEFAULT 1 -);"; - cmd.ExecuteNonQuery(); - } - - private static void EnsureSapSourceTable(AppDbContext db) - { - var conn = db.Database.GetDbConnection(); - if (conn.State != ConnectionState.Open) - conn.Open(); - - using var cmd = conn.CreateCommand(); - cmd.CommandText = @" -CREATE TABLE IF NOT EXISTS SapSourceDefinitions ( - Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - SiteId INTEGER NOT NULL, - Alias TEXT NOT NULL, - EntitySet TEXT NOT NULL, - IsPrimary INTEGER NOT NULL DEFAULT 0, - IsActive INTEGER NOT NULL DEFAULT 1, - SortOrder INTEGER NOT NULL DEFAULT 0, - FOREIGN KEY (SiteId) REFERENCES Sites (Id) -);"; - cmd.ExecuteNonQuery(); - } - - private static void EnsureCurrencyExchangeRateTable(AppDbContext db) - { - var conn = db.Database.GetDbConnection(); - if (conn.State != ConnectionState.Open) - conn.Open(); - - using var cmd = conn.CreateCommand(); - cmd.CommandText = @" -CREATE TABLE IF NOT EXISTS CurrencyExchangeRates ( - Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - FromCurrency TEXT NOT NULL, - ToCurrency TEXT NOT NULL, - Rate REAL NOT NULL, - ValidFrom TEXT NOT NULL, - ValidTo TEXT NULL, - Notes TEXT NOT NULL DEFAULT '', - IsActive INTEGER NOT NULL DEFAULT 1 -);"; - cmd.ExecuteNonQuery(); - } - - private static void EnsureSapJoinTable(AppDbContext db) - { - var conn = db.Database.GetDbConnection(); - if (conn.State != ConnectionState.Open) - conn.Open(); - - using var cmd = conn.CreateCommand(); - cmd.CommandText = @" -CREATE TABLE IF NOT EXISTS SapJoinDefinitions ( - Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - SiteId INTEGER NOT NULL, - LeftAlias TEXT NOT NULL, - RightAlias TEXT NOT NULL, - LeftKeys TEXT NOT NULL, - RightKeys TEXT NOT NULL, - JoinType TEXT NOT NULL DEFAULT 'Left', - IsActive INTEGER NOT NULL DEFAULT 1, - SortOrder INTEGER NOT NULL DEFAULT 0, - FOREIGN KEY (SiteId) REFERENCES Sites (Id) -);"; - cmd.ExecuteNonQuery(); - } - - private static void EnsureSapFieldMappingTable(AppDbContext db) - { - var conn = db.Database.GetDbConnection(); - if (conn.State != ConnectionState.Open) - conn.Open(); - - using var cmd = conn.CreateCommand(); - cmd.CommandText = @" -CREATE TABLE IF NOT EXISTS SapFieldMappings ( - Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - SiteId INTEGER NOT NULL, - TargetField TEXT NOT NULL, - SourceExpression TEXT NOT NULL, - IsRequired INTEGER NOT NULL DEFAULT 0, - IsActive INTEGER NOT NULL DEFAULT 1, - SortOrder INTEGER NOT NULL DEFAULT 0, - FOREIGN KEY (SiteId) REFERENCES Sites (Id) -);"; - cmd.ExecuteNonQuery(); - } - - private static void EnsureCentralSalesRecordTable(AppDbContext db) - { - var conn = db.Database.GetDbConnection(); - if (conn.State != ConnectionState.Open) - conn.Open(); - - using var cmd = conn.CreateCommand(); - cmd.CommandText = @" -CREATE TABLE IF NOT EXISTS CentralSalesRecords ( - Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - StoredAtUtc TEXT NOT NULL, - SiteId INTEGER NOT NULL, - SourceSystem TEXT NOT NULL, - ExtractionDate TEXT NOT NULL, - Tsc TEXT NOT NULL, - InvoiceNumber TEXT NOT NULL, - PositionOnInvoice INTEGER NOT NULL, - Material TEXT NOT NULL, - Name TEXT NOT NULL, - ProductGroup TEXT NOT NULL, - Quantity TEXT NOT NULL, - SupplierNumber TEXT NOT NULL, - SupplierName TEXT NOT NULL, - SupplierCountry TEXT NOT NULL, - CustomerNumber TEXT NOT NULL, - CustomerName TEXT NOT NULL, - CustomerCountry TEXT NOT NULL, - CustomerIndustry TEXT NOT NULL, - StandardCost TEXT NOT NULL, - StandardCostCurrency TEXT NOT NULL, - PurchaseOrderNumber TEXT NOT NULL, - SalesPriceValue TEXT NOT NULL, - SalesCurrency TEXT NOT NULL, - Incoterms2020 TEXT NOT NULL, - SalesResponsibleEmployee TEXT NOT NULL, - InvoiceDate TEXT NULL, - OrderDate TEXT NULL, - Land TEXT NOT NULL, - DocumentType TEXT NOT NULL, - FOREIGN KEY (SiteId) REFERENCES Sites (Id) -);"; - cmd.ExecuteNonQuery(); - } - - private static void EnsureAppEventLogTable(AppDbContext db) - { - var conn = db.Database.GetDbConnection(); - if (conn.State != ConnectionState.Open) - conn.Open(); - - using var cmd = conn.CreateCommand(); - cmd.CommandText = @" -CREATE TABLE IF NOT EXISTS AppEventLogs ( - Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - Timestamp TEXT NOT NULL, - Level TEXT NOT NULL, - Category TEXT NOT NULL, - SiteId INTEGER NULL, - Land TEXT NOT NULL, - Message TEXT NOT NULL, - Details TEXT NOT NULL, - FOREIGN KEY (SiteId) REFERENCES Sites (Id) -);"; - cmd.ExecuteNonQuery(); - } - - private static void EnsureSourceSystemDefinitionTable(AppDbContext db) - { - var conn = db.Database.GetDbConnection(); - if (conn.State != ConnectionState.Open) - conn.Open(); - - using var cmd = conn.CreateCommand(); - cmd.CommandText = @" -CREATE TABLE IF NOT EXISTS SourceSystemDefinitions ( - Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - Code TEXT NOT NULL, - DisplayName TEXT NOT NULL, - ConnectionKind TEXT NOT NULL, - IsActive INTEGER NOT NULL DEFAULT 1, - CentralServiceUrl TEXT NOT NULL DEFAULT '', - CentralUsername TEXT NOT NULL DEFAULT '', - CentralPassword TEXT NOT NULL DEFAULT '' -);"; - cmd.ExecuteNonQuery(); - } - - private static void SeedIfEmpty(AppDbContext db) - { - if (db.Sites.Any() || db.HanaServers.Any() || db.SharePointConfigs.Any() || db.ExportSettings.Any()) - return; - - var serverBi1 = new HanaServer { SourceSystem = "BI1", Name = "BI1", Host = "travtrp0", Port = 30015, Username = "", Password = "" }; - var serverSage = new HanaServer { SourceSystem = "SAGE", Name = "SAGE", Host = "20.197.20.60", Port = 30015, Username = "", Password = "" }; - db.HanaServers.AddRange(serverBi1, serverSage); - db.SaveChanges(); - - db.Sites.AddRange( - new Site { HanaServerId = serverBi1.Id, Schema = "fr01_p", TSC = "TRFR", Land = "Frankreich", SourceSystem = "BI1", IsActive = true }, - new Site { HanaServerId = serverBi1.Id, Schema = "it01_p", TSC = "TRIT", Land = "Italien", SourceSystem = "BI1", IsActive = true }, - new Site { HanaServerId = serverBi1.Id, Schema = "us01_p", TSC = "TRUS", Land = "USA", SourceSystem = "BI1", IsActive = true }, - new Site { HanaServerId = serverSage.Id, Schema = "TRAFAG_LIVE", TSC = "TRIN", Land = "Indien", SourceSystem = "SAGE", IsActive = true } - ); - - db.SharePointConfigs.Add(new SharePointConfig - { - SiteUrl = "https://trafagag.sharepoint.com/sites/WorldwideBIPlatform", - ExportFolder = "/Shared Documents/Exports/", - CentralExportFolder = "", - TenantId = "", - ClientId = "", - ClientSecret = "" - }); - - db.ExportSettings.Add(new ExportSettings - { - DateFilter = "2025-01-01", - TimerHour = 3, - TimerMinute = 0, - TimerEnabled = true, - DebugLoggingEnabled = false, - LocalSiteExportFolder = "", - LocalConsolidatedExportFolder = "" - }); - - db.SaveChanges(); - } - - private static void EnsureRecommendedTransformationRules(AppDbContext db) - { - var recommendedRules = new[] - { - new FieldTransformationRule - { - SourceSystem = "MANUAL_EXCEL", - SourceField = nameof(SalesRecord.SalesCurrency), - TargetField = nameof(SalesRecord.SalesCurrency), - TransformationType = "Replace", - RuleScope = "Value", - Argument = "$=>USD", - SortOrder = 100, - IsActive = true - }, - new FieldTransformationRule - { - SourceSystem = "MANUAL_EXCEL", - SourceField = nameof(SalesRecord.StandardCostCurrency), - TargetField = nameof(SalesRecord.StandardCostCurrency), - TransformationType = "Replace", - RuleScope = "Value", - Argument = "$=>USD", - SortOrder = 110, - IsActive = true - } - }; - - var hasChanges = false; - - foreach (var rule in recommendedRules) - { - var exists = db.FieldTransformationRules.Any(existing => - existing.SourceSystem == rule.SourceSystem && - existing.RuleScope == rule.RuleScope && - existing.SourceField == rule.SourceField && - existing.TargetField == rule.TargetField && - existing.TransformationType == rule.TransformationType && - existing.Argument == rule.Argument); - - if (exists) - continue; - - db.FieldTransformationRules.Add(rule); - hasChanges = true; - } - - if (hasChanges) - db.SaveChanges(); - } - - private static void EnsureCentralHanaServerRecords(AppDbContext db) - { - var centralSystems = db.SourceSystemDefinitions - .AsNoTracking() - .Where(x => x.ConnectionKind == SourceSystemConnectionKinds.Hana) - .OrderBy(x => x.Code) - .Select(x => x.Code) - .ToList(); - var changed = false; - - foreach (var sourceSystem in centralSystems) - { - var existingCentral = db.HanaServers - .OrderBy(x => x.Id) - .FirstOrDefault(x => x.SourceSystem == sourceSystem); - - if (existingCentral is not null) - { - if (string.IsNullOrWhiteSpace(existingCentral.Name)) - { - existingCentral.Name = sourceSystem; - changed = true; - } - - continue; - } - - var linkedServer = db.Sites - .Include(x => x.HanaServer) - .Where(x => x.SourceSystem == sourceSystem && x.HanaServerId != null && x.HanaServer != null) - .Select(x => x.HanaServer!) - .OrderBy(x => x.Id) - .FirstOrDefault(); - - if (linkedServer is not null) - { - linkedServer.SourceSystem = sourceSystem; - if (string.IsNullOrWhiteSpace(linkedServer.Name)) - linkedServer.Name = sourceSystem; - changed = true; - continue; - } - - db.HanaServers.Add(new HanaServer - { - SourceSystem = sourceSystem, - Name = sourceSystem, - Host = string.Empty, - Port = 30015, - Username = string.Empty, - Password = string.Empty, - DatabaseName = string.Empty, - AdditionalParams = string.Empty - }); - changed = true; - } - - if (changed) - db.SaveChanges(); - } - - private static void EnsureSourceSystemDefinitions(AppDbContext db) - { - var defaults = new[] - { - new SourceSystemDefinition { Code = "SAP", DisplayName = "SAP", ConnectionKind = SourceSystemConnectionKinds.SapGateway, IsActive = true }, - new SourceSystemDefinition { Code = "BI1", DisplayName = "BI1", ConnectionKind = SourceSystemConnectionKinds.Hana, IsActive = true }, - new SourceSystemDefinition { Code = "SAGE", DisplayName = "SAGE", ConnectionKind = SourceSystemConnectionKinds.Hana, IsActive = true }, - new SourceSystemDefinition { Code = "MANUAL_EXCEL", DisplayName = "Manual Excel", ConnectionKind = SourceSystemConnectionKinds.ManualExcel, IsActive = true } - }; - - var existing = db.SourceSystemDefinitions.ToList(); - var changed = false; - - foreach (var item in defaults) - { - var current = existing.FirstOrDefault(x => x.Code == item.Code); - if (current is null) - { - db.SourceSystemDefinitions.Add(item); - existing.Add(item); - changed = true; - continue; - } - - if (string.IsNullOrWhiteSpace(current.DisplayName)) - { - current.DisplayName = item.DisplayName; - changed = true; - } - - if (string.IsNullOrWhiteSpace(current.ConnectionKind)) - { - current.ConnectionKind = item.ConnectionKind; - changed = true; - } - - if (string.IsNullOrWhiteSpace(current.CentralServiceUrl) && - string.Equals(current.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase)) - { - var sapSite = db.Sites - .Where(x => x.SourceSystem == current.Code && !string.IsNullOrWhiteSpace(x.SapServiceUrl)) - .OrderBy(x => x.Id) - .FirstOrDefault(); - - if (sapSite is not null) - { - current.CentralServiceUrl = sapSite.SapServiceUrl; - changed = true; - } - } - } - - if (changed) - db.SaveChanges(); - } } diff --git a/TrafagSalesExporter/Services/DatabaseSchemaMaintenanceService.cs b/TrafagSalesExporter/Services/DatabaseSchemaMaintenanceService.cs new file mode 100644 index 0000000..32250fa --- /dev/null +++ b/TrafagSalesExporter/Services/DatabaseSchemaMaintenanceService.cs @@ -0,0 +1,440 @@ +using Microsoft.EntityFrameworkCore; +using TrafagSalesExporter.Data; + +namespace TrafagSalesExporter.Services; + +public class DatabaseSchemaMaintenanceService : IDatabaseSchemaMaintenanceService +{ + public void EnsureSchema(AppDbContext db) + { + EnsureSitesTableSupportsOptionalHanaServer(db); + EnsureExportSettingsTableSupportsCurrentSchema(db); + EnsureHanaServersTableSupportsCurrentSchema(db); + RepairBrokenForeignKeys(db); + AddColumnIfMissing(db, "HanaServers", "SourceSystem", "TEXT NOT NULL DEFAULT ''"); + AddColumnIfMissing(db, "HanaServers", "DatabaseName", "TEXT NOT NULL DEFAULT ''"); + AddColumnIfMissing(db, "HanaServers", "UseSsl", "INTEGER NOT NULL DEFAULT 0"); + AddColumnIfMissing(db, "HanaServers", "ValidateCertificate", "INTEGER NOT NULL DEFAULT 0"); + AddColumnIfMissing(db, "HanaServers", "AdditionalParams", "TEXT NOT NULL DEFAULT ''"); + AddColumnIfMissing(db, "Sites", "SourceSystem", "TEXT NOT NULL DEFAULT 'SAP'"); + AddColumnIfMissing(db, "Sites", "UsernameOverride", "TEXT NOT NULL DEFAULT ''"); + AddColumnIfMissing(db, "Sites", "PasswordOverride", "TEXT NOT NULL DEFAULT ''"); + AddColumnIfMissing(db, "Sites", "LocalExportFolderOverride", "TEXT NOT NULL DEFAULT ''"); + AddColumnIfMissing(db, "Sites", "ManualImportFilePath", "TEXT NOT NULL DEFAULT ''"); + AddColumnIfMissing(db, "Sites", "ManualImportLastUploadedAtUtc", "TEXT NULL"); + AddColumnIfMissing(db, "Sites", "SapServiceUrl", "TEXT NOT NULL DEFAULT ''"); + AddColumnIfMissing(db, "Sites", "SapEntitySet", "TEXT NOT NULL DEFAULT ''"); + AddColumnIfMissing(db, "Sites", "SapEntitySetsCache", "TEXT NOT NULL DEFAULT ''"); + AddColumnIfMissing(db, "Sites", "SapEntitySetsRefreshedAtUtc", "TEXT NULL"); + AddColumnIfMissing(db, "ExportSettings", "DebugLoggingEnabled", "INTEGER NOT NULL DEFAULT 0"); + AddColumnIfMissing(db, "ExportSettings", "LocalSiteExportFolder", "TEXT NOT NULL DEFAULT ''"); + AddColumnIfMissing(db, "ExportSettings", "LocalConsolidatedExportFolder", "TEXT NOT NULL DEFAULT ''"); + AddColumnIfMissing(db, "SharePointConfigs", "CentralExportFolder", "TEXT NOT NULL DEFAULT ''"); + AddColumnIfMissing(db, "ExportLogs", "FilePath", "TEXT NOT NULL DEFAULT ''"); + EnsureTransformationTable(db); + AddColumnIfMissing(db, "FieldTransformationRules", "RuleScope", "TEXT NOT NULL DEFAULT 'Value'"); + EnsureCurrencyExchangeRateTable(db); + EnsureSourceSystemDefinitionTable(db); + AddColumnIfMissing(db, "SourceSystemDefinitions", "CentralServiceUrl", "TEXT NOT NULL DEFAULT ''"); + EnsureSapSourceTable(db); + EnsureSapJoinTable(db); + EnsureSapFieldMappingTable(db); + EnsureCentralSalesRecordTable(db); + EnsureAppEventLogTable(db); + } + + private static void EnsureExportSettingsTableSupportsCurrentSchema(AppDbContext db) + { + var conn = db.Database.GetDbConnection(); + if (conn.State != System.Data.ConnectionState.Open) + conn.Open(); + + var columns = DatabaseSchemaTools.GetTableColumns(conn, transaction: null, "ExportSettings"); + if (columns.Count == 0) + return; + + var legacyColumns = new[] + { + "SapUsername", + "SapPassword", + "Bi1Username", + "Bi1Password", + "SageUsername", + "SagePassword" + }; + + if (!legacyColumns.Any(columns.Contains)) + return; + + DatabaseSchemaTools.RebuildTable(conn, "ExportSettings", DatabaseSchemaSql.GetExportSettingsCreateSql()); + } + + private static void EnsureHanaServersTableSupportsCurrentSchema(AppDbContext db) + { + var conn = db.Database.GetDbConnection(); + if (conn.State != System.Data.ConnectionState.Open) + conn.Open(); + + var columns = DatabaseSchemaTools.GetTableColumns(conn, transaction: null, "HanaServers"); + if (columns.Count == 0) + return; + + if (!columns.Contains("Username") && !columns.Contains("Password")) + return; + + DatabaseSchemaTools.RebuildTable(conn, "HanaServers", DatabaseSchemaSql.GetHanaServersCreateSql()); + } + + private static void EnsureSitesTableSupportsOptionalHanaServer(AppDbContext db) + { + var conn = db.Database.GetDbConnection(); + if (conn.State != System.Data.ConnectionState.Open) + conn.Open(); + + var hanaServerIdIsRequired = false; + { + using var pragma = conn.CreateCommand(); + pragma.CommandText = "PRAGMA table_info(Sites)"; + using var reader = pragma.ExecuteReader(); + + while (reader.Read()) + { + if (string.Equals(reader["name"]?.ToString(), "HanaServerId", StringComparison.OrdinalIgnoreCase)) + { + hanaServerIdIsRequired = Convert.ToInt32(reader["notnull"]) == 1; + break; + } + } + } + + if (!hanaServerIdIsRequired) + return; + + using var disableFk = conn.CreateCommand(); + disableFk.CommandText = "PRAGMA foreign_keys = OFF;"; + disableFk.ExecuteNonQuery(); + + using var transaction = conn.BeginTransaction(); + + using (var rename = conn.CreateCommand()) + { + rename.Transaction = transaction; + rename.CommandText = "ALTER TABLE Sites RENAME TO Sites_old;"; + rename.ExecuteNonQuery(); + } + + using (var create = conn.CreateCommand()) + { + create.Transaction = transaction; + create.CommandText = DatabaseSchemaSql.GetSitesCreateSql(); + create.ExecuteNonQuery(); + } + + using (var copy = conn.CreateCommand()) + { + copy.Transaction = transaction; + copy.CommandText = @" +INSERT INTO Sites ( + Id, HanaServerId, Schema, TSC, Land, SourceSystem, + UsernameOverride, PasswordOverride, LocalExportFolderOverride, ManualImportFilePath, + ManualImportLastUploadedAtUtc, SapServiceUrl, SapEntitySet, SapEntitySetsCache, + SapEntitySetsRefreshedAtUtc, IsActive +) +SELECT + Id, HanaServerId, Schema, TSC, Land, + COALESCE(SourceSystem, 'SAP'), + COALESCE(UsernameOverride, ''), + COALESCE(PasswordOverride, ''), + COALESCE(LocalExportFolderOverride, ''), + COALESCE(ManualImportFilePath, ''), + ManualImportLastUploadedAtUtc, + COALESCE(SapServiceUrl, ''), + COALESCE(SapEntitySet, ''), + COALESCE(SapEntitySetsCache, ''), + SapEntitySetsRefreshedAtUtc, + IsActive +FROM Sites_old;"; + copy.ExecuteNonQuery(); + } + + using (var drop = conn.CreateCommand()) + { + drop.Transaction = transaction; + drop.CommandText = "DROP TABLE Sites_old;"; + drop.ExecuteNonQuery(); + } + + transaction.Commit(); + + using var enableFk = conn.CreateCommand(); + enableFk.CommandText = "PRAGMA foreign_keys = ON;"; + enableFk.ExecuteNonQuery(); + } + + private static void RepairBrokenForeignKeys(AppDbContext db) + { + var conn = db.Database.GetDbConnection(); + if (conn.State != System.Data.ConnectionState.Open) + conn.Open(); + + var siteDependentTables = new[] + { + ("ExportLogs", DatabaseSchemaSql.GetExportLogsCreateSql()), + ("AppEventLogs", DatabaseSchemaSql.GetAppEventLogsCreateSql()), + ("CentralSalesRecords", DatabaseSchemaSql.GetCentralSalesRecordsCreateSql()), + ("SapSourceDefinitions", DatabaseSchemaSql.GetSapSourceDefinitionsCreateSql()), + ("SapJoinDefinitions", DatabaseSchemaSql.GetSapJoinDefinitionsCreateSql()), + ("SapFieldMappings", DatabaseSchemaSql.GetSapFieldMappingsCreateSql()) + }; + + foreach (var (tableName, createSql) in siteDependentTables) + { + if (DatabaseSchemaTools.TableReferences(conn, tableName, "Sites_old")) + DatabaseSchemaTools.RebuildTable(conn, tableName, createSql); + } + + if (DatabaseSchemaTools.TableReferences(conn, "Sites", "HanaServers_repair_old")) + DatabaseSchemaTools.RebuildTable(conn, "Sites", DatabaseSchemaSql.GetSitesCreateSql()); + } + + private static void AddColumnIfMissing(AppDbContext db, string table, string column, string type) + { + var conn = db.Database.GetDbConnection(); + if (conn.State != System.Data.ConnectionState.Open) + conn.Open(); + + var exists = false; + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = $"PRAGMA table_info({table})"; + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + if (string.Equals(reader["name"]?.ToString(), column, StringComparison.OrdinalIgnoreCase)) + { + exists = true; + break; + } + } + } + + if (!exists) + { + using var alter = conn.CreateCommand(); + alter.CommandText = $"ALTER TABLE {table} ADD COLUMN {column} {type}"; + alter.ExecuteNonQuery(); + } + } + + private static void EnsureTransformationTable(AppDbContext db) + { + var conn = db.Database.GetDbConnection(); + if (conn.State != System.Data.ConnectionState.Open) + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = @" +CREATE TABLE IF NOT EXISTS FieldTransformationRules ( + Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + SourceSystem TEXT NOT NULL DEFAULT 'SAP', + SourceField TEXT NOT NULL, + TargetField TEXT NOT NULL, + TransformationType TEXT NOT NULL, + RuleScope TEXT NOT NULL DEFAULT 'Value', + Argument TEXT NOT NULL DEFAULT '', + SortOrder INTEGER NOT NULL DEFAULT 0, + IsActive INTEGER NOT NULL DEFAULT 1 +);"; + cmd.ExecuteNonQuery(); + } + + private static void EnsureSapSourceTable(AppDbContext db) + { + var conn = db.Database.GetDbConnection(); + if (conn.State != System.Data.ConnectionState.Open) + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = DatabaseSchemaSql.GetSapSourceDefinitionsCreateSql().Replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS"); + cmd.ExecuteNonQuery(); + } + + private static void EnsureCurrencyExchangeRateTable(AppDbContext db) + { + var conn = db.Database.GetDbConnection(); + if (conn.State != System.Data.ConnectionState.Open) + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = @" +CREATE TABLE IF NOT EXISTS CurrencyExchangeRates ( + Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + FromCurrency TEXT NOT NULL, + ToCurrency TEXT NOT NULL, + Rate REAL NOT NULL, + ValidFrom TEXT NOT NULL, + ValidTo TEXT NULL, + Notes TEXT NOT NULL DEFAULT '', + IsActive INTEGER NOT NULL DEFAULT 1 +);"; + cmd.ExecuteNonQuery(); + } + + private static void EnsureSapJoinTable(AppDbContext db) + { + var conn = db.Database.GetDbConnection(); + if (conn.State != System.Data.ConnectionState.Open) + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = DatabaseSchemaSql.GetSapJoinDefinitionsCreateSql().Replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS"); + cmd.ExecuteNonQuery(); + } + + private static void EnsureSapFieldMappingTable(AppDbContext db) + { + var conn = db.Database.GetDbConnection(); + if (conn.State != System.Data.ConnectionState.Open) + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = DatabaseSchemaSql.GetSapFieldMappingsCreateSql().Replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS"); + cmd.ExecuteNonQuery(); + } + + private static void EnsureCentralSalesRecordTable(AppDbContext db) + { + var conn = db.Database.GetDbConnection(); + if (conn.State != System.Data.ConnectionState.Open) + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = DatabaseSchemaSql.GetCentralSalesRecordsCreateSql().Replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS"); + cmd.ExecuteNonQuery(); + } + + private static void EnsureAppEventLogTable(AppDbContext db) + { + var conn = db.Database.GetDbConnection(); + if (conn.State != System.Data.ConnectionState.Open) + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = DatabaseSchemaSql.GetAppEventLogsCreateSql().Replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS"); + cmd.ExecuteNonQuery(); + } + + private static void EnsureSourceSystemDefinitionTable(AppDbContext db) + { + var conn = db.Database.GetDbConnection(); + if (conn.State != System.Data.ConnectionState.Open) + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = @" +CREATE TABLE IF NOT EXISTS SourceSystemDefinitions ( + Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + Code TEXT NOT NULL, + DisplayName TEXT NOT NULL, + ConnectionKind TEXT NOT NULL, + IsActive INTEGER NOT NULL DEFAULT 1, + CentralServiceUrl TEXT NOT NULL DEFAULT '', + CentralUsername TEXT NOT NULL DEFAULT '', + CentralPassword TEXT NOT NULL DEFAULT '' +);"; + cmd.ExecuteNonQuery(); + } +} + +internal static class DatabaseSchemaTools +{ + internal static bool TableReferences(System.Data.Common.DbConnection connection, string tableName, string referencedTableName) + { + using var command = connection.CreateCommand(); + command.CommandText = "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = $tableName;"; + + var parameter = command.CreateParameter(); + parameter.ParameterName = "$tableName"; + parameter.Value = tableName; + command.Parameters.Add(parameter); + + var sql = command.ExecuteScalar()?.ToString() ?? string.Empty; + return sql.Contains(referencedTableName, StringComparison.OrdinalIgnoreCase); + } + + internal static void RebuildTable(System.Data.Common.DbConnection connection, string tableName, string createSql) + { + using var disableFk = connection.CreateCommand(); + disableFk.CommandText = "PRAGMA foreign_keys = OFF;"; + disableFk.ExecuteNonQuery(); + + using var transaction = connection.BeginTransaction(); + + var tempTableName = $"{tableName}_repair_old"; + + using (var rename = connection.CreateCommand()) + { + rename.Transaction = transaction; + rename.CommandText = $"ALTER TABLE {tableName} RENAME TO {tempTableName};"; + rename.ExecuteNonQuery(); + } + + using (var create = connection.CreateCommand()) + { + create.Transaction = transaction; + create.CommandText = createSql; + create.ExecuteNonQuery(); + } + + var columns = GetSharedColumns(connection, transaction, tableName, tempTableName); + if (columns.Count > 0) + { + var columnList = string.Join(", ", columns); + + using var copy = connection.CreateCommand(); + copy.Transaction = transaction; + copy.CommandText = $"INSERT INTO {tableName} ({columnList}) SELECT {columnList} FROM {tempTableName};"; + copy.ExecuteNonQuery(); + } + + using (var drop = connection.CreateCommand()) + { + drop.Transaction = transaction; + drop.CommandText = $"DROP TABLE {tempTableName};"; + drop.ExecuteNonQuery(); + } + + transaction.Commit(); + + using var enableFk = connection.CreateCommand(); + enableFk.CommandText = "PRAGMA foreign_keys = ON;"; + enableFk.ExecuteNonQuery(); + } + + internal static List GetSharedColumns(System.Data.Common.DbConnection connection, System.Data.Common.DbTransaction? transaction, string newTableName, string oldTableName) + { + var newColumns = GetTableColumns(connection, transaction, newTableName); + var oldColumns = GetTableColumns(connection, transaction, oldTableName); + + return newColumns.Where(oldColumns.Contains).ToList(); + } + + internal static HashSet GetTableColumns(System.Data.Common.DbConnection connection, System.Data.Common.DbTransaction? transaction, string tableName) + { + var columns = new HashSet(StringComparer.OrdinalIgnoreCase); + + using var command = connection.CreateCommand(); + command.Transaction = transaction; + command.CommandText = $"PRAGMA table_info({tableName})"; + + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + var name = reader["name"]?.ToString(); + if (!string.IsNullOrWhiteSpace(name)) + columns.Add(name); + } + + return columns; + } +} diff --git a/TrafagSalesExporter/Services/DatabaseSeedService.cs b/TrafagSalesExporter/Services/DatabaseSeedService.cs new file mode 100644 index 0000000..3a61ce5 --- /dev/null +++ b/TrafagSalesExporter/Services/DatabaseSeedService.cs @@ -0,0 +1,225 @@ +using Microsoft.EntityFrameworkCore; +using TrafagSalesExporter.Data; +using TrafagSalesExporter.Models; + +namespace TrafagSalesExporter.Services; + +public class DatabaseSeedService : IDatabaseSeedService +{ + public void SeedDefaults(AppDbContext db) + { + SeedIfEmpty(db); + EnsureRecommendedTransformationRules(db); + EnsureSourceSystemDefinitions(db); + EnsureCentralHanaServerRecords(db); + } + + private static void SeedIfEmpty(AppDbContext db) + { + if (db.Sites.Any() || db.HanaServers.Any() || db.SharePointConfigs.Any() || db.ExportSettings.Any()) + return; + + var serverBi1 = new HanaServer { SourceSystem = "BI1", Name = "BI1", Host = "travtrp0", Port = 30015, Username = "", Password = "" }; + var serverSage = new HanaServer { SourceSystem = "SAGE", Name = "SAGE", Host = "20.197.20.60", Port = 30015, Username = "", Password = "" }; + db.HanaServers.AddRange(serverBi1, serverSage); + db.SaveChanges(); + + db.Sites.AddRange( + new Site { HanaServerId = serverBi1.Id, Schema = "fr01_p", TSC = "TRFR", Land = "Frankreich", SourceSystem = "BI1", IsActive = true }, + new Site { HanaServerId = serverBi1.Id, Schema = "it01_p", TSC = "TRIT", Land = "Italien", SourceSystem = "BI1", IsActive = true }, + new Site { HanaServerId = serverBi1.Id, Schema = "us01_p", TSC = "TRUS", Land = "USA", SourceSystem = "BI1", IsActive = true }, + new Site { HanaServerId = serverSage.Id, Schema = "TRAFAG_LIVE", TSC = "TRIN", Land = "Indien", SourceSystem = "SAGE", IsActive = true } + ); + + db.SharePointConfigs.Add(new SharePointConfig + { + SiteUrl = "https://trafagag.sharepoint.com/sites/WorldwideBIPlatform", + ExportFolder = "/Shared Documents/Exports/", + CentralExportFolder = "", + TenantId = "", + ClientId = "", + ClientSecret = "" + }); + + db.ExportSettings.Add(new ExportSettings + { + DateFilter = "2025-01-01", + TimerHour = 3, + TimerMinute = 0, + TimerEnabled = true, + DebugLoggingEnabled = false, + LocalSiteExportFolder = "", + LocalConsolidatedExportFolder = "" + }); + + db.SaveChanges(); + } + + private static void EnsureRecommendedTransformationRules(AppDbContext db) + { + var recommendedRules = new[] + { + new FieldTransformationRule + { + SourceSystem = "MANUAL_EXCEL", + SourceField = nameof(SalesRecord.SalesCurrency), + TargetField = nameof(SalesRecord.SalesCurrency), + TransformationType = "Replace", + RuleScope = "Value", + Argument = "$=>USD", + SortOrder = 100, + IsActive = true + }, + new FieldTransformationRule + { + SourceSystem = "MANUAL_EXCEL", + SourceField = nameof(SalesRecord.StandardCostCurrency), + TargetField = nameof(SalesRecord.StandardCostCurrency), + TransformationType = "Replace", + RuleScope = "Value", + Argument = "$=>USD", + SortOrder = 110, + IsActive = true + } + }; + + var hasChanges = false; + + foreach (var rule in recommendedRules) + { + var exists = db.FieldTransformationRules.Any(existing => + existing.SourceSystem == rule.SourceSystem && + existing.RuleScope == rule.RuleScope && + existing.SourceField == rule.SourceField && + existing.TargetField == rule.TargetField && + existing.TransformationType == rule.TransformationType && + existing.Argument == rule.Argument); + + if (exists) + continue; + + db.FieldTransformationRules.Add(rule); + hasChanges = true; + } + + if (hasChanges) + db.SaveChanges(); + } + + private static void EnsureCentralHanaServerRecords(AppDbContext db) + { + var centralSystems = db.SourceSystemDefinitions + .AsNoTracking() + .Where(x => x.ConnectionKind == SourceSystemConnectionKinds.Hana) + .OrderBy(x => x.Code) + .Select(x => x.Code) + .ToList(); + var changed = false; + + foreach (var sourceSystem in centralSystems) + { + var existingCentral = db.HanaServers + .OrderBy(x => x.Id) + .FirstOrDefault(x => x.SourceSystem == sourceSystem); + + if (existingCentral is not null) + { + if (string.IsNullOrWhiteSpace(existingCentral.Name)) + { + existingCentral.Name = sourceSystem; + changed = true; + } + + continue; + } + + var linkedServer = db.Sites + .Include(x => x.HanaServer) + .Where(x => x.SourceSystem == sourceSystem && x.HanaServerId != null && x.HanaServer != null) + .Select(x => x.HanaServer!) + .OrderBy(x => x.Id) + .FirstOrDefault(); + + if (linkedServer is not null) + { + linkedServer.SourceSystem = sourceSystem; + if (string.IsNullOrWhiteSpace(linkedServer.Name)) + linkedServer.Name = sourceSystem; + changed = true; + continue; + } + + db.HanaServers.Add(new HanaServer + { + SourceSystem = sourceSystem, + Name = sourceSystem, + Host = string.Empty, + Port = 30015, + Username = string.Empty, + Password = string.Empty, + DatabaseName = string.Empty, + AdditionalParams = string.Empty + }); + changed = true; + } + + if (changed) + db.SaveChanges(); + } + + private static void EnsureSourceSystemDefinitions(AppDbContext db) + { + var defaults = new[] + { + new SourceSystemDefinition { Code = "SAP", DisplayName = "SAP", ConnectionKind = SourceSystemConnectionKinds.SapGateway, IsActive = true }, + new SourceSystemDefinition { Code = "BI1", DisplayName = "BI1", ConnectionKind = SourceSystemConnectionKinds.Hana, IsActive = true }, + new SourceSystemDefinition { Code = "SAGE", DisplayName = "SAGE", ConnectionKind = SourceSystemConnectionKinds.Hana, IsActive = true }, + new SourceSystemDefinition { Code = "MANUAL_EXCEL", DisplayName = "Manual Excel", ConnectionKind = SourceSystemConnectionKinds.ManualExcel, IsActive = true } + }; + + var existing = db.SourceSystemDefinitions.ToList(); + var changed = false; + + foreach (var item in defaults) + { + var current = existing.FirstOrDefault(x => x.Code == item.Code); + if (current is null) + { + db.SourceSystemDefinitions.Add(item); + existing.Add(item); + changed = true; + continue; + } + + if (string.IsNullOrWhiteSpace(current.DisplayName)) + { + current.DisplayName = item.DisplayName; + changed = true; + } + + if (string.IsNullOrWhiteSpace(current.ConnectionKind)) + { + current.ConnectionKind = item.ConnectionKind; + changed = true; + } + + if (string.IsNullOrWhiteSpace(current.CentralServiceUrl) && + string.Equals(current.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase)) + { + var sapSite = db.Sites + .Where(x => x.SourceSystem == current.Code && !string.IsNullOrWhiteSpace(x.SapServiceUrl)) + .OrderBy(x => x.Id) + .FirstOrDefault(); + + if (sapSite is not null) + { + current.CentralServiceUrl = sapSite.SapServiceUrl; + changed = true; + } + } + } + + if (changed) + db.SaveChanges(); + } +} diff --git a/TrafagSalesExporter/Services/IDatabaseSchemaMaintenanceService.cs b/TrafagSalesExporter/Services/IDatabaseSchemaMaintenanceService.cs new file mode 100644 index 0000000..1e990e0 --- /dev/null +++ b/TrafagSalesExporter/Services/IDatabaseSchemaMaintenanceService.cs @@ -0,0 +1,8 @@ +using TrafagSalesExporter.Data; + +namespace TrafagSalesExporter.Services; + +public interface IDatabaseSchemaMaintenanceService +{ + void EnsureSchema(AppDbContext db); +} diff --git a/TrafagSalesExporter/Services/IDatabaseSeedService.cs b/TrafagSalesExporter/Services/IDatabaseSeedService.cs new file mode 100644 index 0000000..db21cb2 --- /dev/null +++ b/TrafagSalesExporter/Services/IDatabaseSeedService.cs @@ -0,0 +1,8 @@ +using TrafagSalesExporter.Data; + +namespace TrafagSalesExporter.Services; + +public interface IDatabaseSeedService +{ + void SeedDefaults(AppDbContext db); +} diff --git a/TrafagSalesExporter/Services/LogsPageService.cs b/TrafagSalesExporter/Services/LogsPageService.cs new file mode 100644 index 0000000..96bf371 --- /dev/null +++ b/TrafagSalesExporter/Services/LogsPageService.cs @@ -0,0 +1,69 @@ +using Microsoft.EntityFrameworkCore; +using TrafagSalesExporter.Data; +using TrafagSalesExporter.Models; + +namespace TrafagSalesExporter.Services; + +public interface ILogsPageService +{ + Task LoadAsync(string? filterLand, string? filterStatus, DateTime? filterDate); + Task DeleteOldLogsAsync(int olderThanDays); +} + +public sealed class LogsPageService : ILogsPageService +{ + private readonly IDbContextFactory _dbFactory; + + public LogsPageService(IDbContextFactory dbFactory) + { + _dbFactory = dbFactory; + } + + public async Task LoadAsync(string? filterLand, string? filterStatus, DateTime? filterDate) + { + await using var db = await _dbFactory.CreateDbContextAsync(); + + IQueryable query = db.ExportLogs.OrderByDescending(l => l.Timestamp); + + if (!string.IsNullOrEmpty(filterLand)) + query = query.Where(l => l.Land == filterLand); + + if (!string.IsNullOrEmpty(filterStatus)) + query = query.Where(l => l.Status == filterStatus); + + if (filterDate.HasValue) + query = query.Where(l => l.Timestamp.Date == filterDate.Value.Date); + + IQueryable appLogQuery = db.AppEventLogs.OrderByDescending(l => l.Timestamp); + + if (!string.IsNullOrEmpty(filterLand)) + appLogQuery = appLogQuery.Where(l => l.Land == filterLand); + + if (filterDate.HasValue) + appLogQuery = appLogQuery.Where(l => l.Timestamp.Date == filterDate.Value.Date); + + return new LogsPageState + { + AvailableLands = await db.ExportLogs.Select(l => l.Land).Distinct().OrderBy(l => l).ToListAsync(), + Logs = await query.Take(500).ToListAsync(), + AppLogs = await appLogQuery.Take(500).ToListAsync() + }; + } + + public async Task DeleteOldLogsAsync(int olderThanDays) + { + await using var db = await _dbFactory.CreateDbContextAsync(); + var cutoff = DateTime.Now.AddDays(-olderThanDays); + var oldLogs = await db.ExportLogs.Where(l => l.Timestamp < cutoff).ToListAsync(); + db.ExportLogs.RemoveRange(oldLogs); + await db.SaveChangesAsync(); + return oldLogs.Count; + } +} + +public sealed class LogsPageState +{ + public List Logs { get; set; } = []; + public List AppLogs { get; set; } = []; + public List AvailableLands { get; set; } = []; +} diff --git a/TrafagSalesExporter/Services/ManagementCockpitPageService.cs b/TrafagSalesExporter/Services/ManagementCockpitPageService.cs new file mode 100644 index 0000000..026a502 --- /dev/null +++ b/TrafagSalesExporter/Services/ManagementCockpitPageService.cs @@ -0,0 +1,56 @@ +using TrafagSalesExporter.Models; + +namespace TrafagSalesExporter.Services; + +public interface IManagementCockpitPageService +{ + Task InitializeAsync(string? selectedFilePath, int selectedCentralYear); + Task> LoadFilesAsync(); + Task> LoadCentralYearsAsync(); + Task AnalyzeAsync(string filePath); + Task AnalyzeCentralAsync(int year, int? month); +} + +public sealed class ManagementCockpitPageService : IManagementCockpitPageService +{ + private readonly IManagementCockpitService _cockpitService; + + public ManagementCockpitPageService(IManagementCockpitService cockpitService) + { + _cockpitService = cockpitService; + } + + public async Task InitializeAsync(string? selectedFilePath, int selectedCentralYear) + { + var files = await _cockpitService.GetAvailableFilesAsync(); + var years = await _cockpitService.GetAvailableCentralYearsAsync(); + + return new ManagementCockpitPageState + { + Files = files, + CentralYears = years, + SelectedFilePath = selectedFilePath ?? files.FirstOrDefault()?.Path, + SelectedCentralYear = selectedCentralYear == 0 ? years.LastOrDefault() : selectedCentralYear + }; + } + + public Task> LoadFilesAsync() + => _cockpitService.GetAvailableFilesAsync(); + + public Task> LoadCentralYearsAsync() + => _cockpitService.GetAvailableCentralYearsAsync(); + + public Task AnalyzeAsync(string filePath) + => _cockpitService.AnalyzeAsync(filePath); + + public Task AnalyzeCentralAsync(int year, int? month) + => _cockpitService.AnalyzeCentralAsync(year, month); +} + +public sealed class ManagementCockpitPageState +{ + public List Files { get; set; } = []; + public List CentralYears { get; set; } = []; + public string? SelectedFilePath { get; set; } + public int SelectedCentralYear { get; set; } +} diff --git a/TrafagSalesExporter/Services/SettingsPageService.cs b/TrafagSalesExporter/Services/SettingsPageService.cs new file mode 100644 index 0000000..23185f2 --- /dev/null +++ b/TrafagSalesExporter/Services/SettingsPageService.cs @@ -0,0 +1,324 @@ +using Microsoft.EntityFrameworkCore; +using TrafagSalesExporter.Data; +using TrafagSalesExporter.Models; + +namespace TrafagSalesExporter.Services; + +public interface ISettingsPageService +{ + Task LoadAsync(); + Task SaveSharePointAsync(SharePointConfig config); + Task BuildSharePointTestPreviewAsync(SharePointConfig config); + Task SaveExportSettingsAsync(ExportSettings settings); + Task> SaveSourceSystemsAsync(List sourceSystems); + Task> SaveExchangeRatesAsync(List exchangeRates); + Task RefreshEcbRatesAsync(); + Task ExportConfigurationAsync(bool includeSecrets); + Task ImportConfigurationAsync(string json); + Task TestCentralCredentialsAsync(SourceSystemDefinition definition); +} + +public sealed class SettingsPageService : ISettingsPageService +{ + private readonly IDbContextFactory _dbFactory; + private readonly ISharePointUploadService _sharePointService; + private readonly TimerBackgroundService _timerService; + private readonly IHanaQueryService _hanaService; + private readonly ISapGatewayService _sapGatewayService; + private readonly IConfigTransferService _configTransferService; + private readonly IExchangeRateImportService _exchangeRateImportService; + + public SettingsPageService( + IDbContextFactory dbFactory, + ISharePointUploadService sharePointService, + TimerBackgroundService timerService, + IHanaQueryService hanaService, + ISapGatewayService sapGatewayService, + IConfigTransferService configTransferService, + IExchangeRateImportService exchangeRateImportService) + { + _dbFactory = dbFactory; + _sharePointService = sharePointService; + _timerService = timerService; + _hanaService = hanaService; + _sapGatewayService = sapGatewayService; + _configTransferService = configTransferService; + _exchangeRateImportService = exchangeRateImportService; + } + + public async Task LoadAsync() + { + await using var db = await _dbFactory.CreateDbContextAsync(); + return new SettingsPageState + { + SharePointConfig = await db.SharePointConfigs.FirstOrDefaultAsync() ?? new SharePointConfig(), + ExportSettings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings(), + SourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync(), + ExchangeRates = await LoadExchangeRatesAsync(db) + }; + } + + public async Task SaveSharePointAsync(SharePointConfig config) + { + await using var db = await _dbFactory.CreateDbContextAsync(); + var existing = await db.SharePointConfigs.FirstOrDefaultAsync(); + if (existing is null) + { + db.SharePointConfigs.Add(config); + } + else + { + existing.SiteUrl = config.SiteUrl; + existing.ExportFolder = config.ExportFolder; + existing.CentralExportFolder = config.CentralExportFolder; + existing.TenantId = config.TenantId; + existing.ClientId = config.ClientId; + existing.ClientSecret = config.ClientSecret; + } + + await db.SaveChangesAsync(); + } + + public async Task BuildSharePointTestPreviewAsync(SharePointConfig config) + { + var tenantId = NormalizeConfigValue(config.TenantId); + var clientId = NormalizeConfigValue(config.ClientId); + var clientSecret = NormalizeConfigValue(config.ClientSecret); + var siteUrl = NormalizeConfigValue(config.SiteUrl); + + await _sharePointService.TestConnectionAsync(tenantId, clientId, clientSecret, siteUrl); + return BuildSharePointTestPreview(tenantId, clientId, clientSecret, siteUrl); + } + + public async Task SaveExportSettingsAsync(ExportSettings settings) + { + await using var db = await _dbFactory.CreateDbContextAsync(); + var existing = await db.ExportSettings.FirstOrDefaultAsync(); + if (existing is null) + { + db.ExportSettings.Add(settings); + } + else + { + existing.DateFilter = settings.DateFilter; + existing.TimerHour = settings.TimerHour; + existing.TimerMinute = settings.TimerMinute; + existing.TimerEnabled = settings.TimerEnabled; + existing.DebugLoggingEnabled = settings.DebugLoggingEnabled; + existing.LocalSiteExportFolder = settings.LocalSiteExportFolder; + existing.LocalConsolidatedExportFolder = settings.LocalConsolidatedExportFolder; + } + + await db.SaveChangesAsync(); + _timerService.Recalculate(); + } + + public async Task> SaveSourceSystemsAsync(List sourceSystems) + { + 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))) + throw new InvalidOperationException("Jedes Quellsystem braucht einen Anzeigenamen."); + + var duplicates = normalized.GroupBy(x => x.Code).FirstOrDefault(g => g.Count() > 1); + if (duplicates is not null) + throw new InvalidOperationException($"Quellsystem-Code doppelt vorhanden: {duplicates.Key}"); + + await 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(); + return await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync(); + } + + public async Task> SaveExchangeRatesAsync(List exchangeRates) + { + await 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(); + return await LoadExchangeRatesAsync(db); + } + + public async Task RefreshEcbRatesAsync() + { + var result = await _exchangeRateImportService.RefreshEcbRatesAsync(); + await using var db = await _dbFactory.CreateDbContextAsync(); + return new SettingsExchangeRateRefreshResult + { + ImportedCount = result.ImportedCount, + RateDate = result.RateDate, + ExchangeRates = await LoadExchangeRatesAsync(db) + }; + } + + public Task ExportConfigurationAsync(bool includeSecrets) + => _configTransferService.ExportJsonAsync(includeSecrets); + + public async Task ImportConfigurationAsync(string json) + { + await _configTransferService.ImportJsonAsync(json); + _timerService.Recalculate(); + return await LoadAsync(); + } + + public async Task TestCentralCredentialsAsync(SourceSystemDefinition definition) + { + if (string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase)) + return await TestCentralSapCredentialsAsync(definition); + + if (string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.Hana, StringComparison.OrdinalIgnoreCase)) + return await TestCentralHanaCredentialsAsync(definition); + + return PageActionResult.WarningResult($"Quellsystem '{definition.Code}' hat keinen testbaren Verbindungstyp."); + } + + private async Task TestCentralHanaCredentialsAsync(SourceSystemDefinition definition) + { + var sourceSystem = definition.Code; + var username = definition.CentralUsername; + var password = definition.CentralPassword; + + if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) + return PageActionResult.WarningResult($"Fuer {sourceSystem} sind keine zentralen Zugangsdaten gepflegt."); + + await 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)) + return PageActionResult.WarningResult($"Keine zentrale HANA-Konfiguration fuer {sourceSystem} gefunden."); + + 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)); + return result.Success + ? PageActionResult.SuccessResult($"{sourceSystem}: Zentrale HANA-Verbindung erfolgreich.") + : PageActionResult.ErrorResult($"{sourceSystem}: {result.ExceptionType} - {result.ErrorMessage}"); + } + + private async Task TestCentralSapCredentialsAsync(SourceSystemDefinition definition) + { + var sourceSystem = definition.Code; + var username = definition.CentralUsername; + var password = definition.CentralPassword; + + if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) + return PageActionResult.WarningResult("Fuer SAP sind keine zentralen Gateway-Zugangsdaten gepflegt."); + + if (string.IsNullOrWhiteSpace(definition.CentralServiceUrl)) + return PageActionResult.WarningResult($"Fuer {sourceSystem} ist keine zentrale SAP Service URL gepflegt."); + + try + { + await _sapGatewayService.TestConnectionAsync(definition.CentralServiceUrl, username.Trim(), password.Trim()); + return PageActionResult.SuccessResult($"{sourceSystem}: Zentrale SAP Gateway-Verbindung erfolgreich."); + } + catch (Exception ex) + { + return PageActionResult.ErrorResult($"{sourceSystem}: {ex.Message}"); + } + } + + private static async Task> LoadExchangeRatesAsync(AppDbContext db) + => await db.CurrencyExchangeRates + .OrderBy(x => x.FromCurrency) + .ThenBy(x => x.ToCurrency) + .ThenByDescending(x => x.ValidFrom) + .ToListAsync(); + + public static string NormalizeSourceSystemCode(string? code) => NormalizeConfigValue(code).ToUpperInvariant(); + + public static string NormalizeConnectionKind(string? connectionKind) + => SourceSystemConnectionKinds.All.Contains(connectionKind ?? string.Empty, StringComparer.OrdinalIgnoreCase) + ? (connectionKind ?? string.Empty).Trim().ToUpperInvariant() + : SourceSystemConnectionKinds.Hana; + + public static string NormalizeConfigValue(string? value) => value?.Trim() ?? string.Empty; + + public 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}" + ]); + } +} + +public sealed class SettingsPageState +{ + public SharePointConfig SharePointConfig { get; set; } = new(); + public ExportSettings ExportSettings { get; set; } = new(); + public List SourceSystems { get; set; } = []; + public List ExchangeRates { get; set; } = []; +} + +public sealed class SettingsExchangeRateRefreshResult +{ + public int ImportedCount { get; set; } + public DateTime RateDate { get; set; } + public List ExchangeRates { get; set; } = []; +} + +public sealed class PageActionResult +{ + public bool Success { get; init; } + public bool Warning { get; init; } + public string Message { get; init; } = string.Empty; + + public static PageActionResult SuccessResult(string message) => new() { Success = true, Message = message }; + public static PageActionResult WarningResult(string message) => new() { Warning = true, Message = message }; + public static PageActionResult ErrorResult(string message) => new() { Message = message }; +} diff --git a/TrafagSalesExporter/Services/StandortePageService.cs b/TrafagSalesExporter/Services/StandortePageService.cs new file mode 100644 index 0000000..427a836 --- /dev/null +++ b/TrafagSalesExporter/Services/StandortePageService.cs @@ -0,0 +1,522 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using TrafagSalesExporter.Data; +using TrafagSalesExporter.Models; + +namespace TrafagSalesExporter.Services; + +public interface IStandortePageService +{ + Task LoadAsync(); + Task SaveServerAsync(HanaServer server, IEnumerable hanaSourceSystemCodes); + Task DeleteServerAsync(HanaServer server); + Task TestServerConnectionAsync(HanaServer server); + Task LoadSiteEditorAsync(Site site, IEnumerable sourceSystems); + Task SaveSiteAsync(Site site, bool usesHanaConnection, bool isSapSite, List sapSources, List sapJoins, List sapMappings, List sapEntitySetsCache); + Task DeleteSiteAsync(Site site); + Task> LoadAvailableSchemasAsync(Site site); + Task RefreshSapEntitySetsAsync(Site site); + Task RefreshSapSourceFieldsAsync(Site site, List sapSources, List sapMappings); + Task ValidateManualImportPathAsync(string manualImportFilePath); +} + +public sealed class StandortePageService : IStandortePageService +{ + private readonly IDbContextFactory _dbFactory; + private readonly IHanaQueryService _hanaService; + private readonly ISapGatewayService _sapGatewayService; + private readonly ISharePointUploadService _sharePointService; + private readonly IAppEventLogService _appEventLogService; + + public StandortePageService( + IDbContextFactory dbFactory, + IHanaQueryService hanaService, + ISapGatewayService sapGatewayService, + ISharePointUploadService sharePointService, + IAppEventLogService appEventLogService) + { + _dbFactory = dbFactory; + _hanaService = hanaService; + _sapGatewayService = sapGatewayService; + _sharePointService = sharePointService; + _appEventLogService = appEventLogService; + } + + public async Task LoadAsync() + { + await using var db = await _dbFactory.CreateDbContextAsync(); + var sourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync(); + var hanaSourceSystemCodes = sourceSystems + .Where(x => string.Equals(x.ConnectionKind, SourceSystemConnectionKinds.Hana, StringComparison.OrdinalIgnoreCase)) + .Select(x => x.Code) + .ToList(); + + return new StandortePageState + { + SourceSystems = sourceSystems, + Servers = await db.HanaServers + .Where(s => hanaSourceSystemCodes.Contains(s.SourceSystem)) + .OrderBy(s => s.SourceSystem) + .ThenBy(s => s.Name) + .ToListAsync(), + Sites = await db.Sites.Include(s => s.HanaServer).OrderBy(s => s.Land).ToListAsync() + }; + } + + public async Task SaveServerAsync(HanaServer server, IEnumerable hanaSourceSystemCodes) + { + server.SourceSystem = string.IsNullOrWhiteSpace(server.SourceSystem) + ? hanaSourceSystemCodes.FirstOrDefault() ?? string.Empty + : server.SourceSystem.Trim().ToUpperInvariant(); + server.Name = string.IsNullOrWhiteSpace(server.Name) ? server.SourceSystem : server.Name.Trim(); + server.Host = server.Host.Trim(); + server.DatabaseName = server.DatabaseName.Trim(); + server.AdditionalParams = server.AdditionalParams.Trim(); + server.Username = string.Empty; + server.Password = string.Empty; + + await using var db = await _dbFactory.CreateDbContextAsync(); + if (server.Id == 0) + { + var existingForSourceSystem = await db.HanaServers + .OrderBy(x => x.Id) + .FirstOrDefaultAsync(x => x.SourceSystem == server.SourceSystem); + + if (existingForSourceSystem is null) + { + db.HanaServers.Add(server); + } + else + { + ApplyServer(existingForSourceSystem, server); + } + } + else + { + var existing = await db.HanaServers.FindAsync(server.Id); + if (existing is not null) + ApplyServer(existing, server); + } + + await db.SaveChangesAsync(); + } + + public async Task DeleteServerAsync(HanaServer server) + { + await using var db = await _dbFactory.CreateDbContextAsync(); + var linkedSites = await db.Sites + .Where(s => s.HanaServerId == server.Id) + .OrderBy(s => s.Land) + .Select(s => $"{s.Land} ({s.TSC})") + .ToListAsync(); + + if (linkedSites.Count > 0) + throw new InvalidOperationException($"Server kann nicht geloescht werden. Noch verknuepfte Standorte: {string.Join(", ", linkedSites)}"); + + var entity = await db.HanaServers.FindAsync(server.Id); + if (entity is not null) + { + db.HanaServers.Remove(entity); + await db.SaveChangesAsync(); + } + } + + public async Task TestServerConnectionAsync(HanaServer server) + { + await using var db = await _dbFactory.CreateDbContextAsync(); + var sourceDefinition = await db.SourceSystemDefinitions + .OrderBy(x => x.Id) + .FirstOrDefaultAsync(x => x.Code == server.SourceSystem); + + if (sourceDefinition is null) + throw new InvalidOperationException($"Quellsystem '{server.SourceSystem}' nicht gefunden."); + + if (string.IsNullOrWhiteSpace(sourceDefinition.CentralUsername) || string.IsNullOrWhiteSpace(sourceDefinition.CentralPassword)) + throw new InvalidOperationException($"Fuer {server.SourceSystem} sind keine zentralen Zugangsdaten im Quellsystem gepflegt."); + + var testServer = new HanaServer + { + Id = server.Id, + SourceSystem = server.SourceSystem, + Name = server.Name, + Host = server.Host, + Port = server.Port, + Username = sourceDefinition.CentralUsername.Trim(), + Password = sourceDefinition.CentralPassword, + DatabaseName = server.DatabaseName, + UseSsl = server.UseSsl, + ValidateCertificate = server.ValidateCertificate, + AdditionalParams = server.AdditionalParams + }; + + await _appEventLogService.WriteAsync("HANA", "Server-Test aus UI gestartet", details: testServer.GetConnectionStringPreview()); + return await Task.Run(() => _hanaService.TestConnectionDetailed(testServer)); + } + + public async Task LoadSiteEditorAsync(Site site, IEnumerable sourceSystems) + { + var effectiveSourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) + ? sourceSystems.FirstOrDefault()?.Code ?? "SAP" + : site.SourceSystem; + + await using var db = await _dbFactory.CreateDbContextAsync(); + var sapSources = await db.SapSourceDefinitions.Where(s => s.SiteId == site.Id).OrderBy(s => s.SortOrder).ThenBy(s => s.Id).ToListAsync(); + var sapJoins = await db.SapJoinDefinitions.Where(j => j.SiteId == site.Id).OrderBy(j => j.SortOrder).ThenBy(j => j.Id).ToListAsync(); + var sapMappings = await db.SapFieldMappings.Where(m => m.SiteId == site.Id).OrderBy(m => m.SortOrder).ThenBy(m => m.Id).ToListAsync(); + + return new StandortEditorState + { + Site = new Site + { + Id = site.Id, + HanaServerId = site.HanaServerId, + Schema = site.Schema, + TSC = site.TSC, + Land = site.Land, + SourceSystem = effectiveSourceSystem, + UsernameOverride = site.UsernameOverride, + PasswordOverride = site.PasswordOverride, + LocalExportFolderOverride = site.LocalExportFolderOverride, + ManualImportFilePath = site.ManualImportFilePath, + ManualImportLastUploadedAtUtc = site.ManualImportLastUploadedAtUtc, + SapServiceUrl = site.SapServiceUrl, + SapEntitySet = site.SapEntitySet, + SapEntitySetsCache = site.SapEntitySetsCache, + SapEntitySetsRefreshedAtUtc = site.SapEntitySetsRefreshedAtUtc, + IsActive = site.IsActive + }, + SapEntitySets = ParseSapEntitySets(site.SapEntitySetsCache), + SapSources = sapSources, + SapJoins = sapJoins, + SapMappings = sapMappings + }; + } + + public async Task SaveSiteAsync(Site site, bool usesHanaConnection, bool isSapSite, List sapSources, List sapJoins, List sapMappings, List sapEntitySetsCache) + { + await using var db = await _dbFactory.CreateDbContextAsync(); + var serverId = usesHanaConnection ? await ResolveCentralHanaServerIdAsync(db, site) : (int?)null; + site.HanaServerId = serverId; + site.SapEntitySetsCache = JsonSerializer.Serialize(sapEntitySetsCache); + + if (site.Id == 0) + { + db.Sites.Add(site); + } + else + { + var existing = await db.Sites.FindAsync(site.Id); + if (existing is not null) + ApplySite(existing, site); + } + + await db.SaveChangesAsync(); + await SaveSapConfigurationAsync(db, site.Id, isSapSite, sapSources, sapJoins, sapMappings); + } + + public async Task DeleteSiteAsync(Site site) + { + await using var db = await _dbFactory.CreateDbContextAsync(); + var entity = await db.Sites.FindAsync(site.Id); + if (entity is null) + return; + + var sources = await db.SapSourceDefinitions.Where(s => s.SiteId == site.Id).ToListAsync(); + var joins = await db.SapJoinDefinitions.Where(j => j.SiteId == site.Id).ToListAsync(); + var mappings = await db.SapFieldMappings.Where(m => m.SiteId == site.Id).ToListAsync(); + var centralRows = await db.CentralSalesRecords.Where(r => r.SiteId == site.Id).ToListAsync(); + if (sources.Count > 0) db.SapSourceDefinitions.RemoveRange(sources); + if (joins.Count > 0) db.SapJoinDefinitions.RemoveRange(joins); + if (mappings.Count > 0) db.SapFieldMappings.RemoveRange(mappings); + if (centralRows.Count > 0) db.CentralSalesRecords.RemoveRange(centralRows); + db.Sites.Remove(entity); + await db.SaveChangesAsync(); + } + + public async Task> LoadAvailableSchemasAsync(Site site) + { + await using var db = await _dbFactory.CreateDbContextAsync(); + var sourceDefinition = await db.SourceSystemDefinitions.OrderBy(x => x.Id).FirstOrDefaultAsync(x => x.Code == site.SourceSystem) + ?? throw new InvalidOperationException($"Quellsystem '{site.SourceSystem}' nicht gefunden."); + + var centralServer = await db.HanaServers.OrderBy(x => x.Id).FirstOrDefaultAsync(x => x.SourceSystem == site.SourceSystem); + if (centralServer is null || string.IsNullOrWhiteSpace(centralServer.Host)) + throw new InvalidOperationException($"Fuer {site.SourceSystem} ist keine gueltige zentrale HANA-Konfiguration vorhanden."); + + var username = string.IsNullOrWhiteSpace(site.UsernameOverride) ? sourceDefinition.CentralUsername ?? string.Empty : site.UsernameOverride; + var password = string.IsNullOrWhiteSpace(site.PasswordOverride) ? sourceDefinition.CentralPassword ?? string.Empty : site.PasswordOverride; + if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) + throw new InvalidOperationException($"Fuer {site.SourceSystem} sind weder zentrale Zugangsdaten noch Standort-Overrides gesetzt."); + + var lookupServer = new HanaServer + { + Id = centralServer.Id, + SourceSystem = centralServer.SourceSystem, + Name = centralServer.Name, + Host = centralServer.Host, + Port = centralServer.Port, + Username = username.Trim(), + Password = password, + DatabaseName = centralServer.DatabaseName, + UseSsl = centralServer.UseSsl, + ValidateCertificate = centralServer.ValidateCertificate, + AdditionalParams = centralServer.AdditionalParams + }; + + return await Task.Run(() => _hanaService.GetAvailableSchemas(lookupServer)) + .ContinueWith(task => task.Result + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) + .ToList()); + } + + public async Task RefreshSapEntitySetsAsync(Site site) + { + await using var db = await _dbFactory.CreateDbContextAsync(); + var sourceDefinition = await db.SourceSystemDefinitions.OrderBy(x => x.Id).FirstOrDefaultAsync(x => x.Code == site.SourceSystem); + var serviceUrl = string.IsNullOrWhiteSpace(site.SapServiceUrl) ? sourceDefinition?.CentralServiceUrl ?? string.Empty : site.SapServiceUrl; + if (string.IsNullOrWhiteSpace(serviceUrl)) + throw new InvalidOperationException("Es ist weder eine zentrale SAP Service URL noch ein Standort-Override gesetzt."); + + var username = string.IsNullOrWhiteSpace(site.UsernameOverride) ? sourceDefinition?.CentralUsername ?? string.Empty : site.UsernameOverride; + var password = string.IsNullOrWhiteSpace(site.PasswordOverride) ? sourceDefinition?.CentralPassword ?? string.Empty : site.PasswordOverride; + if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) + throw new InvalidOperationException("Fuer SAP sind weder zentrale Zugangsdaten noch Standort-Overrides gesetzt."); + + await _appEventLogService.WriteAsync("SAP", "Refresh aus UI gestartet", siteId: site.Id, land: site.Land, details: serviceUrl); + var entitySets = await _sapGatewayService.GetEntitySetsAsync(serviceUrl, username.Trim(), password.Trim()); + await _appEventLogService.WriteAsync("SAP", "Refresh aus UI erfolgreich", siteId: site.Id, land: site.Land, details: $"EntitySets={entitySets.Count}"); + + return new SapEntitySetRefreshResult + { + EntitySets = entitySets, + RefreshedAtUtc = DateTime.UtcNow + }; + } + + public async Task RefreshSapSourceFieldsAsync(Site site, List sapSources, List sapMappings) + { + var activeSources = sapSources + .Where(s => s.IsActive && !string.IsNullOrWhiteSpace(s.Alias) && !string.IsNullOrWhiteSpace(s.EntitySet)) + .OrderBy(s => s.SortOrder) + .ThenBy(s => s.Id) + .ToList(); + + if (activeSources.Count == 0) + throw new InvalidOperationException("Es gibt keine aktiven SAP-Quellen mit Alias und Entity Set."); + + await using var db = await _dbFactory.CreateDbContextAsync(); + var sourceDefinition = await db.SourceSystemDefinitions.OrderBy(x => x.Id).FirstOrDefaultAsync(x => x.Code == site.SourceSystem); + var serviceUrl = string.IsNullOrWhiteSpace(site.SapServiceUrl) ? sourceDefinition?.CentralServiceUrl ?? string.Empty : site.SapServiceUrl; + if (string.IsNullOrWhiteSpace(serviceUrl)) + throw new InvalidOperationException("Es ist weder eine zentrale SAP Service URL noch ein Standort-Override gesetzt."); + + var username = string.IsNullOrWhiteSpace(site.UsernameOverride) ? sourceDefinition?.CentralUsername ?? string.Empty : site.UsernameOverride; + var password = string.IsNullOrWhiteSpace(site.PasswordOverride) ? sourceDefinition?.CentralPassword ?? string.Empty : site.PasswordOverride; + if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) + throw new InvalidOperationException("Fuer SAP sind weder zentrale Zugangsdaten noch Standort-Overrides gesetzt."); + + var expressions = new List { "=SAP" }; + var sourceFieldMap = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (var source in activeSources) + { + var fieldNames = await _sapGatewayService.GetEntityFieldNamesAsync(serviceUrl, source.EntitySet, username.Trim(), password.Trim()); + sourceFieldMap[source.Alias] = fieldNames; + expressions.AddRange(fieldNames.Select(field => $"{source.Alias}.{field}")); + } + + foreach (var current in sapMappings.Select(m => m.SourceExpression).Where(x => !string.IsNullOrWhiteSpace(x))) + { + if (!expressions.Contains(current, StringComparer.OrdinalIgnoreCase)) + expressions.Add(current); + } + + return new SapSourceFieldRefreshResult + { + SourceFieldMap = sourceFieldMap, + SourceExpressions = expressions + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) + .ToList() + }; + } + + public async Task ValidateManualImportPathAsync(string manualImportFilePath) + { + var trimmedPath = manualImportFilePath.Trim(); + if (string.IsNullOrWhiteSpace(trimmedPath)) + throw new InvalidOperationException("Bitte zuerst einen Dateipfad eintragen."); + if (!string.Equals(Path.GetExtension(trimmedPath), ".xlsx", StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException("Bitte eine Excel-Datei mit Endung .xlsx angeben."); + + if (File.Exists(trimmedPath)) + return File.GetLastWriteTimeUtc(trimmedPath); + + if (!LooksLikeSharePointReference(trimmedPath)) + throw new InvalidOperationException($"Datei nicht gefunden oder nicht erreichbar: {trimmedPath}"); + + await using var db = await _dbFactory.CreateDbContextAsync(); + var spConfig = await db.SharePointConfigs.FirstOrDefaultAsync(); + if (spConfig is null || + string.IsNullOrWhiteSpace(spConfig.TenantId) || + string.IsNullOrWhiteSpace(spConfig.ClientId) || + string.IsNullOrWhiteSpace(spConfig.ClientSecret) || + string.IsNullOrWhiteSpace(spConfig.SiteUrl)) + { + throw new InvalidOperationException("Fuer SharePoint-Pruefung fehlt eine vollstaendige SharePoint-Konfiguration in Settings."); + } + + var tempPath = await _sharePointService.DownloadToTempFileAsync( + spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret, spConfig.SiteUrl, trimmedPath); + try + { + return File.GetLastWriteTimeUtc(tempPath); + } + finally + { + if (File.Exists(tempPath)) + File.Delete(tempPath); + } + } + + private static void ApplyServer(HanaServer target, HanaServer source) + { + target.SourceSystem = source.SourceSystem; + target.Name = source.Name; + target.Host = source.Host; + target.Port = source.Port; + target.Username = string.Empty; + target.Password = string.Empty; + target.DatabaseName = source.DatabaseName; + target.UseSsl = source.UseSsl; + target.ValidateCertificate = source.ValidateCertificate; + target.AdditionalParams = source.AdditionalParams; + } + + private static void ApplySite(Site target, Site source) + { + target.HanaServerId = source.HanaServerId; + target.Schema = source.Schema; + target.TSC = source.TSC; + target.Land = source.Land; + target.SourceSystem = source.SourceSystem; + target.UsernameOverride = source.UsernameOverride; + target.PasswordOverride = source.PasswordOverride; + target.LocalExportFolderOverride = source.LocalExportFolderOverride; + target.ManualImportFilePath = source.ManualImportFilePath; + target.ManualImportLastUploadedAtUtc = source.ManualImportLastUploadedAtUtc; + target.SapServiceUrl = source.SapServiceUrl; + target.SapEntitySet = source.SapEntitySet; + target.SapEntitySetsCache = source.SapEntitySetsCache; + target.SapEntitySetsRefreshedAtUtc = source.SapEntitySetsRefreshedAtUtc; + target.IsActive = source.IsActive; + } + + private static List ParseSapEntitySets(string json) + { + if (string.IsNullOrWhiteSpace(json)) + return []; + + try + { + return JsonSerializer.Deserialize>(json) ?? []; + } + catch + { + return []; + } + } + + private static bool LooksLikeSharePointReference(string path) + => path.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || + path.StartsWith("https://", StringComparison.OrdinalIgnoreCase) || + path.StartsWith("/Shared Documents/", StringComparison.OrdinalIgnoreCase) || + path.StartsWith("Shared Documents/", StringComparison.OrdinalIgnoreCase); + + private static void NormalizeSapConfigCollections(List sapSources, List sapJoins, List sapMappings) + { + for (var i = 0; i < sapSources.Count; i++) + sapSources[i].SortOrder = i; + for (var i = 0; i < sapJoins.Count; i++) + sapJoins[i].SortOrder = i; + for (var i = 0; i < sapMappings.Count; i++) + sapMappings[i].SortOrder = i; + + var selectedPrimaryIndex = sapSources.FindIndex(s => s.IsPrimary); + var primarySource = selectedPrimaryIndex >= 0 ? sapSources[selectedPrimaryIndex] : sapSources.FirstOrDefault(); + foreach (var source in sapSources) + source.IsPrimary = primarySource is not null && ReferenceEquals(source, primarySource); + if (sapSources.Count > 0 && sapSources.All(s => !s.IsPrimary)) + sapSources[0].IsPrimary = true; + } + + private static async Task SaveSapConfigurationAsync(AppDbContext db, int siteId, bool isSapSite, List sapSources, List sapJoins, List sapMappings) + { + var oldSources = await db.SapSourceDefinitions.Where(s => s.SiteId == siteId).ToListAsync(); + var oldJoins = await db.SapJoinDefinitions.Where(j => j.SiteId == siteId).ToListAsync(); + var oldMappings = await db.SapFieldMappings.Where(m => m.SiteId == siteId).ToListAsync(); + if (oldSources.Count > 0) db.SapSourceDefinitions.RemoveRange(oldSources); + if (oldJoins.Count > 0) db.SapJoinDefinitions.RemoveRange(oldJoins); + if (oldMappings.Count > 0) db.SapFieldMappings.RemoveRange(oldMappings); + + if (isSapSite) + { + NormalizeSapConfigCollections(sapSources, sapJoins, sapMappings); + foreach (var source in sapSources) source.SiteId = siteId; + foreach (var join in sapJoins) join.SiteId = siteId; + foreach (var mapping in sapMappings) mapping.SiteId = siteId; + db.SapSourceDefinitions.AddRange(sapSources); + db.SapJoinDefinitions.AddRange(sapJoins); + db.SapFieldMappings.AddRange(sapMappings); + } + + await db.SaveChangesAsync(); + } + + private static async Task ResolveCentralHanaServerIdAsync(AppDbContext db, Site site) + { + site.UsernameOverride = site.UsernameOverride.Trim(); + site.PasswordOverride = site.PasswordOverride.Trim(); + site.LocalExportFolderOverride = site.LocalExportFolderOverride.Trim(); + site.ManualImportFilePath = site.ManualImportFilePath.Trim(); + site.SapServiceUrl = site.SapServiceUrl.Trim(); + site.SapEntitySet = site.SapEntitySet.Trim(); + + var normalizedSourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) ? string.Empty : site.SourceSystem.Trim().ToUpperInvariant(); + var centralServer = await db.HanaServers.OrderBy(x => x.Id).FirstOrDefaultAsync(x => x.SourceSystem == normalizedSourceSystem); + if (centralServer is null || string.IsNullOrWhiteSpace(centralServer.Host)) + throw new InvalidOperationException($"Fuer Quellsystem '{normalizedSourceSystem}' ist keine gueltige zentrale HANA-Konfiguration vorhanden."); + + return centralServer.Id; + } +} + +public sealed class StandortePageState +{ + public List SourceSystems { get; set; } = []; + public List Servers { get; set; } = []; + public List Sites { get; set; } = []; +} + +public sealed class StandortEditorState +{ + public Site Site { get; set; } = new(); + public List SapEntitySets { get; set; } = []; + public List SapSources { get; set; } = []; + public List SapJoins { get; set; } = []; + public List SapMappings { get; set; } = []; +} + +public sealed class SapEntitySetRefreshResult +{ + public List EntitySets { get; set; } = []; + public DateTime RefreshedAtUtc { get; set; } +} + +public sealed class SapSourceFieldRefreshResult +{ + public List SourceExpressions { get; set; } = []; + public Dictionary> SourceFieldMap { get; set; } = new(StringComparer.OrdinalIgnoreCase); +} diff --git a/TrafagSalesExporter/Services/StandorteSapEditorService.cs b/TrafagSalesExporter/Services/StandorteSapEditorService.cs new file mode 100644 index 0000000..96525c3 --- /dev/null +++ b/TrafagSalesExporter/Services/StandorteSapEditorService.cs @@ -0,0 +1,240 @@ +using TrafagSalesExporter.Models; + +namespace TrafagSalesExporter.Services; + +public interface IStandorteSapEditorService +{ + void AddSapSource(List sapSources, List sapEntitySetsCache); + void RemoveSapSource(List sapSources, SapSourceDefinition source); + void AddSapJoin(List sapJoins); + SapAutoMatchResult AutoMatchSapJoins(List sapSources, List sapJoins, Dictionary> sapSourceFieldMap); + void RemoveSapJoin(List sapJoins, SapJoinDefinition join); + void AddSapMapping(List sapMappings, IReadOnlyList salesRecordFields, List sapAvailableSourceExpressions); + void RemoveSapMapping(List sapMappings, SapFieldMapping mapping); + List BuildSourceExpressionsFromMappings(List sapMappings); + Dictionary> BuildSourceFieldMapFromJoins(List sapJoins); + IEnumerable GetSapAliases(List sapSources); + IEnumerable GetAvailableSourceExpressions(List sapAvailableSourceExpressions, string? currentValue); + IEnumerable GetAvailableJoinFields(Dictionary> sapSourceFieldMap, string? alias, string? currentKeys); + void NormalizeSapConfigCollections(List sapSources, List sapJoins, List sapMappings); +} + +public sealed class StandorteSapEditorService : IStandorteSapEditorService +{ + public void AddSapSource(List sapSources, List sapEntitySetsCache) + { + sapSources.Add(new SapSourceDefinition + { + Alias = $"SRC{sapSources.Count + 1}", + EntitySet = sapEntitySetsCache.FirstOrDefault() ?? string.Empty, + IsActive = true, + IsPrimary = sapSources.Count == 0, + SortOrder = sapSources.Count + }); + } + + public void RemoveSapSource(List sapSources, SapSourceDefinition source) + => sapSources.Remove(source); + + public void AddSapJoin(List sapJoins) + { + sapJoins.Add(new SapJoinDefinition + { + JoinType = "Left", + IsActive = true, + SortOrder = sapJoins.Count + }); + } + + public SapAutoMatchResult AutoMatchSapJoins(List sapSources, List sapJoins, Dictionary> sapSourceFieldMap) + { + var activeSources = sapSources + .Where(s => s.IsActive && !string.IsNullOrWhiteSpace(s.Alias)) + .OrderBy(s => s.SortOrder) + .ThenBy(s => s.Id) + .ToList(); + + if (activeSources.Count < 2) + return SapAutoMatchResult.WarningResult("Fuer Auto-Match werden mindestens zwei aktive SAP-Quellen benoetigt."); + + if (sapSourceFieldMap.Count == 0) + return SapAutoMatchResult.WarningResult("Bitte zuerst 'Felder aus Quellen laden' ausfuehren."); + + var primary = activeSources.FirstOrDefault(s => s.IsPrimary) ?? activeSources.First(); + var createdOrUpdated = 0; + + foreach (var source in activeSources.Where(s => !string.Equals(s.Alias, primary.Alias, StringComparison.OrdinalIgnoreCase))) + { + if (!sapSourceFieldMap.TryGetValue(primary.Alias, out var leftFields) || leftFields.Count == 0) + continue; + if (!sapSourceFieldMap.TryGetValue(source.Alias, out var rightFields) || rightFields.Count == 0) + continue; + + var matchingFields = leftFields + .Intersect(rightFields, StringComparer.OrdinalIgnoreCase) + .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (matchingFields.Count == 0) + continue; + + var existingJoin = sapJoins.FirstOrDefault(j => + string.Equals(j.LeftAlias, primary.Alias, StringComparison.OrdinalIgnoreCase) && + string.Equals(j.RightAlias, source.Alias, StringComparison.OrdinalIgnoreCase)); + + var keyList = string.Join(',', matchingFields); + if (existingJoin is null) + { + sapJoins.Add(new SapJoinDefinition + { + LeftAlias = primary.Alias, + RightAlias = source.Alias, + LeftKeys = keyList, + RightKeys = keyList, + JoinType = "Left", + IsActive = true, + SortOrder = sapJoins.Count + }); + } + else + { + existingJoin.LeftKeys = keyList; + existingJoin.RightKeys = keyList; + existingJoin.JoinType = "Left"; + existingJoin.IsActive = true; + } + + createdOrUpdated++; + } + + if (createdOrUpdated == 0) + return SapAutoMatchResult.InfoResult("Kein passender Join-Vorschlag gefunden."); + + NormalizeSapConfigCollections(sapSources, sapJoins, []); + return SapAutoMatchResult.SuccessResult($"{createdOrUpdated} Join-Vorschlaege gesetzt."); + } + + public void RemoveSapJoin(List sapJoins, SapJoinDefinition join) + => sapJoins.Remove(join); + + public void AddSapMapping(List sapMappings, IReadOnlyList salesRecordFields, List sapAvailableSourceExpressions) + { + sapMappings.Add(new SapFieldMapping + { + TargetField = salesRecordFields.First(), + SourceExpression = sapAvailableSourceExpressions.FirstOrDefault() ?? "=SAP", + IsActive = true, + SortOrder = sapMappings.Count + }); + } + + public void RemoveSapMapping(List sapMappings, SapFieldMapping mapping) + => sapMappings.Remove(mapping); + + public List BuildSourceExpressionsFromMappings(List sapMappings) + => sapMappings + .Select(m => m.SourceExpression) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) + .ToList(); + + public Dictionary> BuildSourceFieldMapFromJoins(List sapJoins) + { + var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach (var join in sapJoins) + { + AddJoinKeysToFieldMap(result, join.LeftAlias, join.LeftKeys); + AddJoinKeysToFieldMap(result, join.RightAlias, join.RightKeys); + } + + return result; + } + + public IEnumerable GetSapAliases(List sapSources) + => sapSources.Where(s => !string.IsNullOrWhiteSpace(s.Alias)).Select(s => s.Alias).Distinct(StringComparer.OrdinalIgnoreCase); + + public IEnumerable GetAvailableSourceExpressions(List sapAvailableSourceExpressions, string? currentValue) + { + var expressions = new List(sapAvailableSourceExpressions); + if (!string.IsNullOrWhiteSpace(currentValue) && !expressions.Contains(currentValue, StringComparer.OrdinalIgnoreCase)) + expressions.Insert(0, currentValue); + + return expressions; + } + + public IEnumerable GetAvailableJoinFields(Dictionary> sapSourceFieldMap, string? alias, string? currentKeys) + { + var values = new List(); + if (!string.IsNullOrWhiteSpace(alias) && sapSourceFieldMap.TryGetValue(alias, out var fields)) + values.AddRange(fields); + + foreach (var key in GetSelectedJoinKeys(currentKeys)) + { + if (!values.Contains(key, StringComparer.OrdinalIgnoreCase)) + values.Add(key); + } + + return values + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) + .ToList(); + } + + public void NormalizeSapConfigCollections(List sapSources, List sapJoins, List sapMappings) + { + for (var i = 0; i < sapSources.Count; i++) + sapSources[i].SortOrder = i; + for (var i = 0; i < sapJoins.Count; i++) + sapJoins[i].SortOrder = i; + for (var i = 0; i < sapMappings.Count; i++) + sapMappings[i].SortOrder = i; + + var selectedPrimaryIndex = sapSources.FindIndex(s => s.IsPrimary); + var primarySource = selectedPrimaryIndex >= 0 ? sapSources[selectedPrimaryIndex] : sapSources.FirstOrDefault(); + foreach (var source in sapSources) + source.IsPrimary = primarySource is not null && ReferenceEquals(source, primarySource); + if (sapSources.Count > 0 && sapSources.All(s => !s.IsPrimary)) + sapSources[0].IsPrimary = true; + } + + private static void AddJoinKeysToFieldMap(Dictionary> target, string alias, string keys) + { + if (string.IsNullOrWhiteSpace(alias)) + return; + + if (!target.TryGetValue(alias, out var fields)) + { + fields = []; + target[alias] = fields; + } + + foreach (var key in GetSelectedJoinKeys(keys)) + { + if (!fields.Contains(key, StringComparer.OrdinalIgnoreCase)) + fields.Add(key); + } + + fields.Sort(StringComparer.OrdinalIgnoreCase); + } + + private static HashSet GetSelectedJoinKeys(string? keys) + => keys? + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .ToHashSet(StringComparer.OrdinalIgnoreCase) + ?? []; +} + +public sealed class SapAutoMatchResult +{ + public bool Success { get; init; } + public bool Warning { get; init; } + public bool Info { get; init; } + public string Message { get; init; } = string.Empty; + + public static SapAutoMatchResult WarningResult(string message) => new() { Warning = true, Message = message }; + public static SapAutoMatchResult InfoResult(string message) => new() { Info = true, Message = message }; + public static SapAutoMatchResult SuccessResult(string message) => new() { Success = true, Message = message }; +} diff --git a/TrafagSalesExporter/Services/TransformationsPageService.cs b/TrafagSalesExporter/Services/TransformationsPageService.cs new file mode 100644 index 0000000..6d8ae28 --- /dev/null +++ b/TrafagSalesExporter/Services/TransformationsPageService.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore; +using TrafagSalesExporter.Data; +using TrafagSalesExporter.Models; + +namespace TrafagSalesExporter.Services; + +public interface ITransformationsPageService +{ + Task LoadAsync(); + Task> SaveAllAsync(List rules); +} + +public sealed class TransformationsPageService : ITransformationsPageService +{ + private readonly IDbContextFactory _dbFactory; + + public TransformationsPageService(IDbContextFactory dbFactory) + { + _dbFactory = dbFactory; + } + + public async Task LoadAsync() + { + await using var db = await _dbFactory.CreateDbContextAsync(); + var rules = await db.FieldTransformationRules.OrderBy(r => r.SortOrder).ThenBy(r => r.Id).ToListAsync(); + + foreach (var rule in rules) + rule.RuleScope = string.IsNullOrWhiteSpace(rule.RuleScope) ? "Value" : rule.RuleScope; + + return new TransformationsPageState + { + SourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync(), + Rules = rules + }; + } + + public async Task> SaveAllAsync(List rules) + { + await using var db = await _dbFactory.CreateDbContextAsync(); + db.FieldTransformationRules.RemoveRange(db.FieldTransformationRules); + await db.SaveChangesAsync(); + + db.FieldTransformationRules.AddRange(rules); + await db.SaveChangesAsync(); + + return await db.FieldTransformationRules.OrderBy(r => r.SortOrder).ThenBy(r => r.Id).ToListAsync(); + } +} + +public sealed class TransformationsPageState +{ + public List Rules { get; set; } = []; + public List SourceSystems { get; set; } = []; +} diff --git a/TrafagSalesExporter/TrafagSalesExporter.Tests/DatabaseInitializationServiceTests.cs b/TrafagSalesExporter/TrafagSalesExporter.Tests/DatabaseInitializationServiceTests.cs index 950b889..38f03a9 100644 --- a/TrafagSalesExporter/TrafagSalesExporter.Tests/DatabaseInitializationServiceTests.cs +++ b/TrafagSalesExporter/TrafagSalesExporter.Tests/DatabaseInitializationServiceTests.cs @@ -1,6 +1,7 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using TrafagSalesExporter.Data; +using TrafagSalesExporter.Models; using TrafagSalesExporter.Services; namespace TrafagSalesExporter.Tests; @@ -37,7 +38,7 @@ public class DatabaseInitializationServiceTests : IDisposable { await PrepareLegacySitesTableAsync(); - var service = new DatabaseInitializationService(_dbFactory); + var service = CreateService(); await service.InitializeAsync(); await using var db = await _dbFactory.CreateDbContextAsync(); @@ -59,7 +60,7 @@ public class DatabaseInitializationServiceTests : IDisposable { await PrepareBrokenHanaServerForeignKeyAsync(); - var service = new DatabaseInitializationService(_dbFactory); + var service = CreateService(); await service.InitializeAsync(); await using var db = await _dbFactory.CreateDbContextAsync(); @@ -72,6 +73,25 @@ public class DatabaseInitializationServiceTests : IDisposable Assert.DoesNotContain("HanaServers_repair_old", tableSql, StringComparison.OrdinalIgnoreCase); } + [Fact] + public async Task InitializeAsync_Seeds_Default_SourceSystems_And_Central_HanaServers() + { + var service = CreateService(); + + await service.InitializeAsync(); + + await using var db = await _dbFactory.CreateDbContextAsync(); + + Assert.Contains(db.SourceSystemDefinitions, x => x.Code == "SAP" && x.ConnectionKind == SourceSystemConnectionKinds.SapGateway); + Assert.Contains(db.SourceSystemDefinitions, x => x.Code == "BI1" && x.ConnectionKind == SourceSystemConnectionKinds.Hana); + Assert.Contains(db.SourceSystemDefinitions, x => x.Code == "SAGE" && x.ConnectionKind == SourceSystemConnectionKinds.Hana); + Assert.Contains(db.SourceSystemDefinitions, x => x.Code == "MANUAL_EXCEL" && x.ConnectionKind == SourceSystemConnectionKinds.ManualExcel); + + Assert.Contains(db.HanaServers, x => x.SourceSystem == "BI1"); + Assert.Contains(db.HanaServers, x => x.SourceSystem == "SAGE"); + Assert.Equal(2, db.FieldTransformationRules.Count(x => x.SourceSystem == "MANUAL_EXCEL")); + } + private async Task PrepareLegacySitesTableAsync() { await using var db = await _dbFactory.CreateDbContextAsync(); @@ -179,6 +199,9 @@ VALUES ( return (await command.ExecuteScalarAsync())?.ToString() ?? string.Empty; } + private DatabaseInitializationService CreateService() + => new(_dbFactory, new DatabaseSchemaMaintenanceService(), new DatabaseSeedService()); + private sealed class TestDbContextFactory : IDbContextFactory { private readonly DbContextOptions _options;