722 lines
34 KiB
C#
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);
|
|
}
|