SAP GWQ
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
var conn = new SqliteConnection(@"Data Source=C:\Users\koi\source\repos\Ai\TrafagSalesExporter\trafag_exporter.db");
|
||||
await conn.OpenAsync();
|
||||
string sapUsername = "", sapPassword = "";
|
||||
var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = "select SapUsername, SapPassword from ExportSettings limit 1";
|
||||
using (var r = await cmd.ExecuteReaderAsync())
|
||||
{
|
||||
if (await r.ReadAsync())
|
||||
{
|
||||
sapUsername = r.IsDBNull(0) ? "" : r.GetString(0);
|
||||
sapPassword = r.IsDBNull(1) ? "" : r.GetString(1);
|
||||
}
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(sapUsername) || string.IsNullOrWhiteSpace(sapPassword)) throw new Exception("Central SAP credentials missing");
|
||||
var serviceUrl = @"http://travt762.sap.trafag.com:8000/sap/opu/odata/sap/ZPOWERBI_EINKAUF_SRV/";
|
||||
using var client = new HttpClient();
|
||||
client.Timeout = TimeSpan.FromSeconds(20);
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{sapUsername}:{sapPassword}")));
|
||||
foreach (var url in new[]{ serviceUrl, serviceUrl + "" })
|
||||
{
|
||||
Console.WriteLine($"URL|{url}");
|
||||
using var response = await client.GetAsync(url);
|
||||
Console.WriteLine($"STATUS|{(int)response.StatusCode}|{response.ReasonPhrase}");
|
||||
foreach (var header in response.Headers)
|
||||
Console.WriteLine($"HEADER|{header.Key}|{string.Join(",", header.Value)}");
|
||||
foreach (var header in response.Content.Headers)
|
||||
Console.WriteLine($"HEADER|{header.Key}|{string.Join(",", header.Value)}");
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Console.WriteLine("BODY_START");
|
||||
Console.WriteLine(body.Length > 5000 ? body[..5000] : body);
|
||||
Console.WriteLine("BODY_END");
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.11" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -7,6 +7,7 @@
|
||||
@inject ISharePointUploadService SpService
|
||||
@inject TimerBackgroundService TimerService
|
||||
@inject IHanaQueryService HanaService
|
||||
@inject ISapGatewayService SapGatewayService
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<PageTitle>Settings</PageTitle>
|
||||
@@ -240,6 +241,17 @@
|
||||
}
|
||||
|
||||
private async Task TestCentralCredentials(string sourceSystem)
|
||||
{
|
||||
if (sourceSystem == "SAP")
|
||||
{
|
||||
await TestCentralSapCredentials();
|
||||
return;
|
||||
}
|
||||
|
||||
await TestCentralHanaCredentials(sourceSystem);
|
||||
}
|
||||
|
||||
private async Task TestCentralHanaCredentials(string sourceSystem)
|
||||
{
|
||||
if (!_testingSystems.Add(sourceSystem))
|
||||
return;
|
||||
@@ -297,6 +309,49 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TestCentralSapCredentials()
|
||||
{
|
||||
const string sourceSystem = "SAP";
|
||||
if (!_testingSystems.Add(sourceSystem))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var username = GetCentralUsername(sourceSystem);
|
||||
var password = GetCentralPassword(sourceSystem);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
Snackbar.Add("Für SAP sind keine zentralen Gateway-Zugangsdaten gepflegt.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
var site = await db.Sites
|
||||
.Where(s => (string.IsNullOrWhiteSpace(s.SourceSystem) ? "SAP" : s.SourceSystem) == sourceSystem
|
||||
&& !string.IsNullOrWhiteSpace(s.SapServiceUrl))
|
||||
.OrderBy(s => s.Land)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (site is null)
|
||||
{
|
||||
Snackbar.Add("Kein SAP-Standort mit Service URL gefunden.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
await SapGatewayService.TestConnectionAsync(site.SapServiceUrl, username.Trim(), password.Trim());
|
||||
Snackbar.Add($"SAP: Gateway-Verbindung erfolgreich über Standort '{site.Land}'.", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"SAP: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_testingSystems.Remove(sourceSystem);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetCentralUsername(string sourceSystem) => sourceSystem switch
|
||||
{
|
||||
"BI1" => _exportSettings.Bi1Username,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
@page "/standorte"
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using System.Text.Json
|
||||
@using TrafagSalesExporter.Data
|
||||
@using TrafagSalesExporter.Models
|
||||
@using TrafagSalesExporter.Services
|
||||
@inject IDbContextFactory<AppDbContext> DbFactory
|
||||
@inject IHanaQueryService HanaService
|
||||
@inject ISapGatewayService SapGatewayService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IDialogService DialogService
|
||||
|
||||
@@ -71,7 +74,7 @@
|
||||
<MudTh>TSC</MudTh>
|
||||
<MudTh>Schema</MudTh>
|
||||
<MudTh>Quellsystem</MudTh>
|
||||
<MudTh>Host</MudTh>
|
||||
<MudTh>Quelle</MudTh>
|
||||
<MudTh>Aktiv</MudTh>
|
||||
<MudTh>Aktionen</MudTh>
|
||||
</HeaderContent>
|
||||
@@ -80,7 +83,7 @@
|
||||
<MudTd>@context.TSC</MudTd>
|
||||
<MudTd>@context.Schema</MudTd>
|
||||
<MudTd>@context.SourceSystem</MudTd>
|
||||
<MudTd>@GetServerNode(context.HanaServer)</MudTd>
|
||||
<MudTd>@GetConnectionTarget(context)</MudTd>
|
||||
<MudTd>
|
||||
@if (context.IsActive)
|
||||
{
|
||||
@@ -122,8 +125,8 @@
|
||||
HelperText="Optional, z.B. sslCryptoProvider=openssl;communicationTimeout=0" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="() => _serverDialogVisible = false">Abbrechen</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveServer">Speichern</MudButton>
|
||||
<MudButton OnClick="CloseServerDialog">Abbrechen</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveServer" Disabled="_savingServer">Speichern</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@@ -138,7 +141,7 @@
|
||||
<MudSelect @bind-Value="_editingSite.SourceSystem" Label="Quellsystem" Required>
|
||||
@foreach (var system in _sourceSystems)
|
||||
{
|
||||
<MudSelectItem Value="system">@system</MudSelectItem>
|
||||
<MudSelectItem Value="@system">@system</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudTextField @bind-Value="_editingSite.UsernameOverride" Label="Username Override"
|
||||
@@ -149,29 +152,67 @@
|
||||
|
||||
<MudDivider Class="my-4" />
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-2">HANA-Verbindung</MudText>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Class="mb-3">
|
||||
Host, Port und technische HANA-Parameter kommen von dieser Verbindung. Username und Password hier dienen nur noch als Fallback für bestehende Einträge.
|
||||
</MudAlert>
|
||||
<MudTextField @bind-Value="_editingSiteServer.Name" Label="Verbindungsname" Required
|
||||
HelperText="Interner Anzeigename für diesen Standort" />
|
||||
<MudTextField @bind-Value="_editingSiteServer.Host" Label="Host oder ServerNode" Required
|
||||
HelperText="z.B. hana01 oder hana01:30015 oder derselbe HanaServer-Wert wie in Power BI" />
|
||||
<MudNumericField @bind-Value="_editingSiteServer.Port" Label="Port"
|
||||
HelperText="Wird ignoriert, wenn im Host bereits ein Port enthalten ist" />
|
||||
<MudTextField @bind-Value="_editingSiteServer.Username" Label="Username" />
|
||||
<MudTextField @bind-Value="_editingSiteServer.Password" Label="Password" InputType="InputType.Password" />
|
||||
<MudTextField @bind-Value="_editingSiteServer.DatabaseName" Label="Database Name (MDC)"
|
||||
HelperText="Nur bei Multi-Tenant Setup angeben, sonst leer lassen" />
|
||||
<MudSwitch @bind-Value="_editingSiteServer.UseSsl" Label="SSL/TLS verwenden (encrypt=true)" Color="Color.Primary" />
|
||||
<MudSwitch @bind-Value="_editingSiteServer.ValidateCertificate" Label="SSL-Zertifikat validieren" Color="Color.Primary"
|
||||
Disabled="!_editingSiteServer.UseSsl" />
|
||||
<MudTextField @bind-Value="_editingSiteServer.AdditionalParams" Label="Zusätzliche Parameter"
|
||||
HelperText="Optional, z.B. sslCryptoProvider=openssl;communicationTimeout=0" />
|
||||
@if (IsSapSite())
|
||||
{
|
||||
<MudText Typo="Typo.h6" Class="mb-2">SAP Gateway</MudText>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Class="mb-3">
|
||||
Die Service-URL zeigt auf den OData-Service. Die verfügbaren Entity Sets werden nur per Knopfdruck aktualisiert und lokal zwischengespeichert.
|
||||
</MudAlert>
|
||||
<MudTextField @bind-Value="_editingSite.SapServiceUrl" Label="SAP Service URL" Required
|
||||
HelperText="z.B. http://server:8000/sap/opu/odata/sap/ZPOWERBI_EINKAUF_SRV/" />
|
||||
<MudStack Row Spacing="2" Class="mb-3">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" OnClick="RefreshSapEntitySets"
|
||||
StartIcon="@Icons.Material.Filled.Refresh" Disabled="_refreshingSapEntitySets">
|
||||
@if (_refreshingSapEntitySets)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate Class="mr-2" />
|
||||
@("Lade...")
|
||||
}
|
||||
else
|
||||
{
|
||||
@("Quellen refreshen")
|
||||
}
|
||||
</MudButton>
|
||||
@if (_editingSite.SapEntitySetsRefreshedAtUtc.HasValue)
|
||||
{
|
||||
<MudText Typo="Typo.caption" Class="mt-2">
|
||||
Letzter Refresh: @_editingSite.SapEntitySetsRefreshedAtUtc.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss")
|
||||
</MudText>
|
||||
}
|
||||
</MudStack>
|
||||
<MudSelect @bind-Value="_editingSite.SapEntitySet" Label="SAP Entity Set" Required>
|
||||
@foreach (var entitySet in _sapEntitySetsCache)
|
||||
{
|
||||
<MudSelectItem Value="@entitySet">@entitySet</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.h6" Class="mb-2">HANA-Verbindung</MudText>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Class="mb-3">
|
||||
Host, Port und technische HANA-Parameter kommen von dieser Verbindung. Username und Password hier dienen nur noch als Fallback für bestehende Einträge.
|
||||
</MudAlert>
|
||||
<MudTextField @bind-Value="_editingSiteServer.Name" Label="Verbindungsname" Required
|
||||
HelperText="Interner Anzeigename für diesen Standort" />
|
||||
<MudTextField @bind-Value="_editingSiteServer.Host" Label="Host oder ServerNode" Required
|
||||
HelperText="z.B. hana01 oder hana01:30015 oder derselbe HanaServer-Wert wie in Power BI" />
|
||||
<MudNumericField @bind-Value="_editingSiteServer.Port" Label="Port"
|
||||
HelperText="Wird ignoriert, wenn im Host bereits ein Port enthalten ist" />
|
||||
<MudTextField @bind-Value="_editingSiteServer.Username" Label="Username" />
|
||||
<MudTextField @bind-Value="_editingSiteServer.Password" Label="Password" InputType="InputType.Password" />
|
||||
<MudTextField @bind-Value="_editingSiteServer.DatabaseName" Label="Database Name (MDC)"
|
||||
HelperText="Nur bei Multi-Tenant Setup angeben, sonst leer lassen" />
|
||||
<MudSwitch @bind-Value="_editingSiteServer.UseSsl" Label="SSL/TLS verwenden (encrypt=true)" Color="Color.Primary" />
|
||||
<MudSwitch @bind-Value="_editingSiteServer.ValidateCertificate" Label="SSL-Zertifikat validieren" Color="Color.Primary"
|
||||
Disabled="!_editingSiteServer.UseSsl" />
|
||||
<MudTextField @bind-Value="_editingSiteServer.AdditionalParams" Label="Zusätzliche Parameter"
|
||||
HelperText="Optional, z.B. sslCryptoProvider=openssl;communicationTimeout=0" />
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="() => _siteDialogVisible = false">Abbrechen</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveSite">Speichern</MudButton>
|
||||
<MudButton OnClick="CloseSiteDialog" Disabled="_savingSite">Abbrechen</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveSite" Disabled="_savingSite || _refreshingSapEntitySets">Speichern</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@@ -180,11 +221,15 @@
|
||||
private readonly Dictionary<int, ConnectionTestResult> _connectionStatus = new();
|
||||
private List<HanaServer> _servers = new();
|
||||
private List<Site> _sites = new();
|
||||
private List<string> _sapEntitySetsCache = [];
|
||||
private HanaServer _editingServer = new();
|
||||
private Site _editingSite = new();
|
||||
private HanaServer _editingSiteServer = new();
|
||||
private bool _serverDialogVisible;
|
||||
private bool _siteDialogVisible;
|
||||
private bool _refreshingSapEntitySets;
|
||||
private bool _savingServer;
|
||||
private bool _savingSite;
|
||||
private readonly DialogOptions _dialogOptions = new() { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -213,6 +258,12 @@
|
||||
|
||||
private async Task SaveServer()
|
||||
{
|
||||
if (_savingServer)
|
||||
return;
|
||||
|
||||
_savingServer = true;
|
||||
try
|
||||
{
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
if (_editingServer.Id == 0)
|
||||
{
|
||||
@@ -239,6 +290,11 @@
|
||||
_serverDialogVisible = false;
|
||||
await LoadDataAsync();
|
||||
Snackbar.Add("Server gespeichert", Severity.Success);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_savingServer = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteServer(HanaServer server)
|
||||
@@ -294,6 +350,7 @@
|
||||
SourceSystem = "SAP",
|
||||
HanaServerId = 0
|
||||
};
|
||||
_sapEntitySetsCache = [];
|
||||
_editingSiteServer = CreateDefaultSiteServer();
|
||||
_siteDialogVisible = true;
|
||||
}
|
||||
@@ -310,8 +367,13 @@
|
||||
SourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) ? "SAP" : site.SourceSystem,
|
||||
UsernameOverride = site.UsernameOverride,
|
||||
PasswordOverride = site.PasswordOverride,
|
||||
SapServiceUrl = site.SapServiceUrl,
|
||||
SapEntitySet = site.SapEntitySet,
|
||||
SapEntitySetsCache = site.SapEntitySetsCache,
|
||||
SapEntitySetsRefreshedAtUtc = site.SapEntitySetsRefreshedAtUtc,
|
||||
IsActive = site.IsActive
|
||||
};
|
||||
_sapEntitySetsCache = ParseSapEntitySets(site.SapEntitySetsCache);
|
||||
_editingSiteServer = site.HanaServer is null
|
||||
? CreateDefaultSiteServer(site)
|
||||
: CloneServer(site.HanaServer);
|
||||
@@ -320,34 +382,54 @@
|
||||
|
||||
private async Task SaveSite()
|
||||
{
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
var serverId = await SaveOrCreateSiteServerAsync(db);
|
||||
if (_savingSite)
|
||||
return;
|
||||
|
||||
if (_editingSite.Id == 0)
|
||||
_savingSite = true;
|
||||
try
|
||||
{
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
var serverId = IsSapSite() ? (int?)null : await SaveOrCreateSiteServerAsync(db);
|
||||
_editingSite.HanaServerId = serverId;
|
||||
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.IsActive = _editingSite.IsActive;
|
||||
}
|
||||
}
|
||||
_editingSite.SapEntitySetsCache = SerializeSapEntitySets(_sapEntitySetsCache);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
_siteDialogVisible = false;
|
||||
await LoadDataAsync();
|
||||
Snackbar.Add("Standort gespeichert", Severity.Success);
|
||||
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.SapServiceUrl = _editingSite.SapServiceUrl;
|
||||
existing.SapEntitySet = _editingSite.SapEntitySet;
|
||||
existing.SapEntitySetsCache = _editingSite.SapEntitySetsCache;
|
||||
existing.SapEntitySetsRefreshedAtUtc = _editingSite.SapEntitySetsRefreshedAtUtc;
|
||||
existing.IsActive = _editingSite.IsActive;
|
||||
}
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
_siteDialogVisible = false;
|
||||
await LoadDataAsync();
|
||||
Snackbar.Add("Standort gespeichert", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Speichern fehlgeschlagen: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_savingSite = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteSite(Site site)
|
||||
@@ -379,6 +461,15 @@
|
||||
return server.Host.Contains(':', StringComparison.Ordinal) ? server.Host : $"{server.Host}:{server.Port}";
|
||||
}
|
||||
|
||||
private static string GetConnectionTarget(Site site)
|
||||
{
|
||||
var sourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) ? "SAP" : site.SourceSystem;
|
||||
if (string.Equals(sourceSystem, "SAP", StringComparison.OrdinalIgnoreCase))
|
||||
return string.IsNullOrWhiteSpace(site.SapServiceUrl) ? "-" : site.SapServiceUrl;
|
||||
|
||||
return GetServerNode(site.HanaServer);
|
||||
}
|
||||
|
||||
private HanaServer CreateDefaultSiteServer(Site? site = null)
|
||||
{
|
||||
var label = !string.IsNullOrWhiteSpace(site?.Land) ? site!.Land : site?.TSC;
|
||||
@@ -416,6 +507,8 @@
|
||||
: _editingSiteServer.Name.Trim();
|
||||
_editingSite.UsernameOverride = _editingSite.UsernameOverride.Trim();
|
||||
_editingSite.PasswordOverride = _editingSite.PasswordOverride.Trim();
|
||||
_editingSite.SapServiceUrl = _editingSite.SapServiceUrl.Trim();
|
||||
_editingSite.SapEntitySet = _editingSite.SapEntitySet.Trim();
|
||||
_editingSiteServer.Host = _editingSiteServer.Host.Trim();
|
||||
_editingSiteServer.Username = _editingSiteServer.Username.Trim();
|
||||
_editingSiteServer.DatabaseName = _editingSiteServer.DatabaseName.Trim();
|
||||
@@ -461,4 +554,82 @@
|
||||
await db.SaveChangesAsync();
|
||||
return existingServer.Id;
|
||||
}
|
||||
|
||||
private bool IsSapSite() => string.Equals(_editingSite.SourceSystem, "SAP", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private async Task RefreshSapEntitySets()
|
||||
{
|
||||
if (_refreshingSapEntitySets)
|
||||
return;
|
||||
|
||||
_refreshingSapEntitySets = true;
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_editingSite.SapServiceUrl))
|
||||
throw new InvalidOperationException("SAP Service URL muss gesetzt sein.");
|
||||
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
var settings = await db.ExportSettings.FirstOrDefaultAsync() ?? new();
|
||||
var username = string.IsNullOrWhiteSpace(_editingSite.UsernameOverride) ? settings.SapUsername : _editingSite.UsernameOverride;
|
||||
var password = string.IsNullOrWhiteSpace(_editingSite.PasswordOverride) ? settings.SapPassword : _editingSite.PasswordOverride;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
throw new InvalidOperationException("Für SAP sind weder zentrale Zugangsdaten noch Standort-Overrides gesetzt.");
|
||||
|
||||
var entitySets = await SapGatewayService.GetEntitySetsAsync(_editingSite.SapServiceUrl, username.Trim(), password.Trim());
|
||||
_sapEntitySetsCache = entitySets;
|
||||
_editingSite.SapEntitySetsCache = SerializeSapEntitySets(entitySets);
|
||||
_editingSite.SapEntitySetsRefreshedAtUtc = DateTime.UtcNow;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_editingSite.SapEntitySet) &&
|
||||
!_sapEntitySetsCache.Contains(_editingSite.SapEntitySet, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
_editingSite.SapEntitySet = string.Empty;
|
||||
}
|
||||
|
||||
Snackbar.Add($"{entitySets.Count} SAP Entity Sets geladen.", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add(ex.Message, Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_refreshingSapEntitySets = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseServerDialog()
|
||||
{
|
||||
if (_savingServer)
|
||||
return;
|
||||
|
||||
_serverDialogVisible = false;
|
||||
}
|
||||
|
||||
private void CloseSiteDialog()
|
||||
{
|
||||
if (_savingSite || _refreshingSapEntitySets)
|
||||
return;
|
||||
|
||||
_siteDialogVisible = false;
|
||||
}
|
||||
|
||||
private static List<string> ParseSapEntitySets(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return [];
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<List<string>>(json) ?? [];
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static string SerializeSapEntitySets(List<string> entitySets)
|
||||
=> JsonSerializer.Serialize(entitySets);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ public class Site
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int HanaServerId { get; set; }
|
||||
public int? HanaServerId { get; set; }
|
||||
|
||||
[ForeignKey(nameof(HanaServerId))]
|
||||
public HanaServer? HanaServer { get; set; }
|
||||
@@ -28,5 +28,13 @@ public class Site
|
||||
|
||||
public string PasswordOverride { get; set; } = string.Empty;
|
||||
|
||||
public string SapServiceUrl { get; set; } = string.Empty;
|
||||
|
||||
public string SapEntitySet { get; set; } = string.Empty;
|
||||
|
||||
public string SapEntitySetsCache { get; set; } = string.Empty;
|
||||
|
||||
public DateTime? SapEntitySetsRefreshedAtUtc { get; set; }
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ builder.Services.AddDbContextFactory<AppDbContext>(options =>
|
||||
builder.Services.AddSingleton<IHanaQueryService, HanaQueryService>();
|
||||
builder.Services.AddSingleton<IExcelExportService, ExcelExportService>();
|
||||
builder.Services.AddSingleton<ISharePointUploadService, SharePointUploadService>();
|
||||
builder.Services.AddSingleton<ISapGatewayService, SapGatewayService>();
|
||||
builder.Services.AddSingleton<ITransformationStrategy, CopyTransformationStrategy>();
|
||||
builder.Services.AddSingleton<ITransformationStrategy, UppercaseTransformationStrategy>();
|
||||
builder.Services.AddSingleton<ITransformationStrategy, LowercaseTransformationStrategy>();
|
||||
|
||||
@@ -24,6 +24,7 @@ public class DatabaseInitializationService : IDatabaseInitializationService
|
||||
|
||||
private static void EnsureSchema(AppDbContext db)
|
||||
{
|
||||
EnsureSitesTableSupportsOptionalHanaServer(db);
|
||||
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");
|
||||
@@ -31,6 +32,10 @@ public class DatabaseInitializationService : IDatabaseInitializationService
|
||||
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", "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", "SapUsername", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "ExportSettings", "SapPassword", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "ExportSettings", "Bi1Username", "TEXT NOT NULL DEFAULT ''");
|
||||
@@ -40,6 +45,104 @@ public class DatabaseInitializationService : IDatabaseInitializationService
|
||||
EnsureTransformationTable(db);
|
||||
}
|
||||
|
||||
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 = @"
|
||||
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 '',
|
||||
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)
|
||||
);";
|
||||
create.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
using (var copy = conn.CreateCommand())
|
||||
{
|
||||
copy.Transaction = transaction;
|
||||
copy.CommandText = @"
|
||||
INSERT INTO Sites (
|
||||
Id, HanaServerId, Schema, TSC, Land, SourceSystem,
|
||||
UsernameOverride, PasswordOverride, SapServiceUrl, SapEntitySet,
|
||||
SapEntitySetsCache, SapEntitySetsRefreshedAtUtc, IsActive
|
||||
)
|
||||
SELECT
|
||||
Id, HanaServerId, Schema, TSC, Land,
|
||||
COALESCE(SourceSystem, 'SAP'),
|
||||
COALESCE(UsernameOverride, ''),
|
||||
COALESCE(PasswordOverride, ''),
|
||||
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 AddColumnIfMissing(AppDbContext db, string table, string column, string type)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
|
||||
@@ -23,6 +23,16 @@ public class ExcelExportService : IExcelExportService
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
public string CreateGenericExcelFile(string outputDirectory, string filePrefix, DateTime fileDate, string worksheetName, IReadOnlyList<IReadOnlyDictionary<string, object?>> rows)
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
var safePrefix = string.IsNullOrWhiteSpace(filePrefix) ? "Export" : filePrefix.Trim();
|
||||
var fileName = $"{safePrefix}_{fileDate:yyyy-MM-dd}.xlsx";
|
||||
var fullPath = Path.Combine(outputDirectory, fileName);
|
||||
WriteGenericWorkbook(fullPath, worksheetName, rows);
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
private static void WriteWorkbook(string fullPath, List<SalesRecord> records)
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
@@ -99,4 +109,35 @@ public class ExcelExportService : IExcelExportService
|
||||
ws.Columns().AdjustToContents();
|
||||
workbook.SaveAs(fullPath);
|
||||
}
|
||||
|
||||
private static void WriteGenericWorkbook(string fullPath, string worksheetName, IReadOnlyList<IReadOnlyDictionary<string, object?>> rows)
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var sheetName = string.IsNullOrWhiteSpace(worksheetName) ? "Export" : worksheetName.Trim();
|
||||
var ws = workbook.Worksheets.Add(sheetName.Length > 31 ? sheetName[..31] : sheetName);
|
||||
|
||||
var headers = rows
|
||||
.SelectMany(r => r.Keys)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
for (var i = 0; i < headers.Count; i++)
|
||||
{
|
||||
ws.Cell(1, i + 1).Value = headers[i];
|
||||
ws.Cell(1, i + 1).Style.Font.Bold = true;
|
||||
}
|
||||
|
||||
for (var rowIndex = 0; rowIndex < rows.Count; rowIndex++)
|
||||
{
|
||||
var row = rows[rowIndex];
|
||||
for (var colIndex = 0; colIndex < headers.Count; colIndex++)
|
||||
{
|
||||
row.TryGetValue(headers[colIndex], out var value);
|
||||
ws.Cell(rowIndex + 2, colIndex + 1).Value = value?.ToString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
ws.Columns().AdjustToContents();
|
||||
workbook.SaveAs(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,6 @@ public class ExportOrchestrationService
|
||||
|
||||
private async Task<SiteExportResult?> ExportSiteAsync(Site site)
|
||||
{
|
||||
if (site.HanaServer is null) return null;
|
||||
SiteExportResult? result = null;
|
||||
|
||||
lock (_lock)
|
||||
|
||||
@@ -6,4 +6,5 @@ public interface IExcelExportService
|
||||
{
|
||||
string CreateExcelFile(string outputDirectory, string tsc, DateTime fileDate, List<SalesRecord> records);
|
||||
string CreateConsolidatedExcelFile(string outputDirectory, DateTime fileDate, List<SalesRecord> records);
|
||||
string CreateGenericExcelFile(string outputDirectory, string filePrefix, DateTime fileDate, string worksheetName, IReadOnlyList<IReadOnlyDictionary<string, object?>> rows);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface ISapGatewayService
|
||||
{
|
||||
Task TestConnectionAsync(string serviceUrl, string username, string password, CancellationToken cancellationToken = default);
|
||||
Task<List<string>> GetEntitySetsAsync(string serviceUrl, string username, string password, CancellationToken cancellationToken = default);
|
||||
Task<List<Dictionary<string, object?>>> GetEntityRowsAsync(string serviceUrl, string entitySet, string username, string password, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class SapGatewayService : ISapGatewayService
|
||||
{
|
||||
private static readonly XNamespace AppNs = "http://www.w3.org/2007/app";
|
||||
private static readonly XNamespace EdmNs = "http://docs.oasis-open.org/odata/ns/edm";
|
||||
|
||||
public async Task TestConnectionAsync(string serviceUrl, string username, string password, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var client = CreateClient(username, password);
|
||||
using var response = await client.GetAsync(BuildServiceUri(serviceUrl), cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetEntitySetsAsync(string serviceUrl, string username, string password, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var client = CreateClient(username, password);
|
||||
var baseUrl = BuildServiceUri(serviceUrl);
|
||||
|
||||
var entitySets = await TryReadEntitySetsFromServiceRootAsync(client, baseUrl, cancellationToken);
|
||||
if (entitySets.Count > 0)
|
||||
return entitySets;
|
||||
|
||||
return await ReadEntitySetsFromMetadataAsync(client, baseUrl, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<Dictionary<string, object?>>> GetEntityRowsAsync(string serviceUrl, string entitySet, string username, string password, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var client = CreateClient(username, password);
|
||||
var requestUrl = $"{BuildServiceUri(serviceUrl)}{entitySet}?$format=json";
|
||||
using var response = await client.GetAsync(requestUrl, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
using var document = JsonDocument.Parse(json);
|
||||
if (!document.RootElement.TryGetProperty("d", out var dNode))
|
||||
return [];
|
||||
|
||||
if (!dNode.TryGetProperty("results", out var resultsNode) || resultsNode.ValueKind != JsonValueKind.Array)
|
||||
return [];
|
||||
|
||||
var rows = new List<Dictionary<string, object?>>();
|
||||
foreach (var item in resultsNode.EnumerateArray())
|
||||
{
|
||||
var row = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var property in item.EnumerateObject())
|
||||
{
|
||||
row[property.Name] = ConvertJsonValue(property.Value);
|
||||
}
|
||||
|
||||
rows.Add(row);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static HttpClient CreateClient(string username, string password)
|
||||
{
|
||||
var client = new HttpClient();
|
||||
client.Timeout = TimeSpan.FromSeconds(15);
|
||||
client.DefaultRequestHeaders.Accept.Clear();
|
||||
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/atomsvc+xml"));
|
||||
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
|
||||
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
|
||||
"Basic",
|
||||
Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}")));
|
||||
return client;
|
||||
}
|
||||
|
||||
private static string BuildServiceUri(string serviceUrl)
|
||||
{
|
||||
var trimmed = serviceUrl.Trim();
|
||||
if (string.IsNullOrWhiteSpace(trimmed))
|
||||
throw new InvalidOperationException("SAP Service URL darf nicht leer sein.");
|
||||
|
||||
var entityPathMarker = "/sap/opu/odata/sap/";
|
||||
var markerIndex = trimmed.IndexOf(entityPathMarker, StringComparison.OrdinalIgnoreCase);
|
||||
if (markerIndex >= 0)
|
||||
{
|
||||
var servicePath = trimmed[(markerIndex + entityPathMarker.Length)..].Trim('/');
|
||||
var parts = servicePath.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
trimmed = $"{trimmed[..(markerIndex + entityPathMarker.Length)]}{parts[0]}/";
|
||||
}
|
||||
}
|
||||
|
||||
return trimmed.EndsWith('/') ? trimmed : $"{trimmed}/";
|
||||
}
|
||||
|
||||
private static async Task<List<string>> TryReadEntitySetsFromServiceRootAsync(HttpClient client, string baseUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
using var response = await client.GetAsync(baseUrl, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var xml = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
var document = XDocument.Parse(xml);
|
||||
|
||||
return document
|
||||
.Descendants(AppNs + "collection")
|
||||
.Select(x => x.Attribute("href")?.Value ?? string.Empty)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static async Task<List<string>> ReadEntitySetsFromMetadataAsync(HttpClient client, string baseUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
using var response = await client.GetAsync($"{baseUrl}$metadata", cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var xml = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
var document = XDocument.Parse(xml);
|
||||
|
||||
return document
|
||||
.Descendants(EdmNs + "EntitySet")
|
||||
.Select(x => x.Attribute("Name")?.Value ?? string.Empty)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static object? ConvertJsonValue(JsonElement element) => element.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => element.GetString(),
|
||||
JsonValueKind.Number => element.ToString(),
|
||||
JsonValueKind.True => true,
|
||||
JsonValueKind.False => false,
|
||||
JsonValueKind.Null => null,
|
||||
_ => element.ToString()
|
||||
};
|
||||
}
|
||||
@@ -9,6 +9,7 @@ public class SiteExportService : ISiteExportService
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||
private readonly IHanaQueryService _hanaService;
|
||||
private readonly ISapGatewayService _sapGatewayService;
|
||||
private readonly IExcelExportService _excelService;
|
||||
private readonly ISharePointUploadService _sharePointService;
|
||||
private readonly IRecordTransformationService _transformationService;
|
||||
@@ -17,6 +18,7 @@ public class SiteExportService : ISiteExportService
|
||||
public SiteExportService(
|
||||
IDbContextFactory<AppDbContext> dbFactory,
|
||||
IHanaQueryService hanaService,
|
||||
ISapGatewayService sapGatewayService,
|
||||
IExcelExportService excelService,
|
||||
ISharePointUploadService sharePointService,
|
||||
IRecordTransformationService transformationService,
|
||||
@@ -24,6 +26,7 @@ public class SiteExportService : ISiteExportService
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_hanaService = hanaService;
|
||||
_sapGatewayService = sapGatewayService;
|
||||
_excelService = excelService;
|
||||
_sharePointService = sharePointService;
|
||||
_transformationService = transformationService;
|
||||
@@ -32,9 +35,6 @@ public class SiteExportService : ISiteExportService
|
||||
|
||||
public async Task<SiteExportResult> ExportAsync(Site site, Action<string>? updateStatus = null)
|
||||
{
|
||||
if (site.HanaServer is null)
|
||||
throw new InvalidOperationException($"Standort '{site.Land}' hat keinen HANA-Server.");
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
var log = new ExportLog
|
||||
{
|
||||
@@ -49,22 +49,44 @@ public class SiteExportService : ISiteExportService
|
||||
using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var settings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings();
|
||||
var spConfig = await db.SharePointConfigs.FirstOrDefaultAsync();
|
||||
var exportServer = BuildEffectiveServer(site, settings);
|
||||
|
||||
updateStatus?.Invoke("HANA Abfrage...");
|
||||
var records = await Task.Run(() => _hanaService.GetSalesRecords(
|
||||
exportServer, site.Schema, site.TSC, site.Land, settings.DateFilter));
|
||||
|
||||
updateStatus?.Invoke("Transformationen anwenden...");
|
||||
var rules = await db.FieldTransformationRules
|
||||
.Where(r => r.IsActive && r.SourceSystem == (string.IsNullOrWhiteSpace(site.SourceSystem) ? "SAP" : site.SourceSystem))
|
||||
.OrderBy(r => r.SortOrder)
|
||||
.ToListAsync();
|
||||
_transformationService.Apply(records, rules);
|
||||
|
||||
updateStatus?.Invoke("Excel erstellen...");
|
||||
var outputDir = Path.Combine(AppContext.BaseDirectory, "output");
|
||||
var filePath = _excelService.CreateExcelFile(outputDir, site.TSC, DateTime.UtcNow.Date, records);
|
||||
var sourceSystem = NormalizeSourceSystem(site.SourceSystem);
|
||||
var records = new List<SalesRecord>();
|
||||
string filePath;
|
||||
|
||||
if (sourceSystem == "SAP")
|
||||
{
|
||||
var credentials = ResolveCredentials(site, settings, sourceSystem);
|
||||
if (string.IsNullOrWhiteSpace(site.SapServiceUrl))
|
||||
throw new InvalidOperationException($"Standort '{site.Land}' hat keine SAP Service URL.");
|
||||
if (string.IsNullOrWhiteSpace(site.SapEntitySet))
|
||||
throw new InvalidOperationException($"Standort '{site.Land}' hat kein SAP Entity Set ausgewählt.");
|
||||
|
||||
updateStatus?.Invoke("SAP Gateway Abfrage...");
|
||||
var rows = await _sapGatewayService.GetEntityRowsAsync(site.SapServiceUrl, site.SapEntitySet, credentials.Username, credentials.Password);
|
||||
updateStatus?.Invoke("Excel erstellen...");
|
||||
filePath = _excelService.CreateGenericExcelFile(outputDir, $"SAP_{site.TSC}_{site.SapEntitySet}", DateTime.UtcNow.Date, site.SapEntitySet, rows);
|
||||
log.RowCount = rows.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
var exportServer = BuildEffectiveServer(site, settings, sourceSystem);
|
||||
updateStatus?.Invoke("HANA Abfrage...");
|
||||
records = await Task.Run(() => _hanaService.GetSalesRecords(
|
||||
exportServer, site.Schema, site.TSC, site.Land, settings.DateFilter));
|
||||
|
||||
updateStatus?.Invoke("Transformationen anwenden...");
|
||||
var rules = await db.FieldTransformationRules
|
||||
.Where(r => r.IsActive && r.SourceSystem == sourceSystem)
|
||||
.OrderBy(r => r.SortOrder)
|
||||
.ToListAsync();
|
||||
_transformationService.Apply(records, rules);
|
||||
|
||||
updateStatus?.Invoke("Excel erstellen...");
|
||||
filePath = _excelService.CreateExcelFile(outputDir, site.TSC, DateTime.UtcNow.Date, records);
|
||||
log.RowCount = records.Count;
|
||||
}
|
||||
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
|
||||
if (spConfig is not null &&
|
||||
@@ -80,12 +102,11 @@ public class SiteExportService : ISiteExportService
|
||||
|
||||
sw.Stop();
|
||||
log.Status = "OK";
|
||||
log.RowCount = records.Count;
|
||||
log.FileName = fileName;
|
||||
log.DurationSeconds = sw.Elapsed.TotalSeconds;
|
||||
|
||||
_logger.LogInformation("Export OK: {Land} ({TSC}) - {Rows} Zeilen in {Duration:F1}s",
|
||||
site.Land, site.TSC, records.Count, sw.Elapsed.TotalSeconds);
|
||||
site.Land, site.TSC, log.RowCount, sw.Elapsed.TotalSeconds);
|
||||
|
||||
return new SiteExportResult
|
||||
{
|
||||
@@ -113,14 +134,12 @@ public class SiteExportService : ISiteExportService
|
||||
}
|
||||
}
|
||||
|
||||
private static HanaServer BuildEffectiveServer(Site site, ExportSettings settings)
|
||||
private static HanaServer BuildEffectiveServer(Site site, ExportSettings settings, string sourceSystem)
|
||||
{
|
||||
if (site.HanaServer is null)
|
||||
throw new InvalidOperationException($"Standort '{site.Land}' hat keinen HANA-Server.");
|
||||
|
||||
var sourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) ? "SAP" : site.SourceSystem.Trim().ToUpperInvariant();
|
||||
var inheritedUsername = GetCentralUsername(sourceSystem, settings);
|
||||
var inheritedPassword = GetCentralPassword(sourceSystem, settings);
|
||||
var credentials = ResolveCredentials(site, settings, sourceSystem);
|
||||
|
||||
return new HanaServer
|
||||
{
|
||||
@@ -128,8 +147,8 @@ public class SiteExportService : ISiteExportService
|
||||
Name = site.HanaServer.Name,
|
||||
Host = site.HanaServer.Host,
|
||||
Port = site.HanaServer.Port,
|
||||
Username = FirstNonEmpty(site.UsernameOverride, inheritedUsername, site.HanaServer.Username),
|
||||
Password = FirstNonEmpty(site.PasswordOverride, inheritedPassword, site.HanaServer.Password),
|
||||
Username = FirstNonEmpty(credentials.Username, site.HanaServer.Username),
|
||||
Password = FirstNonEmpty(credentials.Password, site.HanaServer.Password),
|
||||
DatabaseName = site.HanaServer.DatabaseName,
|
||||
UseSsl = site.HanaServer.UseSsl,
|
||||
ValidateCertificate = site.HanaServer.ValidateCertificate,
|
||||
@@ -137,6 +156,10 @@ public class SiteExportService : ISiteExportService
|
||||
};
|
||||
}
|
||||
|
||||
private static (string Username, string Password) ResolveCredentials(Site site, ExportSettings settings, string sourceSystem)
|
||||
=> (FirstNonEmpty(site.UsernameOverride, GetCentralUsername(sourceSystem, settings)),
|
||||
FirstNonEmpty(site.PasswordOverride, GetCentralPassword(sourceSystem, settings)));
|
||||
|
||||
private static string GetCentralUsername(string sourceSystem, ExportSettings settings) => sourceSystem switch
|
||||
{
|
||||
"BI1" => settings.Bi1Username,
|
||||
@@ -151,6 +174,9 @@ public class SiteExportService : ISiteExportService
|
||||
_ => settings.SapPassword
|
||||
};
|
||||
|
||||
private static string NormalizeSourceSystem(string? sourceSystem)
|
||||
=> string.IsNullOrWhiteSpace(sourceSystem) ? "SAP" : sourceSystem.Trim().ToUpperInvariant();
|
||||
|
||||
private static string FirstNonEmpty(params string[] values)
|
||||
{
|
||||
foreach (var value in values)
|
||||
|
||||
Reference in New Issue
Block a user