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 ISharePointUploadService SpService
|
||||||
@inject TimerBackgroundService TimerService
|
@inject TimerBackgroundService TimerService
|
||||||
@inject IHanaQueryService HanaService
|
@inject IHanaQueryService HanaService
|
||||||
|
@inject ISapGatewayService SapGatewayService
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
<PageTitle>Settings</PageTitle>
|
<PageTitle>Settings</PageTitle>
|
||||||
@@ -240,6 +241,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task TestCentralCredentials(string sourceSystem)
|
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))
|
if (!_testingSystems.Add(sourceSystem))
|
||||||
return;
|
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
|
private string GetCentralUsername(string sourceSystem) => sourceSystem switch
|
||||||
{
|
{
|
||||||
"BI1" => _exportSettings.Bi1Username,
|
"BI1" => _exportSettings.Bi1Username,
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
@page "/standorte"
|
@page "/standorte"
|
||||||
@using Microsoft.EntityFrameworkCore
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
@using System.Text.Json
|
||||||
@using TrafagSalesExporter.Data
|
@using TrafagSalesExporter.Data
|
||||||
|
@using TrafagSalesExporter.Models
|
||||||
@using TrafagSalesExporter.Services
|
@using TrafagSalesExporter.Services
|
||||||
@inject IDbContextFactory<AppDbContext> DbFactory
|
@inject IDbContextFactory<AppDbContext> DbFactory
|
||||||
@inject IHanaQueryService HanaService
|
@inject IHanaQueryService HanaService
|
||||||
|
@inject ISapGatewayService SapGatewayService
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@inject IDialogService DialogService
|
@inject IDialogService DialogService
|
||||||
|
|
||||||
@@ -71,7 +74,7 @@
|
|||||||
<MudTh>TSC</MudTh>
|
<MudTh>TSC</MudTh>
|
||||||
<MudTh>Schema</MudTh>
|
<MudTh>Schema</MudTh>
|
||||||
<MudTh>Quellsystem</MudTh>
|
<MudTh>Quellsystem</MudTh>
|
||||||
<MudTh>Host</MudTh>
|
<MudTh>Quelle</MudTh>
|
||||||
<MudTh>Aktiv</MudTh>
|
<MudTh>Aktiv</MudTh>
|
||||||
<MudTh>Aktionen</MudTh>
|
<MudTh>Aktionen</MudTh>
|
||||||
</HeaderContent>
|
</HeaderContent>
|
||||||
@@ -80,7 +83,7 @@
|
|||||||
<MudTd>@context.TSC</MudTd>
|
<MudTd>@context.TSC</MudTd>
|
||||||
<MudTd>@context.Schema</MudTd>
|
<MudTd>@context.Schema</MudTd>
|
||||||
<MudTd>@context.SourceSystem</MudTd>
|
<MudTd>@context.SourceSystem</MudTd>
|
||||||
<MudTd>@GetServerNode(context.HanaServer)</MudTd>
|
<MudTd>@GetConnectionTarget(context)</MudTd>
|
||||||
<MudTd>
|
<MudTd>
|
||||||
@if (context.IsActive)
|
@if (context.IsActive)
|
||||||
{
|
{
|
||||||
@@ -122,8 +125,8 @@
|
|||||||
HelperText="Optional, z.B. sslCryptoProvider=openssl;communicationTimeout=0" />
|
HelperText="Optional, z.B. sslCryptoProvider=openssl;communicationTimeout=0" />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<MudButton OnClick="() => _serverDialogVisible = false">Abbrechen</MudButton>
|
<MudButton OnClick="CloseServerDialog">Abbrechen</MudButton>
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveServer">Speichern</MudButton>
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveServer" Disabled="_savingServer">Speichern</MudButton>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</MudDialog>
|
</MudDialog>
|
||||||
|
|
||||||
@@ -138,7 +141,7 @@
|
|||||||
<MudSelect @bind-Value="_editingSite.SourceSystem" Label="Quellsystem" Required>
|
<MudSelect @bind-Value="_editingSite.SourceSystem" Label="Quellsystem" Required>
|
||||||
@foreach (var system in _sourceSystems)
|
@foreach (var system in _sourceSystems)
|
||||||
{
|
{
|
||||||
<MudSelectItem Value="system">@system</MudSelectItem>
|
<MudSelectItem Value="@system">@system</MudSelectItem>
|
||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
<MudTextField @bind-Value="_editingSite.UsernameOverride" Label="Username Override"
|
<MudTextField @bind-Value="_editingSite.UsernameOverride" Label="Username Override"
|
||||||
@@ -149,29 +152,67 @@
|
|||||||
|
|
||||||
<MudDivider Class="my-4" />
|
<MudDivider Class="my-4" />
|
||||||
|
|
||||||
<MudText Typo="Typo.h6" Class="mb-2">HANA-Verbindung</MudText>
|
@if (IsSapSite())
|
||||||
<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.
|
<MudText Typo="Typo.h6" Class="mb-2">SAP Gateway</MudText>
|
||||||
</MudAlert>
|
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Class="mb-3">
|
||||||
<MudTextField @bind-Value="_editingSiteServer.Name" Label="Verbindungsname" Required
|
Die Service-URL zeigt auf den OData-Service. Die verfügbaren Entity Sets werden nur per Knopfdruck aktualisiert und lokal zwischengespeichert.
|
||||||
HelperText="Interner Anzeigename für diesen Standort" />
|
</MudAlert>
|
||||||
<MudTextField @bind-Value="_editingSiteServer.Host" Label="Host oder ServerNode" Required
|
<MudTextField @bind-Value="_editingSite.SapServiceUrl" Label="SAP Service URL" Required
|
||||||
HelperText="z.B. hana01 oder hana01:30015 oder derselbe HanaServer-Wert wie in Power BI" />
|
HelperText="z.B. http://server:8000/sap/opu/odata/sap/ZPOWERBI_EINKAUF_SRV/" />
|
||||||
<MudNumericField @bind-Value="_editingSiteServer.Port" Label="Port"
|
<MudStack Row Spacing="2" Class="mb-3">
|
||||||
HelperText="Wird ignoriert, wenn im Host bereits ein Port enthalten ist" />
|
<MudButton Variant="Variant.Outlined" Color="Color.Info" OnClick="RefreshSapEntitySets"
|
||||||
<MudTextField @bind-Value="_editingSiteServer.Username" Label="Username" />
|
StartIcon="@Icons.Material.Filled.Refresh" Disabled="_refreshingSapEntitySets">
|
||||||
<MudTextField @bind-Value="_editingSiteServer.Password" Label="Password" InputType="InputType.Password" />
|
@if (_refreshingSapEntitySets)
|
||||||
<MudTextField @bind-Value="_editingSiteServer.DatabaseName" Label="Database Name (MDC)"
|
{
|
||||||
HelperText="Nur bei Multi-Tenant Setup angeben, sonst leer lassen" />
|
<MudProgressCircular Size="Size.Small" Indeterminate Class="mr-2" />
|
||||||
<MudSwitch @bind-Value="_editingSiteServer.UseSsl" Label="SSL/TLS verwenden (encrypt=true)" Color="Color.Primary" />
|
@("Lade...")
|
||||||
<MudSwitch @bind-Value="_editingSiteServer.ValidateCertificate" Label="SSL-Zertifikat validieren" Color="Color.Primary"
|
}
|
||||||
Disabled="!_editingSiteServer.UseSsl" />
|
else
|
||||||
<MudTextField @bind-Value="_editingSiteServer.AdditionalParams" Label="Zusätzliche Parameter"
|
{
|
||||||
HelperText="Optional, z.B. sslCryptoProvider=openssl;communicationTimeout=0" />
|
@("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>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<MudButton OnClick="() => _siteDialogVisible = false">Abbrechen</MudButton>
|
<MudButton OnClick="CloseSiteDialog" Disabled="_savingSite">Abbrechen</MudButton>
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveSite">Speichern</MudButton>
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveSite" Disabled="_savingSite || _refreshingSapEntitySets">Speichern</MudButton>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</MudDialog>
|
</MudDialog>
|
||||||
|
|
||||||
@@ -180,11 +221,15 @@
|
|||||||
private readonly Dictionary<int, ConnectionTestResult> _connectionStatus = new();
|
private readonly Dictionary<int, ConnectionTestResult> _connectionStatus = new();
|
||||||
private List<HanaServer> _servers = new();
|
private List<HanaServer> _servers = new();
|
||||||
private List<Site> _sites = new();
|
private List<Site> _sites = new();
|
||||||
|
private List<string> _sapEntitySetsCache = [];
|
||||||
private HanaServer _editingServer = new();
|
private HanaServer _editingServer = new();
|
||||||
private Site _editingSite = new();
|
private Site _editingSite = new();
|
||||||
private HanaServer _editingSiteServer = new();
|
private HanaServer _editingSiteServer = new();
|
||||||
private bool _serverDialogVisible;
|
private bool _serverDialogVisible;
|
||||||
private bool _siteDialogVisible;
|
private bool _siteDialogVisible;
|
||||||
|
private bool _refreshingSapEntitySets;
|
||||||
|
private bool _savingServer;
|
||||||
|
private bool _savingSite;
|
||||||
private readonly DialogOptions _dialogOptions = new() { MaxWidth = MaxWidth.Small, FullWidth = true };
|
private readonly DialogOptions _dialogOptions = new() { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
@@ -213,6 +258,12 @@
|
|||||||
|
|
||||||
private async Task SaveServer()
|
private async Task SaveServer()
|
||||||
{
|
{
|
||||||
|
if (_savingServer)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_savingServer = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
using var db = await DbFactory.CreateDbContextAsync();
|
||||||
if (_editingServer.Id == 0)
|
if (_editingServer.Id == 0)
|
||||||
{
|
{
|
||||||
@@ -239,6 +290,11 @@
|
|||||||
_serverDialogVisible = false;
|
_serverDialogVisible = false;
|
||||||
await LoadDataAsync();
|
await LoadDataAsync();
|
||||||
Snackbar.Add("Server gespeichert", Severity.Success);
|
Snackbar.Add("Server gespeichert", Severity.Success);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_savingServer = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DeleteServer(HanaServer server)
|
private async Task DeleteServer(HanaServer server)
|
||||||
@@ -294,6 +350,7 @@
|
|||||||
SourceSystem = "SAP",
|
SourceSystem = "SAP",
|
||||||
HanaServerId = 0
|
HanaServerId = 0
|
||||||
};
|
};
|
||||||
|
_sapEntitySetsCache = [];
|
||||||
_editingSiteServer = CreateDefaultSiteServer();
|
_editingSiteServer = CreateDefaultSiteServer();
|
||||||
_siteDialogVisible = true;
|
_siteDialogVisible = true;
|
||||||
}
|
}
|
||||||
@@ -310,8 +367,13 @@
|
|||||||
SourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) ? "SAP" : site.SourceSystem,
|
SourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) ? "SAP" : site.SourceSystem,
|
||||||
UsernameOverride = site.UsernameOverride,
|
UsernameOverride = site.UsernameOverride,
|
||||||
PasswordOverride = site.PasswordOverride,
|
PasswordOverride = site.PasswordOverride,
|
||||||
|
SapServiceUrl = site.SapServiceUrl,
|
||||||
|
SapEntitySet = site.SapEntitySet,
|
||||||
|
SapEntitySetsCache = site.SapEntitySetsCache,
|
||||||
|
SapEntitySetsRefreshedAtUtc = site.SapEntitySetsRefreshedAtUtc,
|
||||||
IsActive = site.IsActive
|
IsActive = site.IsActive
|
||||||
};
|
};
|
||||||
|
_sapEntitySetsCache = ParseSapEntitySets(site.SapEntitySetsCache);
|
||||||
_editingSiteServer = site.HanaServer is null
|
_editingSiteServer = site.HanaServer is null
|
||||||
? CreateDefaultSiteServer(site)
|
? CreateDefaultSiteServer(site)
|
||||||
: CloneServer(site.HanaServer);
|
: CloneServer(site.HanaServer);
|
||||||
@@ -320,34 +382,54 @@
|
|||||||
|
|
||||||
private async Task SaveSite()
|
private async Task SaveSite()
|
||||||
{
|
{
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
if (_savingSite)
|
||||||
var serverId = await SaveOrCreateSiteServerAsync(db);
|
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;
|
_editingSite.HanaServerId = serverId;
|
||||||
db.Sites.Add(_editingSite);
|
_editingSite.SapEntitySetsCache = SerializeSapEntitySets(_sapEntitySetsCache);
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
if (_editingSite.Id == 0)
|
||||||
_siteDialogVisible = false;
|
{
|
||||||
await LoadDataAsync();
|
db.Sites.Add(_editingSite);
|
||||||
Snackbar.Add("Standort gespeichert", Severity.Success);
|
}
|
||||||
|
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)
|
private async Task DeleteSite(Site site)
|
||||||
@@ -379,6 +461,15 @@
|
|||||||
return server.Host.Contains(':', StringComparison.Ordinal) ? server.Host : $"{server.Host}:{server.Port}";
|
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)
|
private HanaServer CreateDefaultSiteServer(Site? site = null)
|
||||||
{
|
{
|
||||||
var label = !string.IsNullOrWhiteSpace(site?.Land) ? site!.Land : site?.TSC;
|
var label = !string.IsNullOrWhiteSpace(site?.Land) ? site!.Land : site?.TSC;
|
||||||
@@ -416,6 +507,8 @@
|
|||||||
: _editingSiteServer.Name.Trim();
|
: _editingSiteServer.Name.Trim();
|
||||||
_editingSite.UsernameOverride = _editingSite.UsernameOverride.Trim();
|
_editingSite.UsernameOverride = _editingSite.UsernameOverride.Trim();
|
||||||
_editingSite.PasswordOverride = _editingSite.PasswordOverride.Trim();
|
_editingSite.PasswordOverride = _editingSite.PasswordOverride.Trim();
|
||||||
|
_editingSite.SapServiceUrl = _editingSite.SapServiceUrl.Trim();
|
||||||
|
_editingSite.SapEntitySet = _editingSite.SapEntitySet.Trim();
|
||||||
_editingSiteServer.Host = _editingSiteServer.Host.Trim();
|
_editingSiteServer.Host = _editingSiteServer.Host.Trim();
|
||||||
_editingSiteServer.Username = _editingSiteServer.Username.Trim();
|
_editingSiteServer.Username = _editingSiteServer.Username.Trim();
|
||||||
_editingSiteServer.DatabaseName = _editingSiteServer.DatabaseName.Trim();
|
_editingSiteServer.DatabaseName = _editingSiteServer.DatabaseName.Trim();
|
||||||
@@ -461,4 +554,82 @@
|
|||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
return existingServer.Id;
|
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 Id { get; set; }
|
||||||
|
|
||||||
public int HanaServerId { get; set; }
|
public int? HanaServerId { get; set; }
|
||||||
|
|
||||||
[ForeignKey(nameof(HanaServerId))]
|
[ForeignKey(nameof(HanaServerId))]
|
||||||
public HanaServer? HanaServer { get; set; }
|
public HanaServer? HanaServer { get; set; }
|
||||||
@@ -28,5 +28,13 @@ public class Site
|
|||||||
|
|
||||||
public string PasswordOverride { get; set; } = string.Empty;
|
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;
|
public bool IsActive { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ builder.Services.AddDbContextFactory<AppDbContext>(options =>
|
|||||||
builder.Services.AddSingleton<IHanaQueryService, HanaQueryService>();
|
builder.Services.AddSingleton<IHanaQueryService, HanaQueryService>();
|
||||||
builder.Services.AddSingleton<IExcelExportService, ExcelExportService>();
|
builder.Services.AddSingleton<IExcelExportService, ExcelExportService>();
|
||||||
builder.Services.AddSingleton<ISharePointUploadService, SharePointUploadService>();
|
builder.Services.AddSingleton<ISharePointUploadService, SharePointUploadService>();
|
||||||
|
builder.Services.AddSingleton<ISapGatewayService, SapGatewayService>();
|
||||||
builder.Services.AddSingleton<ITransformationStrategy, CopyTransformationStrategy>();
|
builder.Services.AddSingleton<ITransformationStrategy, CopyTransformationStrategy>();
|
||||||
builder.Services.AddSingleton<ITransformationStrategy, UppercaseTransformationStrategy>();
|
builder.Services.AddSingleton<ITransformationStrategy, UppercaseTransformationStrategy>();
|
||||||
builder.Services.AddSingleton<ITransformationStrategy, LowercaseTransformationStrategy>();
|
builder.Services.AddSingleton<ITransformationStrategy, LowercaseTransformationStrategy>();
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public class DatabaseInitializationService : IDatabaseInitializationService
|
|||||||
|
|
||||||
private static void EnsureSchema(AppDbContext db)
|
private static void EnsureSchema(AppDbContext db)
|
||||||
{
|
{
|
||||||
|
EnsureSitesTableSupportsOptionalHanaServer(db);
|
||||||
AddColumnIfMissing(db, "HanaServers", "DatabaseName", "TEXT NOT NULL DEFAULT ''");
|
AddColumnIfMissing(db, "HanaServers", "DatabaseName", "TEXT NOT NULL DEFAULT ''");
|
||||||
AddColumnIfMissing(db, "HanaServers", "UseSsl", "INTEGER NOT NULL DEFAULT 0");
|
AddColumnIfMissing(db, "HanaServers", "UseSsl", "INTEGER NOT NULL DEFAULT 0");
|
||||||
AddColumnIfMissing(db, "HanaServers", "ValidateCertificate", "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", "SourceSystem", "TEXT NOT NULL DEFAULT 'SAP'");
|
||||||
AddColumnIfMissing(db, "Sites", "UsernameOverride", "TEXT NOT NULL DEFAULT ''");
|
AddColumnIfMissing(db, "Sites", "UsernameOverride", "TEXT NOT NULL DEFAULT ''");
|
||||||
AddColumnIfMissing(db, "Sites", "PasswordOverride", "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", "SapUsername", "TEXT NOT NULL DEFAULT ''");
|
||||||
AddColumnIfMissing(db, "ExportSettings", "SapPassword", "TEXT NOT NULL DEFAULT ''");
|
AddColumnIfMissing(db, "ExportSettings", "SapPassword", "TEXT NOT NULL DEFAULT ''");
|
||||||
AddColumnIfMissing(db, "ExportSettings", "Bi1Username", "TEXT NOT NULL DEFAULT ''");
|
AddColumnIfMissing(db, "ExportSettings", "Bi1Username", "TEXT NOT NULL DEFAULT ''");
|
||||||
@@ -40,6 +45,104 @@ public class DatabaseInitializationService : IDatabaseInitializationService
|
|||||||
EnsureTransformationTable(db);
|
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)
|
private static void AddColumnIfMissing(AppDbContext db, string table, string column, string type)
|
||||||
{
|
{
|
||||||
var conn = db.Database.GetDbConnection();
|
var conn = db.Database.GetDbConnection();
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ public class ExcelExportService : IExcelExportService
|
|||||||
return fullPath;
|
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)
|
private static void WriteWorkbook(string fullPath, List<SalesRecord> records)
|
||||||
{
|
{
|
||||||
using var workbook = new XLWorkbook();
|
using var workbook = new XLWorkbook();
|
||||||
@@ -99,4 +109,35 @@ public class ExcelExportService : IExcelExportService
|
|||||||
ws.Columns().AdjustToContents();
|
ws.Columns().AdjustToContents();
|
||||||
workbook.SaveAs(fullPath);
|
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)
|
private async Task<SiteExportResult?> ExportSiteAsync(Site site)
|
||||||
{
|
{
|
||||||
if (site.HanaServer is null) return null;
|
|
||||||
SiteExportResult? result = null;
|
SiteExportResult? result = null;
|
||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ public interface IExcelExportService
|
|||||||
{
|
{
|
||||||
string CreateExcelFile(string outputDirectory, string tsc, DateTime fileDate, List<SalesRecord> records);
|
string CreateExcelFile(string outputDirectory, string tsc, DateTime fileDate, List<SalesRecord> records);
|
||||||
string CreateConsolidatedExcelFile(string outputDirectory, 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 IDbContextFactory<AppDbContext> _dbFactory;
|
||||||
private readonly IHanaQueryService _hanaService;
|
private readonly IHanaQueryService _hanaService;
|
||||||
|
private readonly ISapGatewayService _sapGatewayService;
|
||||||
private readonly IExcelExportService _excelService;
|
private readonly IExcelExportService _excelService;
|
||||||
private readonly ISharePointUploadService _sharePointService;
|
private readonly ISharePointUploadService _sharePointService;
|
||||||
private readonly IRecordTransformationService _transformationService;
|
private readonly IRecordTransformationService _transformationService;
|
||||||
@@ -17,6 +18,7 @@ public class SiteExportService : ISiteExportService
|
|||||||
public SiteExportService(
|
public SiteExportService(
|
||||||
IDbContextFactory<AppDbContext> dbFactory,
|
IDbContextFactory<AppDbContext> dbFactory,
|
||||||
IHanaQueryService hanaService,
|
IHanaQueryService hanaService,
|
||||||
|
ISapGatewayService sapGatewayService,
|
||||||
IExcelExportService excelService,
|
IExcelExportService excelService,
|
||||||
ISharePointUploadService sharePointService,
|
ISharePointUploadService sharePointService,
|
||||||
IRecordTransformationService transformationService,
|
IRecordTransformationService transformationService,
|
||||||
@@ -24,6 +26,7 @@ public class SiteExportService : ISiteExportService
|
|||||||
{
|
{
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
_hanaService = hanaService;
|
_hanaService = hanaService;
|
||||||
|
_sapGatewayService = sapGatewayService;
|
||||||
_excelService = excelService;
|
_excelService = excelService;
|
||||||
_sharePointService = sharePointService;
|
_sharePointService = sharePointService;
|
||||||
_transformationService = transformationService;
|
_transformationService = transformationService;
|
||||||
@@ -32,9 +35,6 @@ public class SiteExportService : ISiteExportService
|
|||||||
|
|
||||||
public async Task<SiteExportResult> ExportAsync(Site site, Action<string>? updateStatus = null)
|
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 sw = Stopwatch.StartNew();
|
||||||
var log = new ExportLog
|
var log = new ExportLog
|
||||||
{
|
{
|
||||||
@@ -49,22 +49,44 @@ public class SiteExportService : ISiteExportService
|
|||||||
using var db = await _dbFactory.CreateDbContextAsync();
|
using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
var settings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings();
|
var settings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings();
|
||||||
var spConfig = await db.SharePointConfigs.FirstOrDefaultAsync();
|
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 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);
|
var fileName = Path.GetFileName(filePath);
|
||||||
|
|
||||||
if (spConfig is not null &&
|
if (spConfig is not null &&
|
||||||
@@ -80,12 +102,11 @@ public class SiteExportService : ISiteExportService
|
|||||||
|
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
log.Status = "OK";
|
log.Status = "OK";
|
||||||
log.RowCount = records.Count;
|
|
||||||
log.FileName = fileName;
|
log.FileName = fileName;
|
||||||
log.DurationSeconds = sw.Elapsed.TotalSeconds;
|
log.DurationSeconds = sw.Elapsed.TotalSeconds;
|
||||||
|
|
||||||
_logger.LogInformation("Export OK: {Land} ({TSC}) - {Rows} Zeilen in {Duration:F1}s",
|
_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
|
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)
|
if (site.HanaServer is null)
|
||||||
throw new InvalidOperationException($"Standort '{site.Land}' hat keinen HANA-Server.");
|
throw new InvalidOperationException($"Standort '{site.Land}' hat keinen HANA-Server.");
|
||||||
|
|
||||||
var sourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) ? "SAP" : site.SourceSystem.Trim().ToUpperInvariant();
|
var credentials = ResolveCredentials(site, settings, sourceSystem);
|
||||||
var inheritedUsername = GetCentralUsername(sourceSystem, settings);
|
|
||||||
var inheritedPassword = GetCentralPassword(sourceSystem, settings);
|
|
||||||
|
|
||||||
return new HanaServer
|
return new HanaServer
|
||||||
{
|
{
|
||||||
@@ -128,8 +147,8 @@ public class SiteExportService : ISiteExportService
|
|||||||
Name = site.HanaServer.Name,
|
Name = site.HanaServer.Name,
|
||||||
Host = site.HanaServer.Host,
|
Host = site.HanaServer.Host,
|
||||||
Port = site.HanaServer.Port,
|
Port = site.HanaServer.Port,
|
||||||
Username = FirstNonEmpty(site.UsernameOverride, inheritedUsername, site.HanaServer.Username),
|
Username = FirstNonEmpty(credentials.Username, site.HanaServer.Username),
|
||||||
Password = FirstNonEmpty(site.PasswordOverride, inheritedPassword, site.HanaServer.Password),
|
Password = FirstNonEmpty(credentials.Password, site.HanaServer.Password),
|
||||||
DatabaseName = site.HanaServer.DatabaseName,
|
DatabaseName = site.HanaServer.DatabaseName,
|
||||||
UseSsl = site.HanaServer.UseSsl,
|
UseSsl = site.HanaServer.UseSsl,
|
||||||
ValidateCertificate = site.HanaServer.ValidateCertificate,
|
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
|
private static string GetCentralUsername(string sourceSystem, ExportSettings settings) => sourceSystem switch
|
||||||
{
|
{
|
||||||
"BI1" => settings.Bi1Username,
|
"BI1" => settings.Bi1Username,
|
||||||
@@ -151,6 +174,9 @@ public class SiteExportService : ISiteExportService
|
|||||||
_ => settings.SapPassword
|
_ => settings.SapPassword
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static string NormalizeSourceSystem(string? sourceSystem)
|
||||||
|
=> string.IsNullOrWhiteSpace(sourceSystem) ? "SAP" : sourceSystem.Trim().ToUpperInvariant();
|
||||||
|
|
||||||
private static string FirstNonEmpty(params string[] values)
|
private static string FirstNonEmpty(params string[] values)
|
||||||
{
|
{
|
||||||
foreach (var value in values)
|
foreach (var value in values)
|
||||||
|
|||||||
Reference in New Issue
Block a user