using System.Text.Json; using ClosedXML.Excel; using Microsoft.EntityFrameworkCore; using Microsoft.VisualBasic.FileIO; 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, bool isManualExcelSite, List sapSources, List sapJoins, List sapMappings, List manualExcelMappings, List sapEntitySetsCache); Task DeleteSiteAsync(Site site); Task> LoadAvailableSchemasAsync(Site site); Task RefreshSapEntitySetsAsync(Site site); Task RefreshSapSourceFieldsAsync(Site site, List sapSources, List sapMappings); Task> LoadManualExcelHeadersAsync(string manualImportFilePath); Task ValidateManualImportPathAsync(string manualImportFilePath); } public sealed class StandortePageService : IStandortePageService { private readonly IDbContextFactory _dbFactory; private readonly IHanaQueryService _hanaService; private readonly ISapGatewayService _sapGatewayService; private readonly IStandorteSapEditorService _sapEditorService; private readonly ISharePointUploadService _sharePointService; private readonly IAppEventLogService _appEventLogService; public StandortePageService( IDbContextFactory dbFactory, IHanaQueryService hanaService, ISapGatewayService sapGatewayService, IStandorteSapEditorService sapEditorService, ISharePointUploadService sharePointService, IAppEventLogService appEventLogService) { _dbFactory = dbFactory; _hanaService = hanaService; _sapGatewayService = sapGatewayService; _sapEditorService = sapEditorService; _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 _hanaService.TestConnectionDetailedAsync(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(); var manualExcelMappings = await db.ManualExcelColumnMappings.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, ManualExcelMappings = manualExcelMappings }; } public async Task SaveSiteAsync(Site site, bool usesHanaConnection, bool isSapSite, bool isManualExcelSite, List sapSources, List sapJoins, List sapMappings, List manualExcelMappings, 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); await SaveManualExcelConfigurationAsync(db, site.Id, isManualExcelSite, manualExcelMappings); } 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 manualMappings = await db.ManualExcelColumnMappings.Where(m => m.SiteId == site.Id).ToListAsync(); var centralRows = await db.CentralSalesRecords.Where(r => r.SiteId == site.Id).ToListAsync(); var exportLogs = await db.ExportLogs.Where(l => l.SiteId == site.Id).ToListAsync(); var appEventLogs = await db.AppEventLogs.Where(l => l.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 (manualMappings.Count > 0) db.ManualExcelColumnMappings.RemoveRange(manualMappings); if (centralRows.Count > 0) db.CentralSalesRecords.RemoveRange(centralRows); if (exportLogs.Count > 0) db.ExportLogs.RemoveRange(exportLogs); if (appEventLogs.Count > 0) db.AppEventLogs.RemoveRange(appEventLogs); 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 }; var schemas = await _hanaService.GetAvailableSchemasAsync(lookupServer); return schemas .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); if (string.Equals(sourceDefinition?.ConnectionKind, SourceSystemConnectionKinds.Hana, StringComparison.OrdinalIgnoreCase)) { var server = await BuildEffectiveHanaServerAsync(db, site, sourceDefinition); if (string.IsNullOrWhiteSpace(site.Schema)) throw new InvalidOperationException("Bitte zuerst ein HANA-Schema eintragen."); var tables = await _hanaService.GetAvailableTablesAsync(server, site.Schema); return new SapEntitySetRefreshResult { EntitySets = tables, RefreshedAtUtc = DateTime.UtcNow }; } 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); if (string.Equals(sourceDefinition?.ConnectionKind, SourceSystemConnectionKinds.Hana, StringComparison.OrdinalIgnoreCase)) { var server = await BuildEffectiveHanaServerAsync(db, site, sourceDefinition); if (string.IsNullOrWhiteSpace(site.Schema)) throw new InvalidOperationException("Bitte zuerst ein HANA-Schema eintragen."); var hanaExpressions = new List { "=HANA" }; var hanaSourceFieldMap = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (var source in activeSources) { var fieldNames = await _hanaService.GetTableFieldNamesAsync(server, site.Schema, source.EntitySet); hanaSourceFieldMap[source.Alias] = fieldNames; hanaExpressions.AddRange(fieldNames.Select(field => $"{source.Alias}.{field}")); } foreach (var current in sapMappings.Select(m => m.SourceExpression).Where(x => !string.IsNullOrWhiteSpace(x))) { if (!hanaExpressions.Contains(current, StringComparer.OrdinalIgnoreCase)) hanaExpressions.Add(current); } return new SapSourceFieldRefreshResult { SourceFieldMap = hanaSourceFieldMap, SourceExpressions = hanaExpressions .Where(x => !string.IsNullOrWhiteSpace(x)) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) .ToList() }; } 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 (!IsSupportedManualImportFile(trimmedPath)) throw new InvalidOperationException("Bitte eine Excel- oder CSV-Datei mit Endung .xlsx oder .csv 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); } } public async Task> LoadManualExcelHeadersAsync(string manualImportFilePath) { var filePath = await ResolveManualImportFilePathAsync(manualImportFilePath); var deleteAfterRead = !string.Equals(filePath, manualImportFilePath?.Trim(), StringComparison.OrdinalIgnoreCase); try { return string.Equals(Path.GetExtension(manualImportFilePath?.Trim()), ".csv", StringComparison.OrdinalIgnoreCase) ? LoadCsvHeaders(filePath) : LoadExcelHeaders(filePath); } finally { if (deleteAfterRead && File.Exists(filePath)) File.Delete(filePath); } } private async Task ResolveManualImportFilePathAsync(string manualImportFilePath) { var trimmedPath = manualImportFilePath.Trim(); if (string.IsNullOrWhiteSpace(trimmedPath)) throw new InvalidOperationException("Bitte zuerst einen Dateipfad eintragen."); if (File.Exists(trimmedPath)) return 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."); } return await _sharePointService.DownloadToTempFileAsync( spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret, spConfig.SiteUrl, trimmedPath); } 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 bool IsSupportedManualImportFile(string path) => string.Equals(Path.GetExtension(path), ".xlsx", StringComparison.OrdinalIgnoreCase) || string.Equals(Path.GetExtension(path), ".csv", StringComparison.OrdinalIgnoreCase); private static List LoadExcelHeaders(string filePath) { using var workbook = new XLWorkbook(filePath); var worksheet = workbook.Worksheets.FirstOrDefault() ?? throw new InvalidOperationException("Die Excel-Datei enthaelt kein Arbeitsblatt."); var usedRange = worksheet.RangeUsed() ?? throw new InvalidOperationException("Die Excel-Datei enthaelt keine Daten."); return usedRange.FirstRow().CellsUsed() .Select(cell => cell.GetString().Trim()) .Where(x => !string.IsNullOrWhiteSpace(x)) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) .ToList(); } private static List LoadCsvHeaders(string filePath) { using var parser = new TextFieldParser(filePath) { TextFieldType = FieldType.Delimited, HasFieldsEnclosedInQuotes = true, TrimWhiteSpace = false }; parser.SetDelimiters(";"); var header = parser.ReadFields() ?? throw new InvalidOperationException("Die CSV-Datei enthaelt keine Kopfzeile."); return header .Select(x => x.Trim()) .Where(x => !string.IsNullOrWhiteSpace(x)) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) .ToList(); } private static void NormalizeManualExcelMappings(List manualExcelMappings) { for (var i = 0; i < manualExcelMappings.Count; i++) manualExcelMappings[i].SortOrder = i; } private 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) { _sapEditorService.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 SaveManualExcelConfigurationAsync(AppDbContext db, int siteId, bool isManualExcelSite, List manualExcelMappings) { var oldMappings = await db.ManualExcelColumnMappings.Where(m => m.SiteId == siteId).ToListAsync(); if (oldMappings.Count > 0) db.ManualExcelColumnMappings.RemoveRange(oldMappings); if (isManualExcelSite) { NormalizeManualExcelMappings(manualExcelMappings); foreach (var mapping in manualExcelMappings) mapping.SiteId = siteId; db.ManualExcelColumnMappings.AddRange(manualExcelMappings); } 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; } private static async Task BuildEffectiveHanaServerAsync(AppDbContext db, Site site, SourceSystemDefinition? sourceDefinition) { var normalizedSourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) ? string.Empty : site.SourceSystem.Trim().ToUpperInvariant(); var centralServer = await db.HanaServers .AsNoTracking() .OrderBy(x => x.Id) .FirstOrDefaultAsync(x => x.SourceSystem == normalizedSourceSystem) ?? throw new InvalidOperationException($"Fuer Quellsystem '{normalizedSourceSystem}' ist keine 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 {normalizedSourceSystem} sind weder zentrale Zugangsdaten noch Standort-Overrides gesetzt."); return 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 }; } } 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 List ManualExcelMappings { 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); }