refactoring

This commit is contained in:
2026-04-17 10:29:41 +02:00
parent 83a400a90e
commit bec0410ef4
17 changed files with 1752 additions and 432 deletions
@@ -173,6 +173,7 @@
using var db = await DbFactory.CreateDbContextAsync();
var sites = await db.Sites.Include(s => s.HanaServer).Where(s => s.IsActive).ToListAsync();
var sourceSystems = await db.SourceSystemDefinitions.AsNoTracking().ToListAsync();
var logs = await db.ExportLogs
.GroupBy(l => l.SiteId)
.Select(g => g.OrderByDescending(l => l.Timestamp).First())
@@ -190,14 +191,15 @@
{
var log = logs.FirstOrDefault(l => l.SiteId == s.Id);
latestAppLogsBySite.TryGetValue(s.Id, out var appLog);
var sourceSystem = sourceSystems.FirstOrDefault(x => string.Equals(x.Code, s.SourceSystem, StringComparison.OrdinalIgnoreCase));
return new DashboardRow
{
SiteId = s.Id,
Land = s.Land,
TSC = s.TSC,
Schema = s.Schema,
ServerName = string.Equals(s.SourceSystem, "SAP", StringComparison.OrdinalIgnoreCase)
? (string.IsNullOrWhiteSpace(s.SapServiceUrl) ? "SAP Gateway" : s.SapServiceUrl)
ServerName = string.Equals(sourceSystem?.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase)
? ResolveDashboardSapServiceUrl(s, sourceSystems)
: s.HanaServer?.Name ?? "",
LastStatus = log?.Status ?? "",
RowCount = log?.RowCount ?? 0,
@@ -319,6 +321,15 @@
OpenFile(row.FilePath);
}
private static string ResolveDashboardSapServiceUrl(Site site, List<SourceSystemDefinition> sourceSystems)
{
if (!string.IsNullOrWhiteSpace(site.SapServiceUrl))
return site.SapServiceUrl;
var sourceSystem = sourceSystems.FirstOrDefault(x => string.Equals(x.Code, site.SourceSystem, StringComparison.OrdinalIgnoreCase));
return string.IsNullOrWhiteSpace(sourceSystem?.CentralServiceUrl) ? "SAP Gateway" : sourceSystem.CentralServiceUrl;
}
private void OpenFile(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
@@ -1,4 +1,4 @@
@page "/settings"
@page "/settings"
@using Microsoft.EntityFrameworkCore
@using TrafagSalesExporter.Data
@using TrafagSalesExporter.Models
@@ -23,7 +23,7 @@
<MudItem xs="12" md="6">
<MudCheckBox @bind-Value="_includeSecretsInExport" Label="Mit Secrets exportieren" />
<MudText Typo="Typo.caption">
Wenn deaktiviert, bleiben Passwörter und Secrets beim Export leer. Beim Import ohne Secrets werden bestehende Secrets auf dem Zielsystem beibehalten.
Wenn deaktiviert, bleiben Passwörter und Secrets beim Export leer. Beim Import ohne Secrets werden bestehende Secrets auf dem Zielsystem beibehalten.
</MudText>
</MudItem>
<MudItem xs="12" md="6">
@@ -98,74 +98,102 @@
</MudGrid>
</MudPaper>
<MudText Typo="Typo.h5" Class="mb-2">Zentrale Quellsystem-Zugangsdaten</MudText>
<MudText Typo="Typo.h5" Class="mb-2">Quellsysteme</MudText>
<MudPaper Class="pa-4 mb-6" Elevation="1">
<MudGrid>
<MudItem xs="12">
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined">
Diese Zugangsdaten werden pro Quellsystem als Standard verwendet. Ein Standort kann sie bei Bedarf mit eigenen Overrides überschreiben.
Diese Zugangsdaten werden pro Quellsystem als Standard verwendet. Ein Standort kann sie bei Bedarf mit eigenen Overrides überschreiben.
</MudAlert>
</MudItem>
<MudItem xs="12" md="4">
<MudText Typo="Typo.h6" Class="mb-2">SAP</MudText>
<MudTextField @bind-Value="_exportSettings.SapUsername" Label="SAP Username" />
<MudTextField @bind-Value="_exportSettings.SapPassword" Label="SAP Password" InputType="InputType.Password" />
<MudButton Variant="Variant.Outlined" Color="Color.Info" OnClick='@(() => TestCentralCredentials("SAP"))'
StartIcon="@Icons.Material.Filled.NetworkCheck" Disabled='@_testingSystems.Contains("SAP")' Class="mt-2">
@if (_testingSystems.Contains("SAP"))
{
<MudProgressCircular Size="Size.Small" Indeterminate Class="mr-2" />
@("Teste...")
}
else
{
@("SAP testen")
}
</MudButton>
</MudItem>
<MudItem xs="12" md="4">
<MudText Typo="Typo.h6" Class="mb-2">BI1</MudText>
<MudTextField @bind-Value="_exportSettings.Bi1Username" Label="BI1 Username" />
<MudTextField @bind-Value="_exportSettings.Bi1Password" Label="BI1 Password" InputType="InputType.Password" />
<MudButton Variant="Variant.Outlined" Color="Color.Info" OnClick='@(() => TestCentralCredentials("BI1"))'
StartIcon="@Icons.Material.Filled.NetworkCheck" Disabled='@_testingSystems.Contains("BI1")' Class="mt-2">
@if (_testingSystems.Contains("BI1"))
{
<MudProgressCircular Size="Size.Small" Indeterminate Class="mr-2" />
@("Teste...")
}
else
{
@("BI1 testen")
}
</MudButton>
</MudItem>
<MudItem xs="12" md="4">
<MudText Typo="Typo.h6" Class="mb-2">SAGE</MudText>
<MudTextField @bind-Value="_exportSettings.SageUsername" Label="SAGE Username" />
<MudTextField @bind-Value="_exportSettings.SagePassword" Label="SAGE Password" InputType="InputType.Password" />
<MudButton Variant="Variant.Outlined" Color="Color.Info" OnClick='@(() => TestCentralCredentials("SAGE"))'
StartIcon="@Icons.Material.Filled.NetworkCheck" Disabled='@_testingSystems.Contains("SAGE")' Class="mt-2">
@if (_testingSystems.Contains("SAGE"))
{
<MudProgressCircular Size="Size.Small" Indeterminate Class="mr-2" />
@("Teste...")
}
else
{
@("SAGE testen")
}
<MudItem xs="12">
<MudButton Variant="Variant.Outlined" Color="Color.Primary" OnClick="AddSourceSystem"
StartIcon="@Icons.Material.Filled.Add" Class="mb-3">
Quellsystem hinzufuegen
</MudButton>
<MudTable Items="_sourceSystems" Dense Hover Striped Breakpoint="Breakpoint.Md">
<HeaderContent>
<MudTh>Code</MudTh>
<MudTh>Name</MudTh>
<MudTh>Anschlussart</MudTh>
<MudTh>Zentrale URL</MudTh>
<MudTh>User</MudTh>
<MudTh>Aktiv</MudTh>
<MudTh>Test</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Code</MudTd>
<MudTd>@context.DisplayName</MudTd>
<MudTd>@GetConnectionKindLabel(context.ConnectionKind)</MudTd>
<MudTd>@GetServiceUrlSummary(context)</MudTd>
<MudTd>@GetUsernameSummary(context)</MudTd>
<MudTd>
@if (context.IsActive)
{
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" />
}
else
{
<MudIcon Icon="@Icons.Material.Filled.Cancel" Color="Color.Default" Size="Size.Small" />
}
</MudTd>
<MudTd>
@if (!UsesManualImport(context))
{
<MudButton Variant="Variant.Outlined" Color="Color.Info" Size="Size.Small"
OnClick='@(() => TestCentralCredentials(context.Code))'
Disabled='@_testingSystems.Contains(context.Code)'>
@(_testingSystems.Contains(context.Code) ? "Teste..." : "Testen")
</MudButton>
}
</MudTd>
<MudTd>
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Primary" Size="Size.Small"
OnClick="() => EditSourceSystem(context)" />
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small"
OnClick="() => RemoveSourceSystem(context)" />
</MudTd>
</RowTemplate>
</MudTable>
</MudItem>
<MudItem xs="12">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveExportSettings"
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveSourceSystems"
StartIcon="@Icons.Material.Filled.Save">
Speichern
Quellsysteme speichern
</MudButton>
</MudItem>
</MudGrid>
</MudPaper>
<MudDialog @bind-Visible="_sourceSystemDialogVisible" Options="_sourceSystemDialogOptions">
<TitleContent>
<MudText Typo="Typo.h6">@(_editingSourceSystem.Id == 0 ? "Quellsystem hinzufuegen" : "Quellsystem bearbeiten")</MudText>
</TitleContent>
<DialogContent>
<MudTextField @bind-Value="_editingSourceSystem.Code" Label="Code" Required />
<MudTextField @bind-Value="_editingSourceSystem.DisplayName" Label="Name" Required />
<MudSelect T="string" @bind-Value="_editingSourceSystem.ConnectionKind" Label="Anschlussart" Required>
@foreach (var kind in SourceSystemConnectionKinds.All)
{
<MudSelectItem Value="@kind">@GetConnectionKindLabel(kind)</MudSelectItem>
}
</MudSelect>
@if (UsesSapGateway(_editingSourceSystem))
{
<MudTextField @bind-Value="_editingSourceSystem.CentralServiceUrl" Label="Zentrale SAP Service URL"
HelperText="Zentrale Standard-URL fuer SAP Gateway. Ein Standort darf sie nur bei Bedarf ueberschreiben." />
}
<MudTextField @bind-Value="_editingSourceSystem.CentralUsername" Label="Zentraler Username" />
<MudTextField @bind-Value="_editingSourceSystem.CentralPassword" Label="Zentrales Passwort" InputType="InputType.Password" />
<MudCheckBox @bind-Value="_editingSourceSystem.IsActive" Label="Aktiv" />
</DialogContent>
<DialogActions>
<MudButton OnClick="CloseSourceSystemDialog">Abbrechen</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveSourceSystemEdit">Uebernehmen</MudButton>
</DialogActions>
</MudDialog>
<MudText Typo="Typo.h5" Class="mb-2">Wechselkurse</MudText>
<MudPaper Class="pa-4 mb-6" Elevation="1">
<MudText Typo="Typo.body2" Class="mb-3">
@@ -250,7 +278,7 @@
<MudItem xs="12" md="4">
<MudSwitch @bind-Value="_exportSettings.DebugLoggingEnabled" Label="Debug Live-Logging" Color="Color.Warning" />
<MudText Typo="Typo.caption">
Schreibt zusätzliche technische Fortschrittsmeldungen für HANA- und SAP-Lesevorgänge ins Dashboard und in die Logs.
Schreibt zusätzliche technische Fortschrittsmeldungen für HANA- und SAP-Lesevorgänge ins Dashboard und in die Logs.
</MudText>
</MudItem>
<MudItem xs="12" md="6">
@@ -285,6 +313,8 @@
@code {
private SharePointConfig _spConfig = new();
private ExportSettings _exportSettings = new();
private List<SourceSystemDefinition> _sourceSystems = [];
private SourceSystemDefinition _editingSourceSystem = new();
private bool _testingSp;
private bool _includeSecretsInExport;
private bool _exportingConfig;
@@ -293,12 +323,15 @@
private string _sharePointTestPreview = string.Empty;
private List<CurrencyExchangeRate> _exchangeRates = [];
private readonly HashSet<string> _testingSystems = [];
private bool _sourceSystemDialogVisible;
private readonly DialogOptions _sourceSystemDialogOptions = new() { MaxWidth = MaxWidth.Small, FullWidth = true };
protected override async Task OnInitializedAsync()
{
using var db = await DbFactory.CreateDbContextAsync();
_spConfig = await db.SharePointConfigs.FirstOrDefaultAsync() ?? new SharePointConfig();
_exportSettings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings();
_sourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync();
_exchangeRates = await db.CurrencyExchangeRates
.OrderBy(x => x.FromCurrency)
.ThenBy(x => x.ToCurrency)
@@ -370,18 +403,138 @@
existing.DebugLoggingEnabled = _exportSettings.DebugLoggingEnabled;
existing.LocalSiteExportFolder = _exportSettings.LocalSiteExportFolder;
existing.LocalConsolidatedExportFolder = _exportSettings.LocalConsolidatedExportFolder;
existing.SapUsername = _exportSettings.SapUsername;
existing.SapPassword = _exportSettings.SapPassword;
existing.Bi1Username = _exportSettings.Bi1Username;
existing.Bi1Password = _exportSettings.Bi1Password;
existing.SageUsername = _exportSettings.SageUsername;
existing.SagePassword = _exportSettings.SagePassword;
}
await db.SaveChangesAsync();
TimerService.Recalculate();
Snackbar.Add("Export Einstellungen gespeichert", Severity.Success);
}
private void AddSourceSystem()
{
_editingSourceSystem = new SourceSystemDefinition
{
Code = string.Empty,
DisplayName = string.Empty,
ConnectionKind = SourceSystemConnectionKinds.Hana,
IsActive = true
};
_sourceSystemDialogVisible = true;
}
private void EditSourceSystem(SourceSystemDefinition definition)
{
_editingSourceSystem = new SourceSystemDefinition
{
Id = definition.Id,
Code = definition.Code,
DisplayName = definition.DisplayName,
ConnectionKind = definition.ConnectionKind,
IsActive = definition.IsActive,
CentralServiceUrl = definition.CentralServiceUrl,
CentralUsername = definition.CentralUsername,
CentralPassword = definition.CentralPassword
};
_sourceSystemDialogVisible = true;
}
private void SaveSourceSystemEdit()
{
_editingSourceSystem.Code = NormalizeSourceSystemCode(_editingSourceSystem.Code);
_editingSourceSystem.DisplayName = NormalizeConfigValue(_editingSourceSystem.DisplayName);
_editingSourceSystem.ConnectionKind = NormalizeConnectionKind(_editingSourceSystem.ConnectionKind);
_editingSourceSystem.CentralServiceUrl = NormalizeConfigValue(_editingSourceSystem.CentralServiceUrl);
_editingSourceSystem.CentralUsername = NormalizeConfigValue(_editingSourceSystem.CentralUsername);
_editingSourceSystem.CentralPassword = _editingSourceSystem.CentralPassword ?? string.Empty;
if (string.IsNullOrWhiteSpace(_editingSourceSystem.Code) || string.IsNullOrWhiteSpace(_editingSourceSystem.DisplayName))
{
Snackbar.Add("Code und Name fuer das Quellsystem sind Pflicht.", Severity.Warning);
return;
}
if (_sourceSystems.Any(x => x.Id != _editingSourceSystem.Id && x.Code == _editingSourceSystem.Code))
{
Snackbar.Add($"Quellsystem-Code doppelt vorhanden: {_editingSourceSystem.Code}", Severity.Warning);
return;
}
if (_editingSourceSystem.Id == 0)
{
_sourceSystems.Add(_editingSourceSystem);
}
else
{
var existing = _sourceSystems.FirstOrDefault(x => x.Id == _editingSourceSystem.Id);
if (existing is not null)
{
existing.Code = _editingSourceSystem.Code;
existing.DisplayName = _editingSourceSystem.DisplayName;
existing.ConnectionKind = _editingSourceSystem.ConnectionKind;
existing.IsActive = _editingSourceSystem.IsActive;
existing.CentralServiceUrl = _editingSourceSystem.CentralServiceUrl;
existing.CentralUsername = _editingSourceSystem.CentralUsername;
existing.CentralPassword = _editingSourceSystem.CentralPassword;
}
}
_sourceSystems = _sourceSystems.OrderBy(x => x.Code).ToList();
_sourceSystemDialogVisible = false;
}
private void CloseSourceSystemDialog()
{
_sourceSystemDialogVisible = false;
}
private void RemoveSourceSystem(SourceSystemDefinition definition)
{
_sourceSystems.Remove(definition);
}
private async Task SaveSourceSystems()
{
var normalized = _sourceSystems
.Select(x => new SourceSystemDefinition
{
Id = x.Id,
Code = NormalizeSourceSystemCode(x.Code),
DisplayName = NormalizeConfigValue(x.DisplayName),
ConnectionKind = NormalizeConnectionKind(x.ConnectionKind),
IsActive = x.IsActive,
CentralServiceUrl = NormalizeConfigValue(x.CentralServiceUrl),
CentralUsername = NormalizeConfigValue(x.CentralUsername),
CentralPassword = x.CentralPassword ?? string.Empty
})
.Where(x => !string.IsNullOrWhiteSpace(x.Code))
.ToList();
if (normalized.Any(x => string.IsNullOrWhiteSpace(x.DisplayName)))
{
Snackbar.Add("Jedes Quellsystem braucht einen Anzeigenamen.", Severity.Warning);
return;
}
var duplicates = normalized
.GroupBy(x => x.Code)
.FirstOrDefault(g => g.Count() > 1);
if (duplicates is not null)
{
Snackbar.Add($"Quellsystem-Code doppelt vorhanden: {duplicates.Key}", Severity.Warning);
return;
}
using var db = await DbFactory.CreateDbContextAsync();
var existing = await db.SourceSystemDefinitions.ToListAsync();
if (existing.Count > 0)
db.SourceSystemDefinitions.RemoveRange(existing);
db.SourceSystemDefinitions.AddRange(normalized);
await db.SaveChangesAsync();
_sourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync();
Snackbar.Add("Quellsysteme gespeichert", Severity.Success);
}
private void AddExchangeRate()
{
_exchangeRates.Add(new CurrencyExchangeRate
@@ -493,6 +646,7 @@
using var db = await DbFactory.CreateDbContextAsync();
_spConfig = await db.SharePointConfigs.FirstOrDefaultAsync() ?? new SharePointConfig();
_exportSettings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings();
_sourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync();
_exchangeRates = await db.CurrencyExchangeRates
.OrderBy(x => x.FromCurrency)
.ThenBy(x => x.ToCurrency)
@@ -513,61 +667,72 @@
private async Task TestCentralCredentials(string sourceSystem)
{
if (sourceSystem == "SAP")
var definition = _sourceSystems.FirstOrDefault(x => string.Equals(x.Code, sourceSystem, StringComparison.OrdinalIgnoreCase));
if (definition is null)
{
await TestCentralSapCredentials();
Snackbar.Add($"Quellsystem '{sourceSystem}' nicht gefunden.", Severity.Warning);
return;
}
await TestCentralHanaCredentials(sourceSystem);
if (string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase))
{
await TestCentralSapCredentials(definition);
return;
}
if (string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.Hana, StringComparison.OrdinalIgnoreCase))
{
await TestCentralHanaCredentials(definition);
}
}
private async Task TestCentralHanaCredentials(string sourceSystem)
private async Task TestCentralHanaCredentials(SourceSystemDefinition definition)
{
var sourceSystem = definition.Code;
if (!_testingSystems.Add(sourceSystem))
return;
try
{
var username = GetCentralUsername(sourceSystem);
var password = GetCentralPassword(sourceSystem);
var username = definition.CentralUsername;
var password = definition.CentralPassword;
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
{
Snackbar.Add($"Für {sourceSystem} sind keine zentralen Zugangsdaten gepflegt.", Severity.Warning);
Snackbar.Add($"Für {sourceSystem} sind keine zentralen Zugangsdaten gepflegt.", Severity.Warning);
return;
}
using var db = await DbFactory.CreateDbContextAsync();
var site = await db.Sites
.Include(s => s.HanaServer)
.Where(s => (string.IsNullOrWhiteSpace(s.SourceSystem) ? "SAP" : s.SourceSystem) == sourceSystem)
.OrderBy(s => s.Land)
var centralServer = await db.HanaServers
.Where(s => s.SourceSystem == sourceSystem)
.OrderBy(s => s.Id)
.FirstOrDefaultAsync();
if (site?.HanaServer is null)
if (centralServer is null || string.IsNullOrWhiteSpace(centralServer.Host))
{
Snackbar.Add($"Kein Standort mit Quellsystem {sourceSystem} und HANA-Verbindung gefunden.", Severity.Warning);
Snackbar.Add($"Keine zentrale HANA-Konfiguration fuer {sourceSystem} gefunden.", Severity.Warning);
return;
}
var testServer = new HanaServer
{
SourceSystem = sourceSystem,
Name = $"{sourceSystem} Central Test",
Host = site.HanaServer.Host,
Port = site.HanaServer.Port,
Host = centralServer.Host,
Port = centralServer.Port,
Username = username.Trim(),
Password = password.Trim(),
DatabaseName = site.HanaServer.DatabaseName,
UseSsl = site.HanaServer.UseSsl,
ValidateCertificate = site.HanaServer.ValidateCertificate,
AdditionalParams = site.HanaServer.AdditionalParams
DatabaseName = centralServer.DatabaseName,
UseSsl = centralServer.UseSsl,
ValidateCertificate = centralServer.ValidateCertificate,
AdditionalParams = centralServer.AdditionalParams
};
var result = await Task.Run(() => HanaService.TestConnectionDetailed(testServer));
if (result.Success)
{
Snackbar.Add($"{sourceSystem}: Verbindung erfolgreich über Standort '{site.Land}'.", Severity.Success);
Snackbar.Add($"{sourceSystem}: Zentrale HANA-Verbindung erfolgreich.", Severity.Success);
}
else
{
@@ -580,42 +745,35 @@
}
}
private async Task TestCentralSapCredentials()
private async Task TestCentralSapCredentials(SourceSystemDefinition definition)
{
const string sourceSystem = "SAP";
var sourceSystem = definition.Code;
if (!_testingSystems.Add(sourceSystem))
return;
try
{
var username = GetCentralUsername(sourceSystem);
var password = GetCentralPassword(sourceSystem);
var username = definition.CentralUsername;
var password = definition.CentralPassword;
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
{
Snackbar.Add("Für SAP sind keine zentralen Gateway-Zugangsdaten gepflegt.", Severity.Warning);
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)
if (string.IsNullOrWhiteSpace(definition.CentralServiceUrl))
{
Snackbar.Add("Kein SAP-Standort mit Service URL gefunden.", Severity.Warning);
Snackbar.Add($"Fuer {sourceSystem} ist keine zentrale SAP Service URL gepflegt.", Severity.Warning);
return;
}
await SapGatewayService.TestConnectionAsync(site.SapServiceUrl, username.Trim(), password.Trim());
Snackbar.Add($"SAP: Gateway-Verbindung erfolgreich über Standort '{site.Land}'.", Severity.Success);
await SapGatewayService.TestConnectionAsync(definition.CentralServiceUrl, username.Trim(), password.Trim());
Snackbar.Add($"{sourceSystem}: Zentrale SAP Gateway-Verbindung erfolgreich.", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"SAP: {ex.Message}", Severity.Error);
Snackbar.Add($"{sourceSystem}: {ex.Message}", Severity.Error);
}
finally
{
@@ -623,19 +781,32 @@
}
}
private string GetCentralUsername(string sourceSystem) => sourceSystem switch
private static string NormalizeSourceSystemCode(string? code) => NormalizeConfigValue(code).ToUpperInvariant();
private static string NormalizeConnectionKind(string? connectionKind)
=> SourceSystemConnectionKinds.All.Contains(connectionKind ?? string.Empty, StringComparer.OrdinalIgnoreCase)
? (connectionKind ?? string.Empty).Trim().ToUpperInvariant()
: SourceSystemConnectionKinds.Hana;
private static string GetConnectionKindLabel(string connectionKind) => connectionKind switch
{
"BI1" => _exportSettings.Bi1Username,
"SAGE" => _exportSettings.SageUsername,
_ => _exportSettings.SapUsername
SourceSystemConnectionKinds.Hana => "HANA",
SourceSystemConnectionKinds.SapGateway => "SAP Gateway",
SourceSystemConnectionKinds.ManualExcel => "Manual Excel",
_ => connectionKind
};
private string GetCentralPassword(string sourceSystem) => sourceSystem switch
{
"BI1" => _exportSettings.Bi1Password,
"SAGE" => _exportSettings.SagePassword,
_ => _exportSettings.SapPassword
};
private static bool UsesManualImport(SourceSystemDefinition definition)
=> string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.ManualExcel, StringComparison.OrdinalIgnoreCase);
private static bool UsesSapGateway(SourceSystemDefinition definition)
=> string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase);
private static string GetServiceUrlSummary(SourceSystemDefinition definition)
=> string.IsNullOrWhiteSpace(definition.CentralServiceUrl) ? "-" : definition.CentralServiceUrl;
private static string GetUsernameSummary(SourceSystemDefinition definition)
=> string.IsNullOrWhiteSpace(definition.CentralUsername) ? "-" : definition.CentralUsername;
private static string NormalizeConfigValue(string? value) => value?.Trim() ?? string.Empty;
@@ -664,3 +835,4 @@
.ToListAsync();
}
}
@@ -1,4 +1,4 @@
@page "/standorte"
@page "/standorte"
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.EntityFrameworkCore
@using System.Text.Json
@@ -17,27 +17,30 @@
<MudText Typo="Typo.h4" Class="mb-4">Standorte</MudText>
<MudText Typo="Typo.h5" Class="mb-2">HANA Server</MudText>
<MudText Typo="Typo.h5" Class="mb-2">Zentrale HANA-Technik</MudText>
<MudPaper Class="pa-4 mb-6" Elevation="1">
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add"
OnClick="AddServer" Class="mb-3">
Server hinzufügen
</MudButton>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Class="mb-3">
Hier erscheinen nur Quellsysteme mit Anschlussart HANA. SAP wird zentral unter Settings -> Quellsysteme gepflegt.
Standorte mit `BI1` oder `SAGE` verwenden diese technischen HANA-Werte automatisch. Im Standort selbst bleiben nur Schema, TSC, Land und optionale Username-/Password-Overrides.
</MudAlert>
<MudText Typo="Typo.body2" Class="mb-3">
Neue HANA-Zeilen entstehen aus den zentral gepflegten Quellsystemen. Falls hier etwas fehlt, lege das Quellsystem in Settings -> Quellsysteme mit Anschlussart `HANA` an.
</MudText>
<MudTable Items="_servers" Dense Hover Striped>
<HeaderContent>
<MudTh>Quellsystem</MudTh>
<MudTh>Name</MudTh>
<MudTh>Host</MudTh>
<MudTh>Port</MudTh>
<MudTh>Username</MudTh>
<MudTh>Verbindungsstatus</MudTh>
<MudTh>Aktionen</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.SourceSystem</MudTd>
<MudTd>@context.Name</MudTd>
<MudTd>@context.Host</MudTd>
<MudTd>@context.Port</MudTd>
<MudTd>@context.Username</MudTd>
<MudTd>
@if (_connectionStatus.TryGetValue(context.Id, out var status))
{
@@ -68,7 +71,7 @@
<MudPaper Class="pa-4" Elevation="1">
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add"
OnClick="AddSite" Class="mb-3">
Neuen Standort hinzufügen
Neuen Standort hinzufügen
</MudButton>
<MudTable Items="_sites" Dense Hover Striped>
@@ -109,22 +112,21 @@
<MudDialog @bind-Visible="_serverDialogVisible" Options="_dialogOptions">
<TitleContent>
<MudText Typo="Typo.h6">@(_editingServer.Id == 0 ? "Server hinzufügen" : "Server bearbeiten")</MudText>
<MudText Typo="Typo.h6">Zentrale HANA-Technik bearbeiten</MudText>
</TitleContent>
<DialogContent>
<MudTextField Value="_editingServer.SourceSystem" Label="Quellsystem" ReadOnly />
<MudTextField @bind-Value="_editingServer.Name" Label="Name" Required />
<MudTextField @bind-Value="_editingServer.Host" Label="Host" Required
HelperText="IP oder Hostname (ohne Protokoll)" />
<MudNumericField @bind-Value="_editingServer.Port" Label="Port"
HelperText="Typisch 30015 (Tenant), 30013 (SystemDB), 3xx15 für Instanz xx" />
<MudTextField @bind-Value="_editingServer.Username" Label="Username" />
<MudTextField @bind-Value="_editingServer.Password" Label="Password" InputType="InputType.Password" />
HelperText="Typisch 30015 (Tenant), 30013 (SystemDB), 3xx15 für Instanz xx" />
<MudTextField @bind-Value="_editingServer.DatabaseName" Label="Database Name (MDC)"
HelperText="Nur bei Multi-Tenant Setup angeben, sonst leer lassen" />
<MudSwitch @bind-Value="_editingServer.UseSsl" Label="SSL/TLS verwenden (encrypt=true)" Color="Color.Primary" />
<MudSwitch @bind-Value="_editingServer.ValidateCertificate" Label="SSL-Zertifikat validieren" Color="Color.Primary"
Disabled="!_editingServer.UseSsl" />
<MudTextField @bind-Value="_editingServer.AdditionalParams" Label="Zusätzliche Parameter"
<MudTextField @bind-Value="_editingServer.AdditionalParams" Label="Zusätzliche Parameter"
HelperText="Optional, z.B. sslCryptoProvider=openssl;communicationTimeout=0" />
</DialogContent>
<DialogActions>
@@ -135,16 +137,16 @@
<MudDialog @bind-Visible="_siteDialogVisible" Options="_dialogOptions">
<TitleContent>
<MudText Typo="Typo.h6">@(_editingSite.Id == 0 ? "Standort hinzufügen" : "Standort bearbeiten")</MudText>
<MudText Typo="Typo.h6">@(_editingSite.Id == 0 ? "Standort hinzufügen" : "Standort bearbeiten")</MudText>
</TitleContent>
<DialogContent>
<MudTextField @bind-Value="_editingSite.Schema" Label="Schema" Required />
<MudTextField @bind-Value="_editingSite.TSC" Label="TSC" Required />
<MudTextField @bind-Value="_editingSite.Land" Label="Land" Required />
<MudSelect @bind-Value="_editingSite.SourceSystem" Label="Quellsystem" Required>
@foreach (var system in _sourceSystems)
@foreach (var system in GetAvailableSourceSystems())
{
<MudSelectItem Value="@system">@system</MudSelectItem>
<MudSelectItem Value="@system.Code">@GetSourceSystemLabel(system)</MudSelectItem>
}
</MudSelect>
<MudTextField @bind-Value="_editingSite.UsernameOverride" Label="Username Override"
@@ -152,7 +154,7 @@
<MudTextField @bind-Value="_editingSite.PasswordOverride" Label="Password Override" InputType="InputType.Password"
HelperText="Optional. Wenn leer, wird das zentrale Passwort des Quellsystems verwendet." />
<MudTextField @bind-Value="_editingSite.LocalExportFolderOverride" Label="Lokaler Exportpfad Override"
HelperText="Optional. Wenn leer, wird der zentrale Standardpfad für Standort-Dateien verwendet." />
HelperText="Optional. Wenn leer, wird der zentrale Standardpfad für Standort-Dateien verwendet." />
<MudCheckBox @bind-Value="_editingSite.IsActive" Label="Aktiv" />
<MudDivider Class="my-4" />
@@ -161,10 +163,11 @@
{
<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.
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/" />
<MudText Typo="Typo.body2">Zentrale SAP Service URL: @GetCentralSapServiceUrlSummary(_editingSite.SourceSystem)</MudText>
<MudTextField @bind-Value="_editingSite.SapServiceUrl" Label="SAP Service URL Override"
HelperText="Optional. Wenn leer, wird die zentrale SAP Service URL des Quellsystems verwendet." />
<MudStack Row Spacing="2" Class="mb-3">
<MudButton Variant="Variant.Outlined" Color="Color.Info" OnClick="RefreshSapEntitySets"
StartIcon="@Icons.Material.Filled.Refresh" Disabled="_refreshingSapEntitySets">
@@ -188,16 +191,16 @@
<MudDivider Class="my-4" />
<MudStack Row Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-2">
<MudText Typo="Typo.h6">SAP Quellen</MudText>
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddSapSource">Quelle hinzufügen</MudButton>
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddSapSource">Quelle hinzufügen</MudButton>
</MudStack>
<MudText Typo="Typo.caption" Class="mb-2">
Pro Quelle Alias und Entity Set definieren. Joins verwenden links/rechts kommagetrennte Schlüsselfelder wie `VBELN,POSNR`. Feldmappings erwarten `Alias.Feldname` oder Konstanten wie `=SAP`.
Pro Quelle Alias und Entity Set definieren. Joins verwenden links/rechts kommagetrennte Schlüsselfelder wie `VBELN,POSNR`. Feldmappings erwarten `Alias.Feldname` oder Konstanten wie `=SAP`.
</MudText>
<MudTable Items="_sapSources" Dense Hover Striped>
<HeaderContent>
<MudTh>Alias</MudTh>
<MudTh>Entity Set</MudTh>
<MudTh>Primär</MudTh>
<MudTh>Primär</MudTh>
<MudTh>Aktiv</MudTh>
<MudTh>Aktionen</MudTh>
</HeaderContent>
@@ -225,7 +228,7 @@
OnClick="AutoMatchSapJoins">
Auto-Match
</MudButton>
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddSapJoin">Join hinzufügen</MudButton>
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddSapJoin">Join hinzufügen</MudButton>
</MudStack>
</MudStack>
<MudTable Items="_sapJoins" Dense Hover Striped>
@@ -305,11 +308,11 @@
@("Felder aus Quellen laden")
}
</MudButton>
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddSapMapping">Mapping hinzufügen</MudButton>
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddSapMapping">Mapping hinzufügen</MudButton>
</MudStack>
</MudStack>
<MudText Typo="Typo.caption" Class="mb-2">
Source Expressions werden aus den hinzugefügten SAP-Quellen als `Alias.Feldname` gelesen. Vorhandene manuelle Werte bleiben auswählbar.
Source Expressions werden aus den hinzugefügten SAP-Quellen als `Alias.Feldname` gelesen. Vorhandene manuelle Werte bleiben auswählbar.
</MudText>
<MudTable Items="_sapMappings" Dense Hover Striped>
<HeaderContent>
@@ -346,7 +349,7 @@
{
<MudText Typo="Typo.h6" Class="mb-2">Manueller Excel-Import</MudText>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Class="mb-3">
Für diesen Standort wird keine SAP- oder HANA-Verbindung verwendet. Es wird die hier hinterlegte Excel-Datei gelesen und in `CentralSalesRecords` übernommen.
Für diesen Standort wird keine SAP- oder HANA-Verbindung verwendet. Es wird die hier hinterlegte Excel-Datei gelesen und in `CentralSalesRecords` übernommen.
</MudAlert>
<InputFile OnChange="UploadManualImportFileAsync" accept=".xlsx" />
@if (_uploadingManualImport)
@@ -371,23 +374,12 @@
{
<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.
Die technische HANA-Verbindung kommt aus der zentralen HANA-Konfiguration des Quellsystems. Im Standort selbst pflegst du nur fachliche Standortdaten und optionale Username-/Password-Overrides.
</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" />
<MudText Typo="Typo.body2">Aktive Zentralverbindung: @GetCentralHanaSummary(_editingSite.SourceSystem)</MudText>
<MudText Typo="Typo.caption" Class="mt-2">
Host, Port, SSL und technische Parameter bearbeitest du oben in der zentralen HANA-Konfiguration.
</MudText>
}
</DialogContent>
<DialogActions>
@@ -397,10 +389,10 @@
</MudDialog>
@code {
private readonly string[] _sourceSystems = ["SAP", "BI1", "SAGE", "MANUAL_EXCEL"];
private readonly Dictionary<int, ConnectionTestResult> _connectionStatus = new();
private List<HanaServer> _servers = new();
private List<Site> _sites = new();
private List<SourceSystemDefinition> _sourceSystemDefinitions = new();
private List<string> _sapEntitySetsCache = [];
private List<string> _sapAvailableSourceExpressions = [];
private Dictionary<string, List<string>> _sapSourceFieldMap = new(StringComparer.OrdinalIgnoreCase);
@@ -413,7 +405,6 @@
.ToArray();
private HanaServer _editingServer = new();
private Site _editingSite = new();
private HanaServer _editingSiteServer = new();
private bool _serverDialogVisible;
private bool _siteDialogVisible;
private bool _refreshingSapEntitySets;
@@ -431,16 +422,17 @@
private async Task LoadDataAsync()
{
using var db = await DbFactory.CreateDbContextAsync();
_servers = await db.HanaServers.OrderBy(s => s.Name).ToListAsync();
_sourceSystemDefinitions = await db.SourceSystemDefinitions
.OrderBy(x => x.Code)
.ToListAsync();
_servers = await db.HanaServers
.Where(s => GetHanaSourceSystemCodes().Contains(s.SourceSystem))
.OrderBy(s => s.SourceSystem)
.ThenBy(s => s.Name)
.ToListAsync();
_sites = await db.Sites.Include(s => s.HanaServer).OrderBy(s => s.Land).ToListAsync();
}
private void AddServer()
{
_editingServer = new HanaServer { Port = 30015 };
_serverDialogVisible = true;
}
private void EditServer(HanaServer server)
{
_editingServer = CloneServer(server);
@@ -455,21 +447,50 @@
_savingServer = true;
try
{
_editingServer.SourceSystem = string.IsNullOrWhiteSpace(_editingServer.SourceSystem)
? GetHanaSourceSystemCodes().FirstOrDefault() ?? string.Empty
: _editingServer.SourceSystem.Trim().ToUpperInvariant();
_editingServer.Name = string.IsNullOrWhiteSpace(_editingServer.Name) ? _editingServer.SourceSystem : _editingServer.Name.Trim();
_editingServer.Host = _editingServer.Host.Trim();
_editingServer.DatabaseName = _editingServer.DatabaseName.Trim();
_editingServer.AdditionalParams = _editingServer.AdditionalParams.Trim();
_editingServer.Username = string.Empty;
_editingServer.Password = string.Empty;
using var db = await DbFactory.CreateDbContextAsync();
if (_editingServer.Id == 0)
{
db.HanaServers.Add(_editingServer);
var existingForSourceSystem = await db.HanaServers
.OrderBy(x => x.Id)
.FirstOrDefaultAsync(x => x.SourceSystem == _editingServer.SourceSystem);
if (existingForSourceSystem is null)
{
db.HanaServers.Add(_editingServer);
}
else
{
existingForSourceSystem.Name = _editingServer.Name;
existingForSourceSystem.Host = _editingServer.Host;
existingForSourceSystem.Port = _editingServer.Port;
existingForSourceSystem.Username = string.Empty;
existingForSourceSystem.Password = string.Empty;
existingForSourceSystem.DatabaseName = _editingServer.DatabaseName;
existingForSourceSystem.UseSsl = _editingServer.UseSsl;
existingForSourceSystem.ValidateCertificate = _editingServer.ValidateCertificate;
existingForSourceSystem.AdditionalParams = _editingServer.AdditionalParams;
}
}
else
{
var existing = await db.HanaServers.FindAsync(_editingServer.Id);
if (existing is not null)
{
existing.SourceSystem = _editingServer.SourceSystem;
existing.Name = _editingServer.Name;
existing.Host = _editingServer.Host;
existing.Port = _editingServer.Port;
existing.Username = _editingServer.Username;
existing.Password = _editingServer.Password;
existing.Username = string.Empty;
existing.Password = string.Empty;
existing.DatabaseName = _editingServer.DatabaseName;
existing.UseSsl = _editingServer.UseSsl;
existing.ValidateCertificate = _editingServer.ValidateCertificate;
@@ -490,10 +511,16 @@
private async Task DeleteServer(HanaServer server)
{
if (IsHanaSourceSystem(server.SourceSystem))
{
Snackbar.Add($"Die zentrale HANA-Konfiguration fuer {server.SourceSystem} kann nicht geloescht werden.", Severity.Warning);
return;
}
var result = await DialogService.ShowMessageBox(
"Server löschen",
$"Server '{server.Name}' wirklich löschen?",
yesText: "Löschen", cancelText: "Abbrechen");
"Server löschen",
$"Server '{server.Name}' wirklich löschen?",
yesText: "Löschen", cancelText: "Abbrechen");
if (result != true) return;
@@ -509,7 +536,7 @@
if (linkedSites.Count > 0)
{
Snackbar.Add(
$"Server kann nicht gelöscht werden. Noch verknüpfte Standorte: {string.Join(", ", linkedSites)}",
$"Server kann nicht gelöscht werden. Noch verknüpfte Standorte: {string.Join(", ", linkedSites)}",
Severity.Warning);
return;
}
@@ -523,19 +550,51 @@
}
catch (Exception ex)
{
Snackbar.Add($"Server konnte nicht gelöscht werden: {ex.Message}", Severity.Error);
Snackbar.Add($"Server konnte nicht gelöscht werden: {ex.Message}", Severity.Error);
return;
}
await LoadDataAsync();
Snackbar.Add("Server gelöscht", Severity.Info);
Snackbar.Add("Server gelöscht", Severity.Info);
}
private async Task TestServerConnection(HanaServer server)
{
using var db = await DbFactory.CreateDbContextAsync();
var sourceDefinition = await db.SourceSystemDefinitions
.OrderBy(x => x.Id)
.FirstOrDefaultAsync(x => x.Code == server.SourceSystem);
if (sourceDefinition is null)
{
Snackbar.Add($"Quellsystem '{server.SourceSystem}' nicht gefunden.", Severity.Warning);
return;
}
if (string.IsNullOrWhiteSpace(sourceDefinition.CentralUsername) || string.IsNullOrWhiteSpace(sourceDefinition.CentralPassword))
{
Snackbar.Add($"Fuer {server.SourceSystem} sind keine zentralen Zugangsdaten im Quellsystem gepflegt.", Severity.Warning);
return;
}
var testServer = new HanaServer
{
Id = server.Id,
SourceSystem = server.SourceSystem,
Name = server.Name,
Host = server.Host,
Port = server.Port,
Username = sourceDefinition.CentralUsername.Trim(),
Password = sourceDefinition.CentralPassword,
DatabaseName = server.DatabaseName,
UseSsl = server.UseSsl,
ValidateCertificate = server.ValidateCertificate,
AdditionalParams = server.AdditionalParams
};
await AppEventLogService.WriteAsync("HANA", "Server-Test aus UI gestartet",
details: server.GetConnectionStringPreview());
var result = await Task.Run(() => HanaService.TestConnectionDetailed(server));
details: testServer.GetConnectionStringPreview());
var result = await Task.Run(() => HanaService.TestConnectionDetailed(testServer));
_connectionStatus[server.Id] = result;
if (result.Success)
@@ -562,7 +621,7 @@
_editingSite = new Site
{
IsActive = true,
SourceSystem = "SAP",
SourceSystem = GetAvailableSourceSystems().FirstOrDefault()?.Code ?? "SAP",
HanaServerId = null,
ManualImportFilePath = string.Empty
};
@@ -572,7 +631,6 @@
_sapSources = [];
_sapJoins = [];
_sapMappings = [];
_editingSiteServer = CreateDefaultSiteServer();
_siteDialogVisible = true;
}
@@ -585,7 +643,9 @@
Schema = site.Schema,
TSC = site.TSC,
Land = site.Land,
SourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) ? "SAP" : site.SourceSystem,
SourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem)
? GetAvailableSourceSystems().FirstOrDefault()?.Code ?? "SAP"
: site.SourceSystem,
UsernameOverride = site.UsernameOverride,
PasswordOverride = site.PasswordOverride,
LocalExportFolderOverride = site.LocalExportFolderOverride,
@@ -604,9 +664,6 @@
_sapMappings = db.SapFieldMappings.Where(m => m.SiteId == site.Id).OrderBy(m => m.SortOrder).ThenBy(m => m.Id).ToList();
_sapAvailableSourceExpressions = BuildSourceExpressionsFromMappings();
_sapSourceFieldMap = BuildSourceFieldMapFromJoins();
_editingSiteServer = site.HanaServer is null
? CreateDefaultSiteServer(site)
: CloneServer(site.HanaServer);
_siteDialogVisible = true;
}
@@ -619,7 +676,7 @@
try
{
using var db = await DbFactory.CreateDbContextAsync();
var serverId = UsesHanaConnection() ? await SaveOrCreateSiteServerAsync(db) : (int?)null;
var serverId = UsesHanaConnection() ? await ResolveCentralHanaServerIdAsync(db, _editingSite.SourceSystem) : (int?)null;
_editingSite.HanaServerId = serverId;
_editingSite.SapEntitySetsCache = SerializeSapEntitySets(_sapEntitySetsCache);
@@ -669,9 +726,9 @@
private async Task DeleteSite(Site site)
{
var result = await DialogService.ShowMessageBox(
"Standort löschen",
$"Standort '{site.Land}' wirklich löschen?",
yesText: "Löschen", cancelText: "Abbrechen");
"Standort löschen",
$"Standort '{site.Land}' wirklich löschen?",
yesText: "Löschen", cancelText: "Abbrechen");
if (result != true) return;
@@ -692,7 +749,7 @@
}
await LoadDataAsync();
Snackbar.Add("Standort gelöscht", Severity.Info);
Snackbar.Add("Standort gelöscht", Severity.Info);
}
private static string GetServerNode(HanaServer? server)
@@ -703,40 +760,17 @@
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;
if (string.Equals(sourceSystem, "MANUAL_EXCEL", StringComparison.OrdinalIgnoreCase))
return string.IsNullOrWhiteSpace(site.ManualImportFilePath) ? "-" : Path.GetFileName(site.ManualImportFilePath);
return GetServerNode(site.HanaServer);
}
private HanaServer CreateDefaultSiteServer(Site? site = null)
{
var label = !string.IsNullOrWhiteSpace(site?.Land) ? site!.Land : site?.TSC;
if (string.IsNullOrWhiteSpace(label))
label = "Neuer Standort";
return new HanaServer
{
Name = $"{label} HANA",
Port = 30015
};
}
private static HanaServer CloneServer(HanaServer server)
{
return new HanaServer
{
Id = server.Id,
SourceSystem = server.SourceSystem,
Name = server.Name,
Host = server.Host,
Port = server.Port,
Username = server.Username,
Password = server.Password,
Username = string.Empty,
Password = string.Empty,
DatabaseName = server.DatabaseName,
UseSsl = server.UseSsl,
ValidateCertificate = server.ValidateCertificate,
@@ -744,66 +778,98 @@
};
}
private async Task<int> SaveOrCreateSiteServerAsync(AppDbContext db)
private async Task<int> ResolveCentralHanaServerIdAsync(AppDbContext db, string sourceSystem)
{
_editingSiteServer.Name = string.IsNullOrWhiteSpace(_editingSiteServer.Name)
? $"{_editingSite.Land} HANA".Trim()
: _editingSiteServer.Name.Trim();
_editingSite.UsernameOverride = _editingSite.UsernameOverride.Trim();
_editingSite.PasswordOverride = _editingSite.PasswordOverride.Trim();
_editingSite.LocalExportFolderOverride = _editingSite.LocalExportFolderOverride.Trim();
_editingSite.ManualImportFilePath = _editingSite.ManualImportFilePath.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();
_editingSiteServer.AdditionalParams = _editingSiteServer.AdditionalParams.Trim();
if (string.IsNullOrWhiteSpace(_editingSiteServer.Host))
throw new InvalidOperationException("Host oder ServerNode muss gesetzt sein.");
var normalizedSourceSystem = string.IsNullOrWhiteSpace(sourceSystem) ? string.Empty : sourceSystem.Trim().ToUpperInvariant();
var centralServer = await db.HanaServers
.OrderBy(x => x.Id)
.FirstOrDefaultAsync(x => x.SourceSystem == normalizedSourceSystem);
if (_editingSite.HanaServerId == 0)
{
db.HanaServers.Add(_editingSiteServer);
await db.SaveChangesAsync();
return _editingSiteServer.Id;
}
if (centralServer is null || string.IsNullOrWhiteSpace(centralServer.Host))
throw new InvalidOperationException($"Fuer Quellsystem '{normalizedSourceSystem}' ist keine gueltige zentrale HANA-Konfiguration vorhanden.");
var sharedUseCount = await db.Sites.CountAsync(s => s.HanaServerId == _editingSite.HanaServerId && s.Id != _editingSite.Id);
if (sharedUseCount > 0)
{
var dedicatedServer = CloneServer(_editingSiteServer);
dedicatedServer.Id = 0;
db.HanaServers.Add(dedicatedServer);
await db.SaveChangesAsync();
return dedicatedServer.Id;
}
var existingServer = await db.HanaServers.FindAsync(_editingSite.HanaServerId);
if (existingServer is null)
{
db.HanaServers.Add(_editingSiteServer);
await db.SaveChangesAsync();
return _editingSiteServer.Id;
}
existingServer.Name = _editingSiteServer.Name;
existingServer.Host = _editingSiteServer.Host;
existingServer.Port = _editingSiteServer.Port;
existingServer.Username = _editingSiteServer.Username;
existingServer.Password = _editingSiteServer.Password;
existingServer.DatabaseName = _editingSiteServer.DatabaseName;
existingServer.UseSsl = _editingSiteServer.UseSsl;
existingServer.ValidateCertificate = _editingSiteServer.ValidateCertificate;
existingServer.AdditionalParams = _editingSiteServer.AdditionalParams;
await db.SaveChangesAsync();
return existingServer.Id;
return centralServer.Id;
}
private bool IsSapSite() => string.Equals(_editingSite.SourceSystem, "SAP", StringComparison.OrdinalIgnoreCase);
private bool IsManualExcelSite() => string.Equals(_editingSite.SourceSystem, "MANUAL_EXCEL", StringComparison.OrdinalIgnoreCase);
private bool UsesHanaConnection() => !IsSapSite() && !IsManualExcelSite();
private IEnumerable<SourceSystemDefinition> GetAvailableSourceSystems()
=> _sourceSystemDefinitions
.Where(x => x.IsActive || string.Equals(x.Code, _editingSite.SourceSystem, StringComparison.OrdinalIgnoreCase))
.OrderBy(x => x.DisplayName)
.ThenBy(x => x.Code);
private List<string> GetHanaSourceSystemCodes()
=> _sourceSystemDefinitions
.Where(x => string.Equals(x.ConnectionKind, SourceSystemConnectionKinds.Hana, StringComparison.OrdinalIgnoreCase))
.Select(x => x.Code)
.OrderBy(x => x)
.ToList();
private string GetSourceSystemConnectionKind(string? sourceSystem)
=> _sourceSystemDefinitions
.FirstOrDefault(x => string.Equals(x.Code, sourceSystem, StringComparison.OrdinalIgnoreCase))
?.ConnectionKind
?? SourceSystemConnectionKinds.SapGateway;
private bool IsHanaSourceSystem(string? sourceSystem)
=> string.Equals(GetSourceSystemConnectionKind(sourceSystem), SourceSystemConnectionKinds.Hana, StringComparison.OrdinalIgnoreCase);
private bool IsSapSite()
=> string.Equals(GetSourceSystemConnectionKind(_editingSite.SourceSystem), SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase);
private bool IsManualExcelSite()
=> string.Equals(GetSourceSystemConnectionKind(_editingSite.SourceSystem), SourceSystemConnectionKinds.ManualExcel, StringComparison.OrdinalIgnoreCase);
private bool UsesHanaConnection() => IsHanaSourceSystem(_editingSite.SourceSystem);
private string GetSourceSystemLabel(SourceSystemDefinition definition)
=> string.IsNullOrWhiteSpace(definition.DisplayName) ? definition.Code : $"{definition.DisplayName} ({definition.Code})";
private string GetConnectionTarget(Site site)
{
var connectionKind = GetSourceSystemConnectionKind(site.SourceSystem);
if (string.Equals(connectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase))
return GetEffectiveSapServiceUrl(site);
if (string.Equals(connectionKind, SourceSystemConnectionKinds.ManualExcel, StringComparison.OrdinalIgnoreCase))
return string.IsNullOrWhiteSpace(site.ManualImportFilePath) ? "-" : Path.GetFileName(site.ManualImportFilePath);
return GetServerNode(site.HanaServer);
}
private string GetEffectiveSapServiceUrl(Site site)
{
if (!string.IsNullOrWhiteSpace(site.SapServiceUrl))
return site.SapServiceUrl;
var sourceDefinition = _sourceSystemDefinitions
.FirstOrDefault(x => string.Equals(x.Code, site.SourceSystem, StringComparison.OrdinalIgnoreCase));
return string.IsNullOrWhiteSpace(sourceDefinition?.CentralServiceUrl) ? "-" : sourceDefinition.CentralServiceUrl;
}
private string GetCentralSapServiceUrlSummary(string sourceSystem)
{
var sourceDefinition = _sourceSystemDefinitions
.FirstOrDefault(x => string.Equals(x.Code, sourceSystem, StringComparison.OrdinalIgnoreCase));
return string.IsNullOrWhiteSpace(sourceDefinition?.CentralServiceUrl) ? "-" : sourceDefinition.CentralServiceUrl;
}
private string GetCentralHanaSummary(string sourceSystem)
{
var normalizedSourceSystem = string.IsNullOrWhiteSpace(sourceSystem) ? string.Empty : sourceSystem.Trim().ToUpperInvariant();
var centralServer = _servers.FirstOrDefault(x => x.SourceSystem == normalizedSourceSystem);
if (centralServer is null || string.IsNullOrWhiteSpace(centralServer.Host))
return $"keine zentrale HANA-Konfiguration fuer {normalizedSourceSystem}";
return $"{centralServer.Name} | {GetServerNode(centralServer)}";
}
private async Task RefreshSapEntitySets()
{
@@ -813,20 +879,28 @@
_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;
var sourceDefinition = await db.SourceSystemDefinitions
.OrderBy(x => x.Id)
.FirstOrDefaultAsync(x => x.Code == _editingSite.SourceSystem);
var serviceUrl = string.IsNullOrWhiteSpace(_editingSite.SapServiceUrl)
? sourceDefinition?.CentralServiceUrl ?? string.Empty
: _editingSite.SapServiceUrl;
if (string.IsNullOrWhiteSpace(serviceUrl))
throw new InvalidOperationException("Es ist weder eine zentrale SAP Service URL noch ein Standort-Override gesetzt.");
var username = string.IsNullOrWhiteSpace(_editingSite.UsernameOverride)
? sourceDefinition?.CentralUsername ?? string.Empty
: _editingSite.UsernameOverride;
var password = string.IsNullOrWhiteSpace(_editingSite.PasswordOverride)
? sourceDefinition?.CentralPassword ?? string.Empty
: _editingSite.PasswordOverride;
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
throw new InvalidOperationException("Für SAP sind weder zentrale Zugangsdaten noch Standort-Overrides gesetzt.");
throw new InvalidOperationException("Für SAP sind weder zentrale Zugangsdaten noch Standort-Overrides gesetzt.");
await AppEventLogService.WriteAsync("SAP", "Refresh aus UI gestartet", siteId: _editingSite.Id, land: _editingSite.Land,
details: _editingSite.SapServiceUrl);
var entitySets = await SapGatewayService.GetEntitySetsAsync(_editingSite.SapServiceUrl, username.Trim(), password.Trim());
details: serviceUrl);
var entitySets = await SapGatewayService.GetEntitySetsAsync(serviceUrl, username.Trim(), password.Trim());
_sapEntitySetsCache = entitySets;
_editingSite.SapEntitySetsCache = SerializeSapEntitySets(entitySets);
_editingSite.SapEntitySetsRefreshedAtUtc = DateTime.UtcNow;
@@ -884,7 +958,7 @@
var extension = Path.GetExtension(file.Name);
if (!string.Equals(extension, ".xlsx", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Bitte eine Excel-Datei mit Endung .xlsx auswählen.");
throw new InvalidOperationException("Bitte eine Excel-Datei mit Endung .xlsx auswählen.");
}
var uploadDirectory = Path.Combine(AppContext.BaseDirectory, "manual-imports");
@@ -974,13 +1048,13 @@
if (activeSources.Count < 2)
{
Snackbar.Add("Für Auto-Match werden mindestens zwei aktive SAP-Quellen benötigt.", Severity.Warning);
Snackbar.Add("Für Auto-Match werden mindestens zwei aktive SAP-Quellen benötigt.", Severity.Warning);
return;
}
if (_sapSourceFieldMap.Count == 0)
{
Snackbar.Add("Bitte zuerst 'Felder aus Quellen laden' ausführen.", Severity.Warning);
Snackbar.Add("Bitte zuerst 'Felder aus Quellen laden' ausführen.", Severity.Warning);
return;
}
@@ -1038,7 +1112,7 @@
}
NormalizeSapConfigCollections();
Snackbar.Add($"{createdOrUpdated} Join-Vorschläge gesetzt.", Severity.Success);
Snackbar.Add($"{createdOrUpdated} Join-Vorschläge gesetzt.", Severity.Success);
}
private void RemoveSapJoin(SapJoinDefinition join)
@@ -1116,9 +1190,6 @@
_refreshingSapSourceFields = true;
try
{
if (string.IsNullOrWhiteSpace(_editingSite.SapServiceUrl))
throw new InvalidOperationException("SAP Service URL muss gesetzt sein.");
var activeSources = _sapSources
.Where(s => s.IsActive && !string.IsNullOrWhiteSpace(s.Alias) && !string.IsNullOrWhiteSpace(s.EntitySet))
.OrderBy(s => s.SortOrder)
@@ -1129,18 +1200,29 @@
throw new InvalidOperationException("Es gibt keine aktiven SAP-Quellen mit Alias und Entity Set.");
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;
var sourceDefinition = await db.SourceSystemDefinitions
.OrderBy(x => x.Id)
.FirstOrDefaultAsync(x => x.Code == _editingSite.SourceSystem);
var serviceUrl = string.IsNullOrWhiteSpace(_editingSite.SapServiceUrl)
? sourceDefinition?.CentralServiceUrl ?? string.Empty
: _editingSite.SapServiceUrl;
if (string.IsNullOrWhiteSpace(serviceUrl))
throw new InvalidOperationException("Es ist weder eine zentrale SAP Service URL noch ein Standort-Override gesetzt.");
var username = string.IsNullOrWhiteSpace(_editingSite.UsernameOverride)
? sourceDefinition?.CentralUsername ?? string.Empty
: _editingSite.UsernameOverride;
var password = string.IsNullOrWhiteSpace(_editingSite.PasswordOverride)
? sourceDefinition?.CentralPassword ?? string.Empty
: _editingSite.PasswordOverride;
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
throw new InvalidOperationException("Für SAP sind weder zentrale Zugangsdaten noch Standort-Overrides gesetzt.");
throw new InvalidOperationException("Für SAP sind weder zentrale Zugangsdaten noch Standort-Overrides gesetzt.");
var expressions = new List<string> { "=SAP" };
var sourceFieldMap = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
foreach (var source in activeSources)
{
var fieldNames = await SapGatewayService.GetEntityFieldNamesAsync(_editingSite.SapServiceUrl, source.EntitySet, username.Trim(), password.Trim());
var fieldNames = await SapGatewayService.GetEntityFieldNamesAsync(serviceUrl, source.EntitySet, username.Trim(), password.Trim());
sourceFieldMap[source.Alias] = fieldNames;
expressions.AddRange(fieldNames.Select(field => $"{source.Alias}.{field}"));
}
@@ -1250,4 +1332,6 @@
.Where(x => !string.IsNullOrWhiteSpace(x))
.ToHashSet(StringComparer.OrdinalIgnoreCase)
?? [];
}
}
@@ -45,9 +45,9 @@
<MudTd><MudCheckBox @bind-Value="context.IsActive" /></MudTd>
<MudTd>
<MudSelect T="string" Value="@context.SourceSystem" ValueChanged="@(v => context.SourceSystem = v)" Dense>
@foreach (var system in _systems)
@foreach (var system in _sourceSystems.Where(x => x.IsActive))
{
<MudSelectItem Value="@system">@system</MudSelectItem>
<MudSelectItem Value="@system.Code">@system.DisplayName (@system.Code)</MudSelectItem>
}
</MudSelect>
</MudTd>
@@ -176,7 +176,6 @@
</MudDialog>
@code {
private readonly string[] _systems = ["SAP", "BI1", "SAGE", "MANUAL_EXCEL"];
private readonly string[] _ruleScopes = ["Value", "Record"];
private readonly string[] _recordFields = typeof(SalesRecord)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
@@ -185,6 +184,7 @@
.ToArray();
private List<FieldTransformationRule> _rules = new();
private List<SourceSystemDefinition> _sourceSystems = [];
private IReadOnlyList<TransformationCatalogItem> _catalogItems = [];
private bool _codeDialogVisible;
private FieldTransformationRule? _selectedRule;
@@ -200,6 +200,7 @@
private async Task LoadAsync()
{
using var db = await DbFactory.CreateDbContextAsync();
_sourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync();
_rules = await db.FieldTransformationRules.OrderBy(r => r.SortOrder).ThenBy(r => r.Id).ToListAsync();
foreach (var rule in _rules)
@@ -217,7 +218,7 @@
var nextSort = _rules.Count == 0 ? 10 : _rules.Max(r => r.SortOrder) + 10;
_rules.Add(new FieldTransformationRule
{
SourceSystem = "SAP",
SourceSystem = _sourceSystems.FirstOrDefault(x => x.IsActive)?.Code ?? "SAP",
RuleScope = "Value",
SourceField = nameof(SalesRecord.Material),
TargetField = nameof(SalesRecord.Material),