Files

722 lines
34 KiB
C#

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<StandortePageState> LoadAsync();
Task SaveServerAsync(HanaServer server, IEnumerable<string> hanaSourceSystemCodes);
Task DeleteServerAsync(HanaServer server);
Task<ConnectionTestResult> TestServerConnectionAsync(HanaServer server);
Task<StandortEditorState> LoadSiteEditorAsync(Site site, IEnumerable<SourceSystemDefinition> sourceSystems);
Task SaveSiteAsync(Site site, bool usesHanaConnection, bool isSapSite, bool isManualExcelSite, List<SapSourceDefinition> sapSources, List<SapJoinDefinition> sapJoins, List<SapFieldMapping> sapMappings, List<ManualExcelColumnMapping> manualExcelMappings, List<string> sapEntitySetsCache);
Task DeleteSiteAsync(Site site);
Task<List<string>> LoadAvailableSchemasAsync(Site site);
Task<SapEntitySetRefreshResult> RefreshSapEntitySetsAsync(Site site);
Task<SapSourceFieldRefreshResult> RefreshSapSourceFieldsAsync(Site site, List<SapSourceDefinition> sapSources, List<SapFieldMapping> sapMappings);
Task<List<string>> LoadManualExcelHeadersAsync(string manualImportFilePath);
Task<DateTime> ValidateManualImportPathAsync(string manualImportFilePath);
}
public sealed class StandortePageService : IStandortePageService
{
private readonly IDbContextFactory<AppDbContext> _dbFactory;
private readonly IHanaQueryService _hanaService;
private readonly ISapGatewayService _sapGatewayService;
private readonly IStandorteSapEditorService _sapEditorService;
private readonly ISharePointUploadService _sharePointService;
private readonly IAppEventLogService _appEventLogService;
public StandortePageService(
IDbContextFactory<AppDbContext> 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<StandortePageState> 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<string> 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<ConnectionTestResult> 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<StandortEditorState> LoadSiteEditorAsync(Site site, IEnumerable<SourceSystemDefinition> 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<SapSourceDefinition> sapSources, List<SapJoinDefinition> sapJoins, List<SapFieldMapping> sapMappings, List<ManualExcelColumnMapping> manualExcelMappings, List<string> 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<List<string>> 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<SapEntitySetRefreshResult> 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<SapSourceFieldRefreshResult> RefreshSapSourceFieldsAsync(Site site, List<SapSourceDefinition> sapSources, List<SapFieldMapping> 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<string> { "=HANA" };
var hanaSourceFieldMap = new Dictionary<string, List<string>>(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<string> { "=SAP" };
var sourceFieldMap = new Dictionary<string, List<string>>(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<DateTime> ValidateManualImportPathAsync(string manualImportFilePath)
{
var trimmedPath = manualImportFilePath.Trim();
if (string.IsNullOrWhiteSpace(trimmedPath))
throw new InvalidOperationException("Bitte zuerst einen Dateipfad eintragen.");
var isSharePointReference = LooksLikeSharePointReference(trimmedPath);
if (!isSharePointReference && !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 (!isSharePointReference)
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 sharePointFileReference = trimmedPath;
if (!IsSupportedManualImportFile(trimmedPath))
{
var latestFile = await _sharePointService.ResolveLatestFileInFolderAsync(
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret, spConfig.SiteUrl, trimmedPath, string.Empty);
sharePointFileReference = latestFile.FileReference;
}
var tempPath = await _sharePointService.DownloadToTempFileAsync(
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret, spConfig.SiteUrl, sharePointFileReference);
try
{
return File.GetLastWriteTimeUtc(tempPath);
}
finally
{
if (File.Exists(tempPath))
File.Delete(tempPath);
}
}
public async Task<List<string>> LoadManualExcelHeadersAsync(string manualImportFilePath)
{
var filePath = await ResolveManualImportFilePathAsync(manualImportFilePath);
var deleteAfterRead = !string.Equals(filePath, manualImportFilePath?.Trim(), StringComparison.OrdinalIgnoreCase);
try
{
return string.Equals(Path.GetExtension(filePath), ".csv", StringComparison.OrdinalIgnoreCase)
? LoadCsvHeaders(filePath)
: LoadExcelHeaders(filePath);
}
finally
{
if (deleteAfterRead && File.Exists(filePath))
File.Delete(filePath);
}
}
private async Task<string> 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.");
}
var sharePointFileReference = trimmedPath;
if (!IsSupportedManualImportFile(trimmedPath))
{
var latestFile = await _sharePointService.ResolveLatestFileInFolderAsync(
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret, spConfig.SiteUrl, trimmedPath, string.Empty);
sharePointFileReference = latestFile.FileReference;
}
return await _sharePointService.DownloadToTempFileAsync(
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret, spConfig.SiteUrl, sharePointFileReference);
}
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<string> ParseSapEntitySets(string json)
{
if (string.IsNullOrWhiteSpace(json))
return [];
try
{
return JsonSerializer.Deserialize<List<string>>(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<string> 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<string> 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<ManualExcelColumnMapping> manualExcelMappings)
{
for (var i = 0; i < manualExcelMappings.Count; i++)
manualExcelMappings[i].SortOrder = i;
}
private async Task SaveSapConfigurationAsync(AppDbContext db, int siteId, bool isSapSite, List<SapSourceDefinition> sapSources, List<SapJoinDefinition> sapJoins, List<SapFieldMapping> 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<ManualExcelColumnMapping> 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<int> 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<HanaServer> 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<SourceSystemDefinition> SourceSystems { get; set; } = [];
public List<HanaServer> Servers { get; set; } = [];
public List<Site> Sites { get; set; } = [];
}
public sealed class StandortEditorState
{
public Site Site { get; set; } = new();
public List<string> SapEntitySets { get; set; } = [];
public List<SapSourceDefinition> SapSources { get; set; } = [];
public List<SapJoinDefinition> SapJoins { get; set; } = [];
public List<SapFieldMapping> SapMappings { get; set; } = [];
public List<ManualExcelColumnMapping> ManualExcelMappings { get; set; } = [];
}
public sealed class SapEntitySetRefreshResult
{
public List<string> EntitySets { get; set; } = [];
public DateTime RefreshedAtUtc { get; set; }
}
public sealed class SapSourceFieldRefreshResult
{
public List<string> SourceExpressions { get; set; } = [];
public Dictionary<string, List<string>> SourceFieldMap { get; set; } = new(StringComparer.OrdinalIgnoreCase);
}