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(); using var db = await DbFactory.CreateDbContextAsync();
var sites = await db.Sites.Include(s => s.HanaServer).Where(s => s.IsActive).ToListAsync(); 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 var logs = await db.ExportLogs
.GroupBy(l => l.SiteId) .GroupBy(l => l.SiteId)
.Select(g => g.OrderByDescending(l => l.Timestamp).First()) .Select(g => g.OrderByDescending(l => l.Timestamp).First())
@@ -190,14 +191,15 @@
{ {
var log = logs.FirstOrDefault(l => l.SiteId == s.Id); var log = logs.FirstOrDefault(l => l.SiteId == s.Id);
latestAppLogsBySite.TryGetValue(s.Id, out var appLog); latestAppLogsBySite.TryGetValue(s.Id, out var appLog);
var sourceSystem = sourceSystems.FirstOrDefault(x => string.Equals(x.Code, s.SourceSystem, StringComparison.OrdinalIgnoreCase));
return new DashboardRow return new DashboardRow
{ {
SiteId = s.Id, SiteId = s.Id,
Land = s.Land, Land = s.Land,
TSC = s.TSC, TSC = s.TSC,
Schema = s.Schema, Schema = s.Schema,
ServerName = string.Equals(s.SourceSystem, "SAP", StringComparison.OrdinalIgnoreCase) ServerName = string.Equals(sourceSystem?.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase)
? (string.IsNullOrWhiteSpace(s.SapServiceUrl) ? "SAP Gateway" : s.SapServiceUrl) ? ResolveDashboardSapServiceUrl(s, sourceSystems)
: s.HanaServer?.Name ?? "", : s.HanaServer?.Name ?? "",
LastStatus = log?.Status ?? "", LastStatus = log?.Status ?? "",
RowCount = log?.RowCount ?? 0, RowCount = log?.RowCount ?? 0,
@@ -319,6 +321,15 @@
OpenFile(row.FilePath); 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) private void OpenFile(string filePath)
{ {
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath)) if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
@@ -1,4 +1,4 @@
@page "/settings" @page "/settings"
@using Microsoft.EntityFrameworkCore @using Microsoft.EntityFrameworkCore
@using TrafagSalesExporter.Data @using TrafagSalesExporter.Data
@using TrafagSalesExporter.Models @using TrafagSalesExporter.Models
@@ -23,7 +23,7 @@
<MudItem xs="12" md="6"> <MudItem xs="12" md="6">
<MudCheckBox @bind-Value="_includeSecretsInExport" Label="Mit Secrets exportieren" /> <MudCheckBox @bind-Value="_includeSecretsInExport" Label="Mit Secrets exportieren" />
<MudText Typo="Typo.caption"> <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> </MudText>
</MudItem> </MudItem>
<MudItem xs="12" md="6"> <MudItem xs="12" md="6">
@@ -98,74 +98,102 @@
</MudGrid> </MudGrid>
</MudPaper> </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"> <MudPaper Class="pa-4 mb-6" Elevation="1">
<MudGrid> <MudGrid>
<MudItem xs="12"> <MudItem xs="12">
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined"> <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> </MudAlert>
</MudItem> </MudItem>
<MudItem xs="12" md="4"> <MudItem xs="12">
<MudText Typo="Typo.h6" Class="mb-2">SAP</MudText> <MudButton Variant="Variant.Outlined" Color="Color.Primary" OnClick="AddSourceSystem"
<MudTextField @bind-Value="_exportSettings.SapUsername" Label="SAP Username" /> StartIcon="@Icons.Material.Filled.Add" Class="mb-3">
<MudTextField @bind-Value="_exportSettings.SapPassword" Label="SAP Password" InputType="InputType.Password" /> Quellsystem hinzufuegen
<MudButton Variant="Variant.Outlined" Color="Color.Info" OnClick='@(() => TestCentralCredentials("SAP"))' </MudButton>
StartIcon="@Icons.Material.Filled.NetworkCheck" Disabled='@_testingSystems.Contains("SAP")' Class="mt-2"> <MudTable Items="_sourceSystems" Dense Hover Striped Breakpoint="Breakpoint.Md">
@if (_testingSystems.Contains("SAP")) <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)
{ {
<MudProgressCircular Size="Size.Small" Indeterminate Class="mr-2" /> <MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" />
@("Teste...")
} }
else else
{ {
@("SAP testen") <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> </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 </MudTd>
{ <MudTd>
@("BI1 testen") <MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Primary" Size="Size.Small"
} OnClick="() => EditSourceSystem(context)" />
</MudButton> <MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small"
</MudItem> OnClick="() => RemoveSourceSystem(context)" />
<MudItem xs="12" md="4"> </MudTd>
<MudText Typo="Typo.h6" Class="mb-2">SAGE</MudText> </RowTemplate>
<MudTextField @bind-Value="_exportSettings.SageUsername" Label="SAGE Username" /> </MudTable>
<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")
}
</MudButton>
</MudItem> </MudItem>
<MudItem xs="12"> <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"> StartIcon="@Icons.Material.Filled.Save">
Speichern Quellsysteme speichern
</MudButton> </MudButton>
</MudItem> </MudItem>
</MudGrid> </MudGrid>
</MudPaper> </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> <MudText Typo="Typo.h5" Class="mb-2">Wechselkurse</MudText>
<MudPaper Class="pa-4 mb-6" Elevation="1"> <MudPaper Class="pa-4 mb-6" Elevation="1">
<MudText Typo="Typo.body2" Class="mb-3"> <MudText Typo="Typo.body2" Class="mb-3">
@@ -250,7 +278,7 @@
<MudItem xs="12" md="4"> <MudItem xs="12" md="4">
<MudSwitch @bind-Value="_exportSettings.DebugLoggingEnabled" Label="Debug Live-Logging" Color="Color.Warning" /> <MudSwitch @bind-Value="_exportSettings.DebugLoggingEnabled" Label="Debug Live-Logging" Color="Color.Warning" />
<MudText Typo="Typo.caption"> <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> </MudText>
</MudItem> </MudItem>
<MudItem xs="12" md="6"> <MudItem xs="12" md="6">
@@ -285,6 +313,8 @@
@code { @code {
private SharePointConfig _spConfig = new(); private SharePointConfig _spConfig = new();
private ExportSettings _exportSettings = new(); private ExportSettings _exportSettings = new();
private List<SourceSystemDefinition> _sourceSystems = [];
private SourceSystemDefinition _editingSourceSystem = new();
private bool _testingSp; private bool _testingSp;
private bool _includeSecretsInExport; private bool _includeSecretsInExport;
private bool _exportingConfig; private bool _exportingConfig;
@@ -293,12 +323,15 @@
private string _sharePointTestPreview = string.Empty; private string _sharePointTestPreview = string.Empty;
private List<CurrencyExchangeRate> _exchangeRates = []; private List<CurrencyExchangeRate> _exchangeRates = [];
private readonly HashSet<string> _testingSystems = []; private readonly HashSet<string> _testingSystems = [];
private bool _sourceSystemDialogVisible;
private readonly DialogOptions _sourceSystemDialogOptions = new() { MaxWidth = MaxWidth.Small, FullWidth = true };
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
using var db = await DbFactory.CreateDbContextAsync(); using var db = await DbFactory.CreateDbContextAsync();
_spConfig = await db.SharePointConfigs.FirstOrDefaultAsync() ?? new SharePointConfig(); _spConfig = await db.SharePointConfigs.FirstOrDefaultAsync() ?? new SharePointConfig();
_exportSettings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings(); _exportSettings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings();
_sourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync();
_exchangeRates = await db.CurrencyExchangeRates _exchangeRates = await db.CurrencyExchangeRates
.OrderBy(x => x.FromCurrency) .OrderBy(x => x.FromCurrency)
.ThenBy(x => x.ToCurrency) .ThenBy(x => x.ToCurrency)
@@ -370,18 +403,138 @@
existing.DebugLoggingEnabled = _exportSettings.DebugLoggingEnabled; existing.DebugLoggingEnabled = _exportSettings.DebugLoggingEnabled;
existing.LocalSiteExportFolder = _exportSettings.LocalSiteExportFolder; existing.LocalSiteExportFolder = _exportSettings.LocalSiteExportFolder;
existing.LocalConsolidatedExportFolder = _exportSettings.LocalConsolidatedExportFolder; 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(); await db.SaveChangesAsync();
TimerService.Recalculate(); TimerService.Recalculate();
Snackbar.Add("Export Einstellungen gespeichert", Severity.Success); 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() private void AddExchangeRate()
{ {
_exchangeRates.Add(new CurrencyExchangeRate _exchangeRates.Add(new CurrencyExchangeRate
@@ -493,6 +646,7 @@
using var db = await DbFactory.CreateDbContextAsync(); using var db = await DbFactory.CreateDbContextAsync();
_spConfig = await db.SharePointConfigs.FirstOrDefaultAsync() ?? new SharePointConfig(); _spConfig = await db.SharePointConfigs.FirstOrDefaultAsync() ?? new SharePointConfig();
_exportSettings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings(); _exportSettings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings();
_sourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync();
_exchangeRates = await db.CurrencyExchangeRates _exchangeRates = await db.CurrencyExchangeRates
.OrderBy(x => x.FromCurrency) .OrderBy(x => x.FromCurrency)
.ThenBy(x => x.ToCurrency) .ThenBy(x => x.ToCurrency)
@@ -513,61 +667,72 @@
private async Task TestCentralCredentials(string sourceSystem) 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; return;
} }
await TestCentralHanaCredentials(sourceSystem); if (string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase))
{
await TestCentralSapCredentials(definition);
return;
} }
private async Task TestCentralHanaCredentials(string sourceSystem) if (string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.Hana, StringComparison.OrdinalIgnoreCase))
{ {
await TestCentralHanaCredentials(definition);
}
}
private async Task TestCentralHanaCredentials(SourceSystemDefinition definition)
{
var sourceSystem = definition.Code;
if (!_testingSystems.Add(sourceSystem)) if (!_testingSystems.Add(sourceSystem))
return; return;
try try
{ {
var username = GetCentralUsername(sourceSystem); var username = definition.CentralUsername;
var password = GetCentralPassword(sourceSystem); var password = definition.CentralPassword;
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) 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; return;
} }
using var db = await DbFactory.CreateDbContextAsync(); using var db = await DbFactory.CreateDbContextAsync();
var site = await db.Sites var centralServer = await db.HanaServers
.Include(s => s.HanaServer) .Where(s => s.SourceSystem == sourceSystem)
.Where(s => (string.IsNullOrWhiteSpace(s.SourceSystem) ? "SAP" : s.SourceSystem) == sourceSystem) .OrderBy(s => s.Id)
.OrderBy(s => s.Land)
.FirstOrDefaultAsync(); .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; return;
} }
var testServer = new HanaServer var testServer = new HanaServer
{ {
SourceSystem = sourceSystem,
Name = $"{sourceSystem} Central Test", Name = $"{sourceSystem} Central Test",
Host = site.HanaServer.Host, Host = centralServer.Host,
Port = site.HanaServer.Port, Port = centralServer.Port,
Username = username.Trim(), Username = username.Trim(),
Password = password.Trim(), Password = password.Trim(),
DatabaseName = site.HanaServer.DatabaseName, DatabaseName = centralServer.DatabaseName,
UseSsl = site.HanaServer.UseSsl, UseSsl = centralServer.UseSsl,
ValidateCertificate = site.HanaServer.ValidateCertificate, ValidateCertificate = centralServer.ValidateCertificate,
AdditionalParams = site.HanaServer.AdditionalParams AdditionalParams = centralServer.AdditionalParams
}; };
var result = await Task.Run(() => HanaService.TestConnectionDetailed(testServer)); var result = await Task.Run(() => HanaService.TestConnectionDetailed(testServer));
if (result.Success) if (result.Success)
{ {
Snackbar.Add($"{sourceSystem}: Verbindung erfolgreich über Standort '{site.Land}'.", Severity.Success); Snackbar.Add($"{sourceSystem}: Zentrale HANA-Verbindung erfolgreich.", Severity.Success);
} }
else 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)) if (!_testingSystems.Add(sourceSystem))
return; return;
try try
{ {
var username = GetCentralUsername(sourceSystem); var username = definition.CentralUsername;
var password = GetCentralPassword(sourceSystem); var password = definition.CentralPassword;
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) 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; return;
} }
using var db = await DbFactory.CreateDbContextAsync(); if (string.IsNullOrWhiteSpace(definition.CentralServiceUrl))
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); Snackbar.Add($"Fuer {sourceSystem} ist keine zentrale SAP Service URL gepflegt.", Severity.Warning);
return; return;
} }
await SapGatewayService.TestConnectionAsync(site.SapServiceUrl, username.Trim(), password.Trim()); await SapGatewayService.TestConnectionAsync(definition.CentralServiceUrl, username.Trim(), password.Trim());
Snackbar.Add($"SAP: Gateway-Verbindung erfolgreich über Standort '{site.Land}'.", Severity.Success); Snackbar.Add($"{sourceSystem}: Zentrale SAP Gateway-Verbindung erfolgreich.", Severity.Success);
} }
catch (Exception ex) catch (Exception ex)
{ {
Snackbar.Add($"SAP: {ex.Message}", Severity.Error); Snackbar.Add($"{sourceSystem}: {ex.Message}", Severity.Error);
} }
finally 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, SourceSystemConnectionKinds.Hana => "HANA",
"SAGE" => _exportSettings.SageUsername, SourceSystemConnectionKinds.SapGateway => "SAP Gateway",
_ => _exportSettings.SapUsername SourceSystemConnectionKinds.ManualExcel => "Manual Excel",
_ => connectionKind
}; };
private string GetCentralPassword(string sourceSystem) => sourceSystem switch private static bool UsesManualImport(SourceSystemDefinition definition)
{ => string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.ManualExcel, StringComparison.OrdinalIgnoreCase);
"BI1" => _exportSettings.Bi1Password,
"SAGE" => _exportSettings.SagePassword, private static bool UsesSapGateway(SourceSystemDefinition definition)
_ => _exportSettings.SapPassword => 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; private static string NormalizeConfigValue(string? value) => value?.Trim() ?? string.Empty;
@@ -664,3 +835,4 @@
.ToListAsync(); .ToListAsync();
} }
} }
@@ -1,4 +1,4 @@
@page "/standorte" @page "/standorte"
@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Forms
@using Microsoft.EntityFrameworkCore @using Microsoft.EntityFrameworkCore
@using System.Text.Json @using System.Text.Json
@@ -17,27 +17,30 @@
<MudText Typo="Typo.h4" Class="mb-4">Standorte</MudText> <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"> <MudPaper Class="pa-4 mb-6" Elevation="1">
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" <MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Class="mb-3">
OnClick="AddServer" Class="mb-3"> Hier erscheinen nur Quellsysteme mit Anschlussart HANA. SAP wird zentral unter Settings -> Quellsysteme gepflegt.
Server hinzufügen Standorte mit `BI1` oder `SAGE` verwenden diese technischen HANA-Werte automatisch. Im Standort selbst bleiben nur Schema, TSC, Land und optionale Username-/Password-Overrides.
</MudButton> </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> <MudTable Items="_servers" Dense Hover Striped>
<HeaderContent> <HeaderContent>
<MudTh>Quellsystem</MudTh>
<MudTh>Name</MudTh> <MudTh>Name</MudTh>
<MudTh>Host</MudTh> <MudTh>Host</MudTh>
<MudTh>Port</MudTh> <MudTh>Port</MudTh>
<MudTh>Username</MudTh>
<MudTh>Verbindungsstatus</MudTh> <MudTh>Verbindungsstatus</MudTh>
<MudTh>Aktionen</MudTh> <MudTh>Aktionen</MudTh>
</HeaderContent> </HeaderContent>
<RowTemplate> <RowTemplate>
<MudTd>@context.SourceSystem</MudTd>
<MudTd>@context.Name</MudTd> <MudTd>@context.Name</MudTd>
<MudTd>@context.Host</MudTd> <MudTd>@context.Host</MudTd>
<MudTd>@context.Port</MudTd> <MudTd>@context.Port</MudTd>
<MudTd>@context.Username</MudTd>
<MudTd> <MudTd>
@if (_connectionStatus.TryGetValue(context.Id, out var status)) @if (_connectionStatus.TryGetValue(context.Id, out var status))
{ {
@@ -68,7 +71,7 @@
<MudPaper Class="pa-4" Elevation="1"> <MudPaper Class="pa-4" Elevation="1">
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" <MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add"
OnClick="AddSite" Class="mb-3"> OnClick="AddSite" Class="mb-3">
Neuen Standort hinzufügen Neuen Standort hinzufügen
</MudButton> </MudButton>
<MudTable Items="_sites" Dense Hover Striped> <MudTable Items="_sites" Dense Hover Striped>
@@ -109,22 +112,21 @@
<MudDialog @bind-Visible="_serverDialogVisible" Options="_dialogOptions"> <MudDialog @bind-Visible="_serverDialogVisible" Options="_dialogOptions">
<TitleContent> <TitleContent>
<MudText Typo="Typo.h6">@(_editingServer.Id == 0 ? "Server hinzufügen" : "Server bearbeiten")</MudText> <MudText Typo="Typo.h6">Zentrale HANA-Technik bearbeiten</MudText>
</TitleContent> </TitleContent>
<DialogContent> <DialogContent>
<MudTextField Value="_editingServer.SourceSystem" Label="Quellsystem" ReadOnly />
<MudTextField @bind-Value="_editingServer.Name" Label="Name" Required /> <MudTextField @bind-Value="_editingServer.Name" Label="Name" Required />
<MudTextField @bind-Value="_editingServer.Host" Label="Host" Required <MudTextField @bind-Value="_editingServer.Host" Label="Host" Required
HelperText="IP oder Hostname (ohne Protokoll)" /> HelperText="IP oder Hostname (ohne Protokoll)" />
<MudNumericField @bind-Value="_editingServer.Port" Label="Port" <MudNumericField @bind-Value="_editingServer.Port" Label="Port"
HelperText="Typisch 30015 (Tenant), 30013 (SystemDB), 3xx15 für Instanz xx" /> 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" />
<MudTextField @bind-Value="_editingServer.DatabaseName" Label="Database Name (MDC)" <MudTextField @bind-Value="_editingServer.DatabaseName" Label="Database Name (MDC)"
HelperText="Nur bei Multi-Tenant Setup angeben, sonst leer lassen" /> 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.UseSsl" Label="SSL/TLS verwenden (encrypt=true)" Color="Color.Primary" />
<MudSwitch @bind-Value="_editingServer.ValidateCertificate" Label="SSL-Zertifikat validieren" Color="Color.Primary" <MudSwitch @bind-Value="_editingServer.ValidateCertificate" Label="SSL-Zertifikat validieren" Color="Color.Primary"
Disabled="!_editingServer.UseSsl" /> 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" /> HelperText="Optional, z.B. sslCryptoProvider=openssl;communicationTimeout=0" />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
@@ -135,16 +137,16 @@
<MudDialog @bind-Visible="_siteDialogVisible" Options="_dialogOptions"> <MudDialog @bind-Visible="_siteDialogVisible" Options="_dialogOptions">
<TitleContent> <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> </TitleContent>
<DialogContent> <DialogContent>
<MudTextField @bind-Value="_editingSite.Schema" Label="Schema" Required /> <MudTextField @bind-Value="_editingSite.Schema" Label="Schema" Required />
<MudTextField @bind-Value="_editingSite.TSC" Label="TSC" Required /> <MudTextField @bind-Value="_editingSite.TSC" Label="TSC" Required />
<MudTextField @bind-Value="_editingSite.Land" Label="Land" Required /> <MudTextField @bind-Value="_editingSite.Land" Label="Land" Required />
<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 GetAvailableSourceSystems())
{ {
<MudSelectItem Value="@system">@system</MudSelectItem> <MudSelectItem Value="@system.Code">@GetSourceSystemLabel(system)</MudSelectItem>
} }
</MudSelect> </MudSelect>
<MudTextField @bind-Value="_editingSite.UsernameOverride" Label="Username Override" <MudTextField @bind-Value="_editingSite.UsernameOverride" Label="Username Override"
@@ -152,7 +154,7 @@
<MudTextField @bind-Value="_editingSite.PasswordOverride" Label="Password Override" InputType="InputType.Password" <MudTextField @bind-Value="_editingSite.PasswordOverride" Label="Password Override" InputType="InputType.Password"
HelperText="Optional. Wenn leer, wird das zentrale Passwort des Quellsystems verwendet." /> HelperText="Optional. Wenn leer, wird das zentrale Passwort des Quellsystems verwendet." />
<MudTextField @bind-Value="_editingSite.LocalExportFolderOverride" Label="Lokaler Exportpfad Override" <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" /> <MudCheckBox @bind-Value="_editingSite.IsActive" Label="Aktiv" />
<MudDivider Class="my-4" /> <MudDivider Class="my-4" />
@@ -161,10 +163,11 @@
{ {
<MudText Typo="Typo.h6" Class="mb-2">SAP Gateway</MudText> <MudText Typo="Typo.h6" Class="mb-2">SAP Gateway</MudText>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Class="mb-3"> <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> </MudAlert>
<MudTextField @bind-Value="_editingSite.SapServiceUrl" Label="SAP Service URL" Required <MudText Typo="Typo.body2">Zentrale SAP Service URL: @GetCentralSapServiceUrlSummary(_editingSite.SourceSystem)</MudText>
HelperText="z.B. http://server:8000/sap/opu/odata/sap/ZPOWERBI_EINKAUF_SRV/" /> <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"> <MudStack Row Spacing="2" Class="mb-3">
<MudButton Variant="Variant.Outlined" Color="Color.Info" OnClick="RefreshSapEntitySets" <MudButton Variant="Variant.Outlined" Color="Color.Info" OnClick="RefreshSapEntitySets"
StartIcon="@Icons.Material.Filled.Refresh" Disabled="_refreshingSapEntitySets"> StartIcon="@Icons.Material.Filled.Refresh" Disabled="_refreshingSapEntitySets">
@@ -188,16 +191,16 @@
<MudDivider Class="my-4" /> <MudDivider Class="my-4" />
<MudStack Row Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-2"> <MudStack Row Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-2">
<MudText Typo="Typo.h6">SAP Quellen</MudText> <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> </MudStack>
<MudText Typo="Typo.caption" Class="mb-2"> <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> </MudText>
<MudTable Items="_sapSources" Dense Hover Striped> <MudTable Items="_sapSources" Dense Hover Striped>
<HeaderContent> <HeaderContent>
<MudTh>Alias</MudTh> <MudTh>Alias</MudTh>
<MudTh>Entity Set</MudTh> <MudTh>Entity Set</MudTh>
<MudTh>Primär</MudTh> <MudTh>Primär</MudTh>
<MudTh>Aktiv</MudTh> <MudTh>Aktiv</MudTh>
<MudTh>Aktionen</MudTh> <MudTh>Aktionen</MudTh>
</HeaderContent> </HeaderContent>
@@ -225,7 +228,7 @@
OnClick="AutoMatchSapJoins"> OnClick="AutoMatchSapJoins">
Auto-Match Auto-Match
</MudButton> </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>
</MudStack> </MudStack>
<MudTable Items="_sapJoins" Dense Hover Striped> <MudTable Items="_sapJoins" Dense Hover Striped>
@@ -305,11 +308,11 @@
@("Felder aus Quellen laden") @("Felder aus Quellen laden")
} }
</MudButton> </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>
</MudStack> </MudStack>
<MudText Typo="Typo.caption" Class="mb-2"> <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> </MudText>
<MudTable Items="_sapMappings" Dense Hover Striped> <MudTable Items="_sapMappings" Dense Hover Striped>
<HeaderContent> <HeaderContent>
@@ -346,7 +349,7 @@
{ {
<MudText Typo="Typo.h6" Class="mb-2">Manueller Excel-Import</MudText> <MudText Typo="Typo.h6" Class="mb-2">Manueller Excel-Import</MudText>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Class="mb-3"> <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> </MudAlert>
<InputFile OnChange="UploadManualImportFileAsync" accept=".xlsx" /> <InputFile OnChange="UploadManualImportFileAsync" accept=".xlsx" />
@if (_uploadingManualImport) @if (_uploadingManualImport)
@@ -371,23 +374,12 @@
{ {
<MudText Typo="Typo.h6" Class="mb-2">HANA-Verbindung</MudText> <MudText Typo="Typo.h6" Class="mb-2">HANA-Verbindung</MudText>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Class="mb-3"> <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> </MudAlert>
<MudTextField @bind-Value="_editingSiteServer.Name" Label="Verbindungsname" Required <MudText Typo="Typo.body2">Aktive Zentralverbindung: @GetCentralHanaSummary(_editingSite.SourceSystem)</MudText>
HelperText="Interner Anzeigename für diesen Standort" /> <MudText Typo="Typo.caption" Class="mt-2">
<MudTextField @bind-Value="_editingSiteServer.Host" Label="Host oder ServerNode" Required Host, Port, SSL und technische Parameter bearbeitest du oben in der zentralen HANA-Konfiguration.
HelperText="z.B. hana01 oder hana01:30015 oder derselbe HanaServer-Wert wie in Power BI" /> </MudText>
<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>
@@ -397,10 +389,10 @@
</MudDialog> </MudDialog>
@code { @code {
private readonly string[] _sourceSystems = ["SAP", "BI1", "SAGE", "MANUAL_EXCEL"];
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<SourceSystemDefinition> _sourceSystemDefinitions = new();
private List<string> _sapEntitySetsCache = []; private List<string> _sapEntitySetsCache = [];
private List<string> _sapAvailableSourceExpressions = []; private List<string> _sapAvailableSourceExpressions = [];
private Dictionary<string, List<string>> _sapSourceFieldMap = new(StringComparer.OrdinalIgnoreCase); private Dictionary<string, List<string>> _sapSourceFieldMap = new(StringComparer.OrdinalIgnoreCase);
@@ -413,7 +405,6 @@
.ToArray(); .ToArray();
private HanaServer _editingServer = new(); private HanaServer _editingServer = new();
private Site _editingSite = new(); private Site _editingSite = new();
private HanaServer _editingSiteServer = new();
private bool _serverDialogVisible; private bool _serverDialogVisible;
private bool _siteDialogVisible; private bool _siteDialogVisible;
private bool _refreshingSapEntitySets; private bool _refreshingSapEntitySets;
@@ -431,16 +422,17 @@
private async Task LoadDataAsync() private async Task LoadDataAsync()
{ {
using var db = await DbFactory.CreateDbContextAsync(); 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(); _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) private void EditServer(HanaServer server)
{ {
_editingServer = CloneServer(server); _editingServer = CloneServer(server);
@@ -455,21 +447,50 @@
_savingServer = true; _savingServer = true;
try 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(); using var db = await DbFactory.CreateDbContextAsync();
if (_editingServer.Id == 0) if (_editingServer.Id == 0)
{
var existingForSourceSystem = await db.HanaServers
.OrderBy(x => x.Id)
.FirstOrDefaultAsync(x => x.SourceSystem == _editingServer.SourceSystem);
if (existingForSourceSystem is null)
{ {
db.HanaServers.Add(_editingServer); db.HanaServers.Add(_editingServer);
} }
else 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); var existing = await db.HanaServers.FindAsync(_editingServer.Id);
if (existing is not null) if (existing is not null)
{ {
existing.SourceSystem = _editingServer.SourceSystem;
existing.Name = _editingServer.Name; existing.Name = _editingServer.Name;
existing.Host = _editingServer.Host; existing.Host = _editingServer.Host;
existing.Port = _editingServer.Port; existing.Port = _editingServer.Port;
existing.Username = _editingServer.Username; existing.Username = string.Empty;
existing.Password = _editingServer.Password; existing.Password = string.Empty;
existing.DatabaseName = _editingServer.DatabaseName; existing.DatabaseName = _editingServer.DatabaseName;
existing.UseSsl = _editingServer.UseSsl; existing.UseSsl = _editingServer.UseSsl;
existing.ValidateCertificate = _editingServer.ValidateCertificate; existing.ValidateCertificate = _editingServer.ValidateCertificate;
@@ -490,10 +511,16 @@
private async Task DeleteServer(HanaServer server) 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( var result = await DialogService.ShowMessageBox(
"Server löschen", "Server löschen",
$"Server '{server.Name}' wirklich löschen?", $"Server '{server.Name}' wirklich löschen?",
yesText: "Löschen", cancelText: "Abbrechen"); yesText: "Löschen", cancelText: "Abbrechen");
if (result != true) return; if (result != true) return;
@@ -509,7 +536,7 @@
if (linkedSites.Count > 0) if (linkedSites.Count > 0)
{ {
Snackbar.Add( 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); Severity.Warning);
return; return;
} }
@@ -523,19 +550,51 @@
} }
catch (Exception ex) 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; return;
} }
await LoadDataAsync(); await LoadDataAsync();
Snackbar.Add("Server gelöscht", Severity.Info); Snackbar.Add("Server gelöscht", Severity.Info);
} }
private async Task TestServerConnection(HanaServer server) 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", await AppEventLogService.WriteAsync("HANA", "Server-Test aus UI gestartet",
details: server.GetConnectionStringPreview()); details: testServer.GetConnectionStringPreview());
var result = await Task.Run(() => HanaService.TestConnectionDetailed(server)); var result = await Task.Run(() => HanaService.TestConnectionDetailed(testServer));
_connectionStatus[server.Id] = result; _connectionStatus[server.Id] = result;
if (result.Success) if (result.Success)
@@ -562,7 +621,7 @@
_editingSite = new Site _editingSite = new Site
{ {
IsActive = true, IsActive = true,
SourceSystem = "SAP", SourceSystem = GetAvailableSourceSystems().FirstOrDefault()?.Code ?? "SAP",
HanaServerId = null, HanaServerId = null,
ManualImportFilePath = string.Empty ManualImportFilePath = string.Empty
}; };
@@ -572,7 +631,6 @@
_sapSources = []; _sapSources = [];
_sapJoins = []; _sapJoins = [];
_sapMappings = []; _sapMappings = [];
_editingSiteServer = CreateDefaultSiteServer();
_siteDialogVisible = true; _siteDialogVisible = true;
} }
@@ -585,7 +643,9 @@
Schema = site.Schema, Schema = site.Schema,
TSC = site.TSC, TSC = site.TSC,
Land = site.Land, 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, UsernameOverride = site.UsernameOverride,
PasswordOverride = site.PasswordOverride, PasswordOverride = site.PasswordOverride,
LocalExportFolderOverride = site.LocalExportFolderOverride, 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(); _sapMappings = db.SapFieldMappings.Where(m => m.SiteId == site.Id).OrderBy(m => m.SortOrder).ThenBy(m => m.Id).ToList();
_sapAvailableSourceExpressions = BuildSourceExpressionsFromMappings(); _sapAvailableSourceExpressions = BuildSourceExpressionsFromMappings();
_sapSourceFieldMap = BuildSourceFieldMapFromJoins(); _sapSourceFieldMap = BuildSourceFieldMapFromJoins();
_editingSiteServer = site.HanaServer is null
? CreateDefaultSiteServer(site)
: CloneServer(site.HanaServer);
_siteDialogVisible = true; _siteDialogVisible = true;
} }
@@ -619,7 +676,7 @@
try try
{ {
using var db = await DbFactory.CreateDbContextAsync(); 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.HanaServerId = serverId;
_editingSite.SapEntitySetsCache = SerializeSapEntitySets(_sapEntitySetsCache); _editingSite.SapEntitySetsCache = SerializeSapEntitySets(_sapEntitySetsCache);
@@ -669,9 +726,9 @@
private async Task DeleteSite(Site site) private async Task DeleteSite(Site site)
{ {
var result = await DialogService.ShowMessageBox( var result = await DialogService.ShowMessageBox(
"Standort löschen", "Standort löschen",
$"Standort '{site.Land}' wirklich löschen?", $"Standort '{site.Land}' wirklich löschen?",
yesText: "Löschen", cancelText: "Abbrechen"); yesText: "Löschen", cancelText: "Abbrechen");
if (result != true) return; if (result != true) return;
@@ -692,7 +749,7 @@
} }
await LoadDataAsync(); await LoadDataAsync();
Snackbar.Add("Standort gelöscht", Severity.Info); Snackbar.Add("Standort gelöscht", Severity.Info);
} }
private static string GetServerNode(HanaServer? server) private static string GetServerNode(HanaServer? server)
@@ -703,40 +760,17 @@
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;
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) private static HanaServer CloneServer(HanaServer server)
{ {
return new HanaServer return new HanaServer
{ {
Id = server.Id, Id = server.Id,
SourceSystem = server.SourceSystem,
Name = server.Name, Name = server.Name,
Host = server.Host, Host = server.Host,
Port = server.Port, Port = server.Port,
Username = server.Username, Username = string.Empty,
Password = server.Password, Password = string.Empty,
DatabaseName = server.DatabaseName, DatabaseName = server.DatabaseName,
UseSsl = server.UseSsl, UseSsl = server.UseSsl,
ValidateCertificate = server.ValidateCertificate, 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.UsernameOverride = _editingSite.UsernameOverride.Trim();
_editingSite.PasswordOverride = _editingSite.PasswordOverride.Trim(); _editingSite.PasswordOverride = _editingSite.PasswordOverride.Trim();
_editingSite.LocalExportFolderOverride = _editingSite.LocalExportFolderOverride.Trim(); _editingSite.LocalExportFolderOverride = _editingSite.LocalExportFolderOverride.Trim();
_editingSite.ManualImportFilePath = _editingSite.ManualImportFilePath.Trim(); _editingSite.ManualImportFilePath = _editingSite.ManualImportFilePath.Trim();
_editingSite.SapServiceUrl = _editingSite.SapServiceUrl.Trim(); _editingSite.SapServiceUrl = _editingSite.SapServiceUrl.Trim();
_editingSite.SapEntitySet = _editingSite.SapEntitySet.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)) var normalizedSourceSystem = string.IsNullOrWhiteSpace(sourceSystem) ? string.Empty : sourceSystem.Trim().ToUpperInvariant();
throw new InvalidOperationException("Host oder ServerNode muss gesetzt sein."); var centralServer = await db.HanaServers
.OrderBy(x => x.Id)
.FirstOrDefaultAsync(x => x.SourceSystem == normalizedSourceSystem);
if (_editingSite.HanaServerId == 0) if (centralServer is null || string.IsNullOrWhiteSpace(centralServer.Host))
throw new InvalidOperationException($"Fuer Quellsystem '{normalizedSourceSystem}' ist keine gueltige zentrale HANA-Konfiguration vorhanden.");
return centralServer.Id;
}
private 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)
{ {
db.HanaServers.Add(_editingSiteServer); var connectionKind = GetSourceSystemConnectionKind(site.SourceSystem);
await db.SaveChangesAsync(); if (string.Equals(connectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase))
return _editingSiteServer.Id; return GetEffectiveSapServiceUrl(site);
if (string.Equals(connectionKind, SourceSystemConnectionKinds.ManualExcel, StringComparison.OrdinalIgnoreCase))
return string.IsNullOrWhiteSpace(site.ManualImportFilePath) ? "-" : Path.GetFileName(site.ManualImportFilePath);
return GetServerNode(site.HanaServer);
} }
var sharedUseCount = await db.Sites.CountAsync(s => s.HanaServerId == _editingSite.HanaServerId && s.Id != _editingSite.Id); private string GetEffectiveSapServiceUrl(Site site)
if (sharedUseCount > 0)
{ {
var dedicatedServer = CloneServer(_editingSiteServer); if (!string.IsNullOrWhiteSpace(site.SapServiceUrl))
dedicatedServer.Id = 0; return site.SapServiceUrl;
db.HanaServers.Add(dedicatedServer);
await db.SaveChangesAsync(); var sourceDefinition = _sourceSystemDefinitions
return dedicatedServer.Id; .FirstOrDefault(x => string.Equals(x.Code, site.SourceSystem, StringComparison.OrdinalIgnoreCase));
return string.IsNullOrWhiteSpace(sourceDefinition?.CentralServiceUrl) ? "-" : sourceDefinition.CentralServiceUrl;
} }
var existingServer = await db.HanaServers.FindAsync(_editingSite.HanaServerId); private string GetCentralSapServiceUrlSummary(string sourceSystem)
if (existingServer is null)
{ {
db.HanaServers.Add(_editingSiteServer); var sourceDefinition = _sourceSystemDefinitions
await db.SaveChangesAsync(); .FirstOrDefault(x => string.Equals(x.Code, sourceSystem, StringComparison.OrdinalIgnoreCase));
return _editingSiteServer.Id;
return string.IsNullOrWhiteSpace(sourceDefinition?.CentralServiceUrl) ? "-" : sourceDefinition.CentralServiceUrl;
} }
existingServer.Name = _editingSiteServer.Name; private string GetCentralHanaSummary(string sourceSystem)
existingServer.Host = _editingSiteServer.Host; {
existingServer.Port = _editingSiteServer.Port; var normalizedSourceSystem = string.IsNullOrWhiteSpace(sourceSystem) ? string.Empty : sourceSystem.Trim().ToUpperInvariant();
existingServer.Username = _editingSiteServer.Username; var centralServer = _servers.FirstOrDefault(x => x.SourceSystem == normalizedSourceSystem);
existingServer.Password = _editingSiteServer.Password; if (centralServer is null || string.IsNullOrWhiteSpace(centralServer.Host))
existingServer.DatabaseName = _editingSiteServer.DatabaseName; return $"keine zentrale HANA-Konfiguration fuer {normalizedSourceSystem}";
existingServer.UseSsl = _editingSiteServer.UseSsl;
existingServer.ValidateCertificate = _editingSiteServer.ValidateCertificate;
existingServer.AdditionalParams = _editingSiteServer.AdditionalParams;
await db.SaveChangesAsync();
return existingServer.Id;
}
private bool IsSapSite() => string.Equals(_editingSite.SourceSystem, "SAP", StringComparison.OrdinalIgnoreCase); return $"{centralServer.Name} | {GetServerNode(centralServer)}";
private bool IsManualExcelSite() => string.Equals(_editingSite.SourceSystem, "MANUAL_EXCEL", StringComparison.OrdinalIgnoreCase); }
private bool UsesHanaConnection() => !IsSapSite() && !IsManualExcelSite();
private async Task RefreshSapEntitySets() private async Task RefreshSapEntitySets()
{ {
@@ -813,20 +879,28 @@
_refreshingSapEntitySets = true; _refreshingSapEntitySets = true;
try try
{ {
if (string.IsNullOrWhiteSpace(_editingSite.SapServiceUrl))
throw new InvalidOperationException("SAP Service URL muss gesetzt sein.");
using var db = await DbFactory.CreateDbContextAsync(); using var db = await DbFactory.CreateDbContextAsync();
var settings = await db.ExportSettings.FirstOrDefaultAsync() ?? new(); var sourceDefinition = await db.SourceSystemDefinitions
var username = string.IsNullOrWhiteSpace(_editingSite.UsernameOverride) ? settings.SapUsername : _editingSite.UsernameOverride; .OrderBy(x => x.Id)
var password = string.IsNullOrWhiteSpace(_editingSite.PasswordOverride) ? settings.SapPassword : _editingSite.PasswordOverride; .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)) 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, await AppEventLogService.WriteAsync("SAP", "Refresh aus UI gestartet", siteId: _editingSite.Id, land: _editingSite.Land,
details: _editingSite.SapServiceUrl); details: serviceUrl);
var entitySets = await SapGatewayService.GetEntitySetsAsync(_editingSite.SapServiceUrl, username.Trim(), password.Trim()); var entitySets = await SapGatewayService.GetEntitySetsAsync(serviceUrl, username.Trim(), password.Trim());
_sapEntitySetsCache = entitySets; _sapEntitySetsCache = entitySets;
_editingSite.SapEntitySetsCache = SerializeSapEntitySets(entitySets); _editingSite.SapEntitySetsCache = SerializeSapEntitySets(entitySets);
_editingSite.SapEntitySetsRefreshedAtUtc = DateTime.UtcNow; _editingSite.SapEntitySetsRefreshedAtUtc = DateTime.UtcNow;
@@ -884,7 +958,7 @@
var extension = Path.GetExtension(file.Name); var extension = Path.GetExtension(file.Name);
if (!string.Equals(extension, ".xlsx", StringComparison.OrdinalIgnoreCase)) 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"); var uploadDirectory = Path.Combine(AppContext.BaseDirectory, "manual-imports");
@@ -974,13 +1048,13 @@
if (activeSources.Count < 2) 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; return;
} }
if (_sapSourceFieldMap.Count == 0) 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; return;
} }
@@ -1038,7 +1112,7 @@
} }
NormalizeSapConfigCollections(); NormalizeSapConfigCollections();
Snackbar.Add($"{createdOrUpdated} Join-Vorschläge gesetzt.", Severity.Success); Snackbar.Add($"{createdOrUpdated} Join-Vorschläge gesetzt.", Severity.Success);
} }
private void RemoveSapJoin(SapJoinDefinition join) private void RemoveSapJoin(SapJoinDefinition join)
@@ -1116,9 +1190,6 @@
_refreshingSapSourceFields = true; _refreshingSapSourceFields = true;
try try
{ {
if (string.IsNullOrWhiteSpace(_editingSite.SapServiceUrl))
throw new InvalidOperationException("SAP Service URL muss gesetzt sein.");
var activeSources = _sapSources var activeSources = _sapSources
.Where(s => s.IsActive && !string.IsNullOrWhiteSpace(s.Alias) && !string.IsNullOrWhiteSpace(s.EntitySet)) .Where(s => s.IsActive && !string.IsNullOrWhiteSpace(s.Alias) && !string.IsNullOrWhiteSpace(s.EntitySet))
.OrderBy(s => s.SortOrder) .OrderBy(s => s.SortOrder)
@@ -1129,18 +1200,29 @@
throw new InvalidOperationException("Es gibt keine aktiven SAP-Quellen mit Alias und Entity Set."); throw new InvalidOperationException("Es gibt keine aktiven SAP-Quellen mit Alias und Entity Set.");
using var db = await DbFactory.CreateDbContextAsync(); using var db = await DbFactory.CreateDbContextAsync();
var settings = await db.ExportSettings.FirstOrDefaultAsync() ?? new(); var sourceDefinition = await db.SourceSystemDefinitions
var username = string.IsNullOrWhiteSpace(_editingSite.UsernameOverride) ? settings.SapUsername : _editingSite.UsernameOverride; .OrderBy(x => x.Id)
var password = string.IsNullOrWhiteSpace(_editingSite.PasswordOverride) ? settings.SapPassword : _editingSite.PasswordOverride; .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)) 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 expressions = new List<string> { "=SAP" };
var sourceFieldMap = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase); var sourceFieldMap = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
foreach (var source in activeSources) 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; sourceFieldMap[source.Alias] = fieldNames;
expressions.AddRange(fieldNames.Select(field => $"{source.Alias}.{field}")); expressions.AddRange(fieldNames.Select(field => $"{source.Alias}.{field}"));
} }
@@ -1251,3 +1333,5 @@
.ToHashSet(StringComparer.OrdinalIgnoreCase) .ToHashSet(StringComparer.OrdinalIgnoreCase)
?? []; ?? [];
} }
@@ -45,9 +45,9 @@
<MudTd><MudCheckBox @bind-Value="context.IsActive" /></MudTd> <MudTd><MudCheckBox @bind-Value="context.IsActive" /></MudTd>
<MudTd> <MudTd>
<MudSelect T="string" Value="@context.SourceSystem" ValueChanged="@(v => context.SourceSystem = v)" Dense> <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> </MudSelect>
</MudTd> </MudTd>
@@ -176,7 +176,6 @@
</MudDialog> </MudDialog>
@code { @code {
private readonly string[] _systems = ["SAP", "BI1", "SAGE", "MANUAL_EXCEL"];
private readonly string[] _ruleScopes = ["Value", "Record"]; private readonly string[] _ruleScopes = ["Value", "Record"];
private readonly string[] _recordFields = typeof(SalesRecord) private readonly string[] _recordFields = typeof(SalesRecord)
.GetProperties(BindingFlags.Public | BindingFlags.Instance) .GetProperties(BindingFlags.Public | BindingFlags.Instance)
@@ -185,6 +184,7 @@
.ToArray(); .ToArray();
private List<FieldTransformationRule> _rules = new(); private List<FieldTransformationRule> _rules = new();
private List<SourceSystemDefinition> _sourceSystems = [];
private IReadOnlyList<TransformationCatalogItem> _catalogItems = []; private IReadOnlyList<TransformationCatalogItem> _catalogItems = [];
private bool _codeDialogVisible; private bool _codeDialogVisible;
private FieldTransformationRule? _selectedRule; private FieldTransformationRule? _selectedRule;
@@ -200,6 +200,7 @@
private async Task LoadAsync() private async Task LoadAsync()
{ {
using var db = await DbFactory.CreateDbContextAsync(); 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(); _rules = await db.FieldTransformationRules.OrderBy(r => r.SortOrder).ThenBy(r => r.Id).ToListAsync();
foreach (var rule in _rules) foreach (var rule in _rules)
@@ -217,7 +218,7 @@
var nextSort = _rules.Count == 0 ? 10 : _rules.Max(r => r.SortOrder) + 10; var nextSort = _rules.Count == 0 ? 10 : _rules.Max(r => r.SortOrder) + 10;
_rules.Add(new FieldTransformationRule _rules.Add(new FieldTransformationRule
{ {
SourceSystem = "SAP", SourceSystem = _sourceSystems.FirstOrDefault(x => x.IsActive)?.Code ?? "SAP",
RuleScope = "Value", RuleScope = "Value",
SourceField = nameof(SalesRecord.Material), SourceField = nameof(SalesRecord.Material),
TargetField = nameof(SalesRecord.Material), TargetField = nameof(SalesRecord.Material),
+1
View File
@@ -8,6 +8,7 @@ public class AppDbContext : DbContext
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<HanaServer> HanaServers => Set<HanaServer>(); public DbSet<HanaServer> HanaServers => Set<HanaServer>();
public DbSet<SourceSystemDefinition> SourceSystemDefinitions => Set<SourceSystemDefinition>();
public DbSet<Site> Sites => Set<Site>(); public DbSet<Site> Sites => Set<Site>();
public DbSet<SharePointConfig> SharePointConfigs => Set<SharePointConfig>(); public DbSet<SharePointConfig> SharePointConfigs => Set<SharePointConfig>();
public DbSet<ExportSettings> ExportSettings => Set<ExportSettings>(); public DbSet<ExportSettings> ExportSettings => Set<ExportSettings>();
+462
View File
@@ -43,6 +43,426 @@ Ergebnis:
- bekannte Warnung bleibt: - bekannte Warnung bleibt:
- SAP HANA Architekturwarnung `MSB3270` - SAP HANA Architekturwarnung `MSB3270`
## Architekturpruefung 2026-04-17
Es wurde eine erneute Gesamtpruefung der Architektur gemacht, ausdruecklich ohne neue Implementierung.
### Gesamturteil
Die Grundrichtung ist weiterhin sinnvoll:
- klare Trennung der Quellsysteme `SAP`, `BI1`, `SAGE`, `MANUAL_EXCEL`
- zentrales fachliches Zielschema ueber `SalesRecord`
- zentrale technische Ablage ueber `CentralSalesRecords`
- separater Orchestrator fuer Standort- und Konsolidierungsexport
- Transformationssystem als eigener Layer
Aber:
- die Architektur ist **noch nicht stabil genug**, um sie als "fertig sauber" zu betrachten
- die groessten Risiken liegen aktuell nicht in SAP oder Waehrungen, sondern in
- Start-/Schema-Initialisierung
- Config-Import
- Verteilung von Logik zwischen Razor-Seiten und Services
### Wichtigste Architektur-Risiken
#### 1. Start-/Schema-Initialisierung ist fragil
`DatabaseInitializationService` mischt derzeit:
- `EnsureCreated`
- manuelle `ALTER TABLE`-Pflege
- FK-Reparaturlogik
- Seeding
- empfohlenes Regel-Seeding
Das ist funktional hilfreich, aber architektonisch gefaehrlich, weil:
- die App-Initialisierung dadurch viel implizite Datenmigration enthaelt
- Verhalten schwer vorhersehbar wird
- Fehler im Migrationspfad sofort produktive Daten treffen
Wichtiger konkreter Befund aus der Pruefung:
- beim Kopieren von `Sites_old` nach `Sites` ist die Spaltenreihenfolge im SQL inkonsistent
- dadurch koennen Werte wie `ManualImportFilePath`, `SapServiceUrl`, `SapEntitySet` verschoben gespeichert werden
- das ist eine reale Datenkorruptionsgefahr und kein reines Architekturthema
### 2. Config-Import ist destruktiv und nicht atomar
`ConfigTransferService.ImportJsonAsync` loescht aktuell zuerst grosse Teile der Konfiguration und Daten:
- Sites
- HanaServers
- Transformation Rules
- SAP-Konfiguration
- Wechselkurse
- sogar `CentralSalesRecords`
und baut danach mit mehreren `SaveChangesAsync()`-Zwischenschritten neu auf.
Risiko:
- wenn der Import in der Mitte scheitert, bleibt das System teilweise geloescht zurueck
- `CentralSalesRecords` gehoeren fachlich ohnehin nicht sauber in einen normalen Config-Import
### 3. Zu viel Fach- und Persistenzlogik in Razor-Seiten
`Settings.razor` und `Standorte.razor` machen aktuell sehr viel direkt:
- `DbContext` oeffnen
- Daten laden und speichern
- Konfigurationsimport/-export anstossen
- SAP-Refresh
- Upload-Handling
- Teile der Validierung / Persistenzlogik
Das funktioniert momentan, fuehrt aber langfristig zu:
- schwer testbarer UI-Logik
- verstreuten Regeln
- hoeherem Seiteneffekt-Risiko bei Erweiterungen
### 4. Vertrag zwischen Orchestrator und konsolidiertem Export ist unscharf
`ExportOrchestrationService` sammelt bei `ExportAllAsync` bereits `consolidatedRecords`, uebergibt sie weiter, aber `ConsolidatedExportService` ignoriert diesen Parameter und liest erneut aus `CentralSalesRecords`.
Das zeigt ein offenes Architekturthema:
- Soll die zentrale Datei aus dem Live-Exportlauf gebaut werden?
- oder immer nur aus dem persistenten Read Model `CentralSalesRecords`?
Aktuell ist beides halb vorhanden.
### 5. Reporting-/Cockpit-Logik ist noch nicht voll verallgemeinert
Bei der Pruefung wurde gesehen:
- `ManagementCockpitService` enthaelt noch hartcodierte Jahreslogik fuer `2025` und `2026`
- die Rohsicht bleibt bewusst ohne CHF-Umrechnung
Das ist fuer den aktuellen Stand akzeptabel, zeigt aber:
- Reporting ist noch kein voll abstrahierter fachlicher Layer
## Empfohlenes Sollbild
Die naechste Architektur-Stufe sollte in diese Richtung gehen:
### 1. Klare Schichten
- UI:
- Razor nur fuer Interaktion, Anzeige, Formularzustand
- Application:
- Use Cases / Commands / Queries fuer Export, Config, SAP-Refresh, Wechselkurse, Standortpflege
- Domain / Fachlogik:
- Transformationen, Mappingregeln, Waehrungsumrechnung, Cockpit-Berechnungen
- Infrastructure:
- HANA, SAP Gateway, SQLite, SharePoint, Dateisystem
### 2. Versionierte Migrationen statt manueller Start-Reparaturen
Statt immer mehr Reparaturlogik beim App-Start:
- Schema-Aenderungen versionieren
- Migrationspfade testbar machen
- Startlogik nur noch fuer minimale Bootstrap-Aufgaben behalten
### 3. Config-Import als atomarer Vorgang
Ziel:
- alles in einer Transaktion oder bewusst in klar getrennten Phasen
- kein halb geloeschter Zustand bei Fehlern
- `CentralSalesRecords` aus normalem Config-Import eher herausnehmen
### 4. Zentrale Export-Semantik entscheiden
Explizit festlegen:
- zentrale Datei immer aus `CentralSalesRecords`
oder
- zentrale Datei aus dem aktuellen Export-Snapshot
Danach die doppelte Semantik entfernen.
## Priorisierung aus Architektursicht
Wenn nach Stabilitaet priorisiert wird, dann in dieser Reihenfolge:
1. `DatabaseInitializationService` / Migrationspfad absichern
2. `ConfigTransferService.ImportJsonAsync` atomar und weniger destruktiv machen
3. Logik aus `Settings.razor` und `Standorte.razor` in Anwendungsservices verschieben
4. Export-Semantik fuer Konsolidierung vereinheitlichen
5. erst danach weitere Fachfeatures wie Cockpit-CHF, Budget, Gruppenlogik
## Kurzfazit
Die Architektur ist nicht schlecht. Das Grundmodell traegt.
Aber:
- sie ist noch nicht robust genug fuer ruhigen weiteren Ausbau ohne technische Konsolidierung
- die aktuelle Hauptgefahr liegt in Infrastruktur- und Persistenzlogik, nicht in den Fachfeatures
Fuer den naechsten Einstieg nach Absturz gilt daher:
1. zuerst diesen Architektur-Nachtrag lesen
2. dann `DatabaseInitializationService` und `ConfigTransferService` als Risikobloecke ansehen
3. neue Fachfeatures erst nach dieser technischen Konsolidierung beginnen
## Nachtrag HANA-/Standort-Workflow 2026-04-17
Nach der Architekturpruefung wurde der doppelte HANA-Workflow bereinigt.
### Altes Problem
Vorher gab es zwei konkurrierende Stellen fuer HANA-Konfiguration:
- oben eine eigene `HANA Server`-Verwaltung
- unten im Standortdialog noch einmal eine fast vollstaendige HANA-Verbindung
Dadurch war unklar:
- was die zentrale Wahrheit ist
- wann ein zentraler Server geaendert wird
- wann still ein separater Server pro Standort entsteht
### Neue Logik
Oben gilt jetzt:
- `HANA Server` ist zentrale HANA-Konfiguration pro Quellsystem
- aktuell relevant fuer:
- `BI1`
- `SAGE`
Unten im Standort gilt jetzt:
- Standort pflegt nur noch standortspezifische Daten
- `Schema`
- `TSC`
- `Land`
- `SourceSystem`
- optionale Username-/Password-Overrides
- die technische HANA-Verbindung kommt aus der zentralen Konfiguration des Quellsystems
### Technische Umsetzung
- `HanaServer` hat jetzt zusaetzlich `SourceSystem`
- `DatabaseInitializationService` stellt zentrale Eintraege fuer `BI1` und `SAGE` sicher
- bestehende verknuepfte HANA-Server werden dabei moeglichst auf `BI1` / `SAGE` gemappt
- `SiteExportService` baut HANA-Verbindungen jetzt aus der zentralen HANA-Konfiguration des Quellsystems
- `Settings.razor` testet BI1/SAGE nicht mehr ueber einen Beispiel-Standort, sondern ueber die zentrale HANA-Konfiguration
- `Standorte.razor` speichert im Standort fuer HANA-basierte Systeme keine eigene Vollverbindung mehr
### Wichtige Konsequenz
Fachlich gilt jetzt:
- oben = Standardkonfiguration pro Quellsystem
- unten = Standort + optionale Credential-Overrides
Das entspricht der gewuenschten Logik:
- gleiche BI1-/SAGE-Standorte koennen zentrale Verbindungswerte teilen
- Ausnahmen koennen weiter ueber Username-/Password-Overrides reagieren
### UI-Nachtrag
Die frueher doppelte und dadurch verwirrende UI wurde danach auch sichtbar bereinigt.
Aktueller UI-Stand:
- oben heisst der Bereich jetzt klar `Zentrale HANA-Konfiguration`
- im Standortdialog gibt es fuer HANA keine zweite technische Eingabestrecke mehr
- dort wird nur noch die aktive Zentralverbindung angezeigt
- Host, Port, SSL und technische Parameter werden explizit nach oben verwiesen
- der zentrale Verbindungstest in `Settings.razor` meldet jetzt sauber die zentrale HANA-Verbindung
## Nachtrag Quellsystem-Verwaltung 2026-04-17
Die bisher noch hart codierten Quellsystem-Listen wurden entfernt und durch echte Stammdaten ersetzt.
### Neuer Stand
- neues Modell `SourceSystemDefinition`
- Quellsysteme werden jetzt zentral in der DB gehalten statt in Razor-Arrays
- pro Quellsystem werden gepflegt:
- `Code`
- `DisplayName`
- `ConnectionKind`
- `IsActive`
- `CentralUsername`
- `CentralPassword`
### Neue GUI-Logik
- `Settings.razor` enthaelt jetzt eine pflegbare Quellsystem-Tabelle
- dort koennen Quellsysteme per GUI angelegt, bearbeitet und gespeichert werden
- Anschlussart ist nicht mehr implizit im Code, sondern pro Quellsystem konfigurierbar
- zentrale Zugangsdaten haengen jetzt am Quellsystem selbst
### Anschlussarten
Aktuell technisch vorgesehen:
- `HANA`
- `SAP_GATEWAY`
- `MANUAL_EXCEL`
Damit gilt:
- HANA-Konfiguration oben in `Standorte.razor` nur noch fuer Quellsysteme mit Anschlussart `HANA`
- Standort-Dropdown zieht seine Quellsysteme jetzt aus `SourceSystemDefinitions`
- Transformationsregeln ziehen ihre Quellsystem-Auswahl ebenfalls aus `SourceSystemDefinitions`
### Technische Umsetzung
- `AppDbContext` hat jetzt `DbSet<SourceSystemDefinition>`
- `DatabaseInitializationService` erzeugt und seedet `SourceSystemDefinitions`
- `SiteExportService` loest zentrale Credentials jetzt ueber `SourceSystemDefinition`
- `ConfigTransferService` exportiert/importiert jetzt auch `SourceSystemDefinitions`
### Verifikation
Nach dieser Umstellung geprueft:
```text
dotnet build .\TrafagSalesExporter.csproj -v minimal
dotnet test .\TrafagSalesExporter.Tests\TrafagSalesExporter.Tests.csproj --verbosity minimal
```
Ergebnis:
- Build erfolgreich
- Tests erfolgreich
- `31/31` Tests gruen
### Bereinigung der Legacy-Credentials
Danach wurden auch die alten zentralen Credential-Felder technisch bereinigt.
Aktueller Stand:
- `ExportSettings` enthaelt keine alten Felder mehr fuer `SapUsername`, `Bi1Username`, `SageUsername` usw.
- der Config-Export schreibt zentrale Zugangsdaten nur noch ueber `SourceSystemDefinitions`
- `ConfigTransferService` hat keinen aktiven Legacy-Credential-Pfad mehr
- die fruehere Temp-Datei `standorte_numbered.tmp` wurde entfernt
Wichtig:
- bestehende DB-Spalten koennen physisch noch vorhanden sein, sind aber kein aktiver Codepfad mehr
- fuehrende Wahrheit fuer zentrale Zugangsdaten ist jetzt ausschliesslich `SourceSystemDefinition`
### Schema-Bereinigung
Danach wurde auch die SQLite-Schemabereinigung nachgezogen.
Aktueller Stand:
- `DatabaseInitializationService` erkennt alte Credential-Spalten in `ExportSettings`
- wenn diese Legacy-Spalten noch existieren, wird `ExportSettings` beim Start auf das neue Schema rekonstruiert
- erhalten bleiben nur die noch gueltigen Felder:
- `DateFilter`
- `TimerHour`
- `TimerMinute`
- `TimerEnabled`
- `DebugLoggingEnabled`
- `LocalSiteExportFolder`
- `LocalConsolidatedExportFolder`
Damit gilt jetzt:
- alte zentrale SAP/BI1/SAGE-Credentials sind nicht nur logisch entfernt
- sie werden bei bestehender DB auch aktiv aus dem `ExportSettings`-Schema entfernt
### Letzte Bereinigung HANA-Credentials
Danach wurde auch die letzte doppelte Credential-Stelle in der HANA-Verwaltung entfernt.
Aktueller Stand:
- zentrale HANA-Konfiguration speichert nur noch technische Verbindungsdaten
- `Host`
- `Port`
- `DatabaseName`
- `UseSsl`
- `ValidateCertificate`
- `AdditionalParams`
- Username/Password werden nicht mehr in der zentralen HANA-UI gepflegt
- HANA-Verbindungstests in `Standorte.razor` verwenden jetzt die zentralen Credentials aus `SourceSystemDefinition`
- `SiteExportService` faellt bei HANA nicht mehr auf in `HanaServer` gespeicherte Credentials zurueck
- `ConfigTransferService` exportiert/importiert fuer `HanaServer` keine Username-/Password-Werte mehr
- `DatabaseInitializationService` bereinigt bei bestehender DB auch das `HanaServers`-Schema und entfernt die Altspalten `Username` / `Password`
Die fachliche Reihenfolge ist jetzt eindeutig:
1. zentrale Credentials aus `SourceSystemDefinition`
2. optionale Override-Credentials am `Site`
3. technische HANA-Verbindung aus der zentralen HANA-Konfiguration
### EF-/SQLite-Fix
Beim ersten Lauf nach der Schema-Bereinigung trat noch ein Mapping-Fehler auf:
- `SQLite Error 1: 'no such column: h.Password'`
Ursache:
- `HanaServers`-Schema war bereits ohne `Username` / `Password`
- das EF-Modell `HanaServer` hat diese Properties aber noch als normale Spalten behandelt
Fix:
- `HanaServer.Username` und `HanaServer.Password` sind jetzt `[NotMapped]`
- damit bleiben sie fuer Laufzeit-Verbindungsaufbau und Tests nutzbar
- EF erwartet sie aber nicht mehr als Datenbankspalten
## Nachtrag Zentrale SAP-Steuerung 2026-04-17
Der verbleibende Architekturbruch bei SAP wurde ebenfalls bereinigt.
### Neuer Stand
- `SourceSystemDefinition` enthaelt jetzt auch `CentralServiceUrl`
- zentrale SAP-Service-URL wird damit am Quellsystem gepflegt, nicht mehr primaer am Standort
- `Standorte.razor` behandelt `SapServiceUrl` jetzt als Override
- wenn kein Override gesetzt ist, zieht SAP die URL zentral aus dem Quellsystem
### UI
- `Settings.razor` hat fuer Quellsysteme jetzt eine Dialogbearbeitung statt nur Inline-Tabellenfelder
- dadurch ist das Quellsystem sauber editierbar
- fuer `SAP_GATEWAY` wird dort die zentrale SAP-Service-URL gepflegt
- `Standorte.razor` zeigt bei SAP jetzt:
- zentrale SAP Service URL
- optionales `SAP Service URL Override`
### Laufzeitlogik
- `SiteExportService` verwendet bei SAP die effektive URL aus
- Standort-Override
- sonst `SourceSystemDefinition.CentralServiceUrl`
- SAP-Verbindungstest in `Settings.razor` testet die zentrale URL direkt aus dem Quellsystem
- Dashboard zeigt fuer SAP jetzt ebenfalls die effektive zentrale bzw. ueberschriebene URL
### Verifikation
Nach der Umstellung geprueft:
```text
dotnet build .\TrafagSalesExporter.csproj -v minimal
dotnet test .\TrafagSalesExporter.Tests\TrafagSalesExporter.Tests.csproj --verbosity minimal
```
Ergebnis:
- Build erfolgreich
- Tests erfolgreich
- `31/31` Tests gruen
## Nachtrag 2026-04-16 ## Nachtrag 2026-04-16
Seit dem letzten Handoff wurden weitere Funktionen umgesetzt, die unten im alten Stand noch nicht voll enthalten sind. Seit dem letzten Handoff wurden weitere Funktionen umgesetzt, die unten im alten Stand noch nicht voll enthalten sind.
@@ -518,3 +938,45 @@ Ergebnis:
- bekannte Warnungen bleiben: - bekannte Warnungen bleiben:
- SAP HANA Architekturwarnung `MSB3270` - SAP HANA Architekturwarnung `MSB3270`
- MudBlazor Analyzer `Dense` - MudBlazor Analyzer `Dense`
## Nachtrag 2026-04-17 UI-Klarstellung HANA vs. SAP
- `Components/Pages/Standorte.razor`
- Bereich oben heisst jetzt bewusst `Zentrale HANA-Technik`
- Hinweistext stellt klar: dort erscheinen nur Quellsysteme mit Anschlussart `HANA`
- `SAP` wird zentral unter `Settings -> Quellsysteme` gepflegt und gehoert nicht in diese Box
- der irrefuehrende Button `Server hinzufuegen` wurde entfernt
- neue HANA-Zeilen entstehen aus den Quellsystem-Stammdaten, nicht mehr aus einer zweiten UI-Erfassung
- Dialogtitel fuer HANA wurde auf reine Bearbeitung der zentralen Technik reduziert
Fachliche Regel jetzt:
- `Quellsysteme` verwalten die zentralen Systeme und deren Anschlussart
- `Standorte` zeigen fuer HANA nur noch die technische Zentralverbindung
- `SAP` wird nicht mehr implizit in der HANA-Box erwartet
## Nachtrag 2026-04-17 Pruefung Config-Import/Export
Der aktuelle Config-Transfer wurde nach den Umbauten nochmals geprueft.
Status:
- Das aktuelle Import-/Exportformat passt zum neuen Modell.
- `SourceSystemDefinitions` werden mit `ConnectionKind`, `CentralServiceUrl`, `CentralUsername`, `CentralPassword` importiert/exportiert.
- `HanaServers` enthalten nur noch technische HANA-Verbindungsdaten und keine Credentials mehr.
- Standort-Overrides fuer Username/Password sowie SAP Service URL gehen weiterhin mit.
- Die vorhandenen `ConfigTransferServiceTests` laufen grün.
Weiterhin offene Architekturpunkte:
- `ConfigTransferService.ImportJsonAsync` ist weiterhin destruktiv und nicht atomar.
- Erst werden bestehende Daten geloescht, danach wird in mehreren Schritten neu aufgebaut.
- Wenn der Import in der Mitte scheitert, bleibt ein teilweiser Zustand zurueck.
- Altformat-Risiko bei `ConnectionKind`:
- Wenn ein aelteres JSON bereits `SourceSystemDefinitions` enthaelt, aber noch ohne `ConnectionKind`, faellt der DTO-Default auf `HANA`.
- Dadurch koennte ein altes `SAP` beim Import falsch als `HANA` landen.
Fazit:
- Fuer Exporte aus dem aktuellen Stand ist der Config-Transfer konsistent.
- Fuer aeltere JSON-Staende braucht der Import noch eine explizite Migrations-/Fallback-Logik.
@@ -7,6 +7,7 @@ public class ConfigTransferPackage
public bool IncludesSecrets { get; set; } public bool IncludesSecrets { get; set; }
public ConfigTransferSharePoint? SharePointConfig { get; set; } public ConfigTransferSharePoint? SharePointConfig { get; set; }
public ConfigTransferExportSettings? ExportSettings { get; set; } public ConfigTransferExportSettings? ExportSettings { get; set; }
public List<ConfigTransferSourceSystemDefinition> SourceSystemDefinitions { get; set; } = [];
public List<ConfigTransferCurrencyExchangeRate> CurrencyExchangeRates { get; set; } = []; public List<ConfigTransferCurrencyExchangeRate> CurrencyExchangeRates { get; set; } = [];
public List<ConfigTransferHanaServer> HanaServers { get; set; } = []; public List<ConfigTransferHanaServer> HanaServers { get; set; } = [];
public List<ConfigTransferSite> Sites { get; set; } = []; public List<ConfigTransferSite> Sites { get; set; } = [];
@@ -16,6 +17,17 @@ public class ConfigTransferPackage
public List<ConfigTransferSapFieldMapping> SapFieldMappings { get; set; } = []; public List<ConfigTransferSapFieldMapping> SapFieldMappings { get; set; } = [];
} }
public class ConfigTransferSourceSystemDefinition
{
public string Code { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string ConnectionKind { get; set; } = SourceSystemConnectionKinds.Hana;
public bool IsActive { get; set; } = true;
public string CentralServiceUrl { get; set; } = string.Empty;
public string? CentralUsername { get; set; }
public string? CentralPassword { get; set; }
}
public class ConfigTransferSharePoint public class ConfigTransferSharePoint
{ {
public string SiteUrl { get; set; } = string.Empty; public string SiteUrl { get; set; } = string.Empty;
@@ -35,12 +47,6 @@ public class ConfigTransferExportSettings
public bool DebugLoggingEnabled { get; set; } public bool DebugLoggingEnabled { get; set; }
public string LocalSiteExportFolder { get; set; } = string.Empty; public string LocalSiteExportFolder { get; set; } = string.Empty;
public string LocalConsolidatedExportFolder { get; set; } = string.Empty; public string LocalConsolidatedExportFolder { get; set; } = string.Empty;
public string? SapUsername { get; set; }
public string? SapPassword { get; set; }
public string? Bi1Username { get; set; }
public string? Bi1Password { get; set; }
public string? SageUsername { get; set; }
public string? SagePassword { get; set; }
} }
public class ConfigTransferCurrencyExchangeRate public class ConfigTransferCurrencyExchangeRate
@@ -57,11 +63,10 @@ public class ConfigTransferCurrencyExchangeRate
public class ConfigTransferHanaServer public class ConfigTransferHanaServer
{ {
public string Key { get; set; } = Guid.NewGuid().ToString("N"); public string Key { get; set; } = Guid.NewGuid().ToString("N");
public string SourceSystem { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public string Host { get; set; } = string.Empty; public string Host { get; set; } = string.Empty;
public int Port { get; set; } = 30015; public int Port { get; set; } = 30015;
public string? Username { get; set; }
public string? Password { get; set; }
public string DatabaseName { get; set; } = string.Empty; public string DatabaseName { get; set; } = string.Empty;
public bool UseSsl { get; set; } public bool UseSsl { get; set; }
public bool ValidateCertificate { get; set; } public bool ValidateCertificate { get; set; }
@@ -75,7 +80,7 @@ public class ConfigTransferSite
public string Schema { get; set; } = string.Empty; public string Schema { get; set; } = string.Empty;
public string TSC { get; set; } = string.Empty; public string TSC { get; set; } = string.Empty;
public string Land { get; set; } = string.Empty; public string Land { get; set; } = string.Empty;
public string SourceSystem { get; set; } = "SAP"; public string SourceSystem { get; set; } = string.Empty;
public string? UsernameOverride { get; set; } public string? UsernameOverride { get; set; }
public string? PasswordOverride { get; set; } public string? PasswordOverride { get; set; }
public string LocalExportFolderOverride { get; set; } = string.Empty; public string LocalExportFolderOverride { get; set; } = string.Empty;
@@ -10,10 +10,4 @@ public class ExportSettings
public bool DebugLoggingEnabled { get; set; } public bool DebugLoggingEnabled { get; set; }
public string LocalSiteExportFolder { get; set; } = string.Empty; public string LocalSiteExportFolder { get; set; } = string.Empty;
public string LocalConsolidatedExportFolder { get; set; } = string.Empty; public string LocalConsolidatedExportFolder { get; set; } = string.Empty;
public string SapUsername { get; set; } = string.Empty;
public string SapPassword { get; set; } = string.Empty;
public string Bi1Username { get; set; } = string.Empty;
public string Bi1Password { get; set; } = string.Empty;
public string SageUsername { get; set; } = string.Empty;
public string SagePassword { get; set; } = string.Empty;
} }
@@ -7,7 +7,7 @@ public class FieldTransformationRule
public int Id { get; set; } public int Id { get; set; }
[Required] [Required]
public string SourceSystem { get; set; } = "SAP"; public string SourceSystem { get; set; } = string.Empty;
[Required] [Required]
public string SourceField { get; set; } = nameof(SalesRecord.Material); public string SourceField { get; set; } = nameof(SalesRecord.Material);
+7
View File
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Common; using System.Data.Common;
namespace TrafagSalesExporter.Models; namespace TrafagSalesExporter.Models;
@@ -7,6 +8,9 @@ public class HanaServer
{ {
public int Id { get; set; } public int Id { get; set; }
[Required]
public string SourceSystem { get; set; } = string.Empty;
[Required] [Required]
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
@@ -15,8 +19,10 @@ public class HanaServer
public int Port { get; set; } = 30015; public int Port { get; set; } = 30015;
[NotMapped]
public string Username { get; set; } = string.Empty; public string Username { get; set; } = string.Empty;
[NotMapped]
public string Password { get; set; } = string.Empty; public string Password { get; set; } = string.Empty;
/// <summary> /// <summary>
@@ -66,6 +72,7 @@ public class HanaServer
var pwdMasked = string.IsNullOrEmpty(Password) ? "" : "***"; var pwdMasked = string.IsNullOrEmpty(Password) ? "" : "***";
var copy = new HanaServer var copy = new HanaServer
{ {
SourceSystem = SourceSystem,
Host = Host, Host = Host,
Port = Port, Port = Port,
Username = Username, Username = Username,
+1 -1
View File
@@ -22,7 +22,7 @@ public class Site
public string Land { get; set; } = string.Empty; public string Land { get; set; } = string.Empty;
[Required] [Required]
public string SourceSystem { get; set; } = "SAP"; public string SourceSystem { get; set; } = string.Empty;
public string UsernameOverride { get; set; } = string.Empty; public string UsernameOverride { get; set; } = string.Empty;
@@ -0,0 +1,34 @@
using System.ComponentModel.DataAnnotations;
namespace TrafagSalesExporter.Models;
public class SourceSystemDefinition
{
public int Id { get; set; }
[Required]
public string Code { get; set; } = string.Empty;
[Required]
public string DisplayName { get; set; } = string.Empty;
[Required]
public string ConnectionKind { get; set; } = SourceSystemConnectionKinds.Hana;
public bool IsActive { get; set; } = true;
public string CentralServiceUrl { get; set; } = string.Empty;
public string CentralUsername { get; set; } = string.Empty;
public string CentralPassword { get; set; } = string.Empty;
}
public static class SourceSystemConnectionKinds
{
public const string Hana = "HANA";
public const string SapGateway = "SAP_GATEWAY";
public const string ManualExcel = "MANUAL_EXCEL";
public static readonly string[] All = [Hana, SapGateway, ManualExcel];
}
@@ -27,6 +27,147 @@ Was fuer Waehrungen trotzdem noch offen bleibt:
- bestaetigen, fuer welche Sichten CHF die Zielwaehrung sein soll - bestaetigen, fuer welche Sichten CHF die Zielwaehrung sein soll
- Management-Cockpit-Rohsicht nur dann auf CHF umstellen, wenn fachlich gewuenscht - Management-Cockpit-Rohsicht nur dann auf CHF umstellen, wenn fachlich gewuenscht
## Architektur-Nachtrag 2026-04-17
Nach einer separaten Architekturpruefung wurden die naechsten Schritte neu priorisiert.
Wichtig:
- neue Fachfeatures sind aktuell **nicht** der erste Engpass
- zuerst muessen die Architektur-Risiken in Initialisierung, Config-Import und UI-Service-Schnitt bereinigt werden
### Neue Top-Prioritaeten
#### 1. `DatabaseInitializationService` absichern
Prio sehr hoch.
Gruende:
- Startlogik enthaelt manuelle Schema-Migrationen
- FK-Reparaturen laufen produktiv beim App-Start
- dort wurde ein konkretes Risiko fuer verschobene Spaltenwerte beim `Sites_old`-Kopierpfad erkannt
Vor weiterer Fachentwicklung:
- Initialisierungspfad genau pruefen
- SQL-Kopierlogik validieren
- moeglichst Richtung versionierte Migrationen bewegen
#### 2. `ConfigTransferService.ImportJsonAsync` neu denken
Prio sehr hoch.
Aktuelles Problem:
- Import loescht sehr viel und baut danach stueckweise neu auf
- nicht atomar
- potenziell teilzerstoerter Zustand bei Fehlern
- `CentralSalesRecords` werden mitimportiert/mitgeloescht, obwohl sie eher Laufzeitdaten als Konfiguration sind
Ziel:
- atomarer Import
- saubere Trennung zwischen Konfiguration und Betriebsdaten
#### 3. Razor-Seiten entlasten
Prio hoch.
Betroffen vor allem:
- `Components/Pages/Settings.razor`
- `Components/Pages/Standorte.razor`
Ziel:
- DB- und Fachlogik aus UI-Code in Services / Application-Layer verschieben
- Seiten nur noch fuer Interaktion und Formularzustand
#### 4. Konsolidierten Export semantisch klaeren
Prio mittel.
Offene Frage:
- zentrale Datei aus laufendem Snapshot
oder
- zentrale Datei immer aus `CentralSalesRecords`
Aktuell ist die Verantwortung unscharf.
#### 5. Reporting verallgemeinern
Prio mittel.
Erst nach den Infrastrukturthemen:
- hartcodierte Jahreslogik im Cockpit entfernen
- fachlich entscheiden, ob und wo CHF-Rohsicht gebraucht wird
### Praktische Reihenfolge fuer den naechsten Wiedereinstieg
Wenn nach erneutem Absturz oder Kontextverlust weitergemacht wird:
1. `HANDOFF_2026-04-15.md` lesen, speziell die Architekturpruefung vom 2026-04-17
2. `DatabaseInitializationService` als ersten Risikoblock ansehen
3. `ConfigTransferService.ImportJsonAsync` als zweiten Risikoblock ansehen
4. erst danach wieder an Cockpit / CHF / weitere Fachfeatures gehen
## Nachtrag HANA-/Standort-Workflow 2026-04-17
Der doppelte HANA-Workflow wurde inzwischen bereits bereinigt.
Neuer Stand:
- oben zentrale HANA-Konfiguration pro Quellsystem `BI1` / `SAGE`
- unten im Standort keine eigene wirksame Voll-HANA-Konfiguration mehr
- HANA-basierte Standorte ziehen ihre technische Verbindung aus der zentralen Quellsystem-Konfiguration
- Standort bleibt fuer fachliche Daten und optionale Credential-Overrides zustaendig
- die frueher doppelte HANA-UI im Standortdialog ist inzwischen auch sichtbar entfernt
- der Verbindungstest in `Settings.razor` prueft und meldet jetzt die zentrale HANA-Verbindung klar
### Was dazu noch praktisch geprueft werden sollte
- `Standorte`-Seite im UI manuell durchklicken
- pruefen, ob `BI1`- und `SAGE`-Standort beim Speichern sauber auf die zentrale HANA-Konfiguration zeigen
- pruefen, ob Aenderung oben bei zentraler HANA-Konfiguration in nachfolgenden Exporten wirklich greift
### Anschlussarbeiten
- `ConfigTransferService` spaeter auf das neue zentrale HANA-Modell fachlich nachziehen und kritisch pruefen
- `DatabaseInitializationService` weiter konsolidieren, damit die Zuordnung alter HANA-Daten langfristig robuster wird
## Nachtrag Quellsystem-Verwaltung 2026-04-17
Die bisher hart codierten Quellsystem-Listen wurden ersetzt.
Neuer Stand:
- `SourceSystemDefinition` ist jetzt die zentrale Stammdatenquelle fuer Quellsysteme
- `Settings.razor` hat jetzt eine GUI zur Pflege von Quellsystemen
- `Standorte.razor` zieht seine Quellsystem-Auswahl aus diesen Stammdaten
- `Transformations.razor` zieht die Systemauswahl ebenfalls aus diesen Stammdaten
- zentrale Credentials haengen jetzt am Quellsystem selbst
- HANA-Zentralverbindungen werden nur noch fuer Quellsysteme mit Anschlussart `HANA` gezeigt
- alte zentrale Credential-Felder in `ExportSettings` sind aus dem aktiven Codepfad entfernt
- `ExportSettings` wird beim Start auch schematisch auf das neue Feldset bereinigt
- HANA speichert zentral keine eigenen Credentials mehr; dort bleiben nur technische Verbindungsdaten
- `HanaServer.Username` / `Password` sind nur noch Laufzeitfelder und nicht mehr im EF-Schema gemappt
- SAP Service URL wird jetzt zentral im Quellsystem gepflegt; der Standort haelt nur noch ein optionales Override
- Quellsysteme werden jetzt per Dialog bearbeitet statt nur ueber Inline-Tabellenfelder
### Was dazu noch praktisch geprueft werden sollte
- in `Settings` ein neues Quellsystem per GUI anlegen
- pruefen, ob es danach in `Standorte` und `Transformations` sofort auswählbar ist
- pruefen, ob deaktivierte Quellsysteme in neuen Standort-/Regelanlagen nicht mehr normal angeboten werden
- pruefen, ob Aenderung der Anschlussart von `HANA` auf `SAP_GATEWAY` oder `MANUAL_EXCEL` fachlich sauber wirkt
- pruefen, ob bestehende BI1/SAGE/SAP-Daten nach Startmigration korrekt in `SourceSystemDefinitions` stehen
- pruefen, ob Konfiguration-Export/Import ohne die alten Credential-Felder sauber mit `SourceSystemDefinitions` arbeitet
- pruefen, ob zentrale SAP Service URL ohne Override sauber fuer Refresh, Export und Dashboard greift
- pruefen, ob SAP Service URL Override am Standort die zentrale URL erwartungsgemaess uebersteuert
## Nachtrag 2026-04-16 ## Nachtrag 2026-04-16
Seit dem letzten Stand kamen mehrere groessere Erweiterungen dazu. Die offenen Punkte unten muessen deshalb im neuen Kontext gelesen werden. Seit dem letzten Stand kamen mehrere groessere Erweiterungen dazu. Die offenen Punkte unten muessen deshalb im neuen Kontext gelesen werden.
@@ -183,3 +324,37 @@ Aktueller Teststatus:
Fuer den vollstaendigen Kontext zuerst lesen: Fuer den vollstaendigen Kontext zuerst lesen:
- `HANDOFF_2026-04-15.md` - `HANDOFF_2026-04-15.md`
## 8. Letzte bereinigte UI-Irritation
Stand 2026-04-17:
- In `Standorte` wurde die obere Box auf `Zentrale HANA-Technik` geklaert.
- Dort gibt es keinen `Server hinzufuegen`-Pfad mehr.
- Grund: zentrale HANA-Eintraege werden aus `Quellsystemen` mit Anschlussart `HANA` abgeleitet.
- `SAP` gehoert fachlich nicht in diese Box, sondern in `Settings -> Quellsysteme`.
Wichtig fuer den naechsten Wiedereinstieg:
- Wenn ein Benutzer fragt `wo ist SAP?`, ist die richtige Antwort: nicht in der HANA-Box, sondern in der zentralen Quellsystem-Verwaltung.
- Wenn ein HANA-System oben fehlt, zuerst `Settings -> Quellsysteme` pruefen und dort Anschlussart `HANA` setzen.
## 9. Config-Transfer erneut geprueft
Stand 2026-04-17:
- Der aktuelle Config-Import/-Export passt zum neuen Datenmodell.
- Zentral verwaltete Quellsysteme, SAP-Zentral-URL, HANA-Technik ohne HANA-Credentials und Standort-Overrides werden korrekt im Transferformat abgebildet.
- Die vorhandenen `ConfigTransferServiceTests` bestaetigen den aktuellen Rundlauf.
Fuer den naechsten Wiedereinstieg wichtig:
- Das aktuelle Format ist fuer heutige Exporte konsistent.
- `ImportJsonAsync` ist aber weiterhin nicht atomar und loescht zuerst produktive Konfiguration.
- Zusaetzlich gibt es ein Altformat-Risiko:
- aeltere JSONs mit `SourceSystemDefinitions`, aber ohne `ConnectionKind`, koennen wegen DTO-Default falsch als `HANA` interpretiert werden.
Naechste saubere Haertung fuer dieses Thema:
- Config-Import transaktional machen
- Legacy-Fallback fuer fehlendes `ConnectionKind` einbauen
@@ -20,6 +20,7 @@ public class ConfigTransferService : IConfigTransferService
using var db = await _dbFactory.CreateDbContextAsync(); using var db = await _dbFactory.CreateDbContextAsync();
var sharePoint = await db.SharePointConfigs.FirstOrDefaultAsync(); var sharePoint = await db.SharePointConfigs.FirstOrDefaultAsync();
var exportSettings = await db.ExportSettings.FirstOrDefaultAsync(); var exportSettings = await db.ExportSettings.FirstOrDefaultAsync();
var sourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync();
var exchangeRates = await db.CurrencyExchangeRates var exchangeRates = await db.CurrencyExchangeRates
.OrderBy(x => x.FromCurrency) .OrderBy(x => x.FromCurrency)
.ThenBy(x => x.ToCurrency) .ThenBy(x => x.ToCurrency)
@@ -55,14 +56,18 @@ public class ConfigTransferService : IConfigTransferService
TimerEnabled = exportSettings.TimerEnabled, TimerEnabled = exportSettings.TimerEnabled,
DebugLoggingEnabled = exportSettings.DebugLoggingEnabled, DebugLoggingEnabled = exportSettings.DebugLoggingEnabled,
LocalSiteExportFolder = exportSettings.LocalSiteExportFolder, LocalSiteExportFolder = exportSettings.LocalSiteExportFolder,
LocalConsolidatedExportFolder = exportSettings.LocalConsolidatedExportFolder, LocalConsolidatedExportFolder = exportSettings.LocalConsolidatedExportFolder
SapUsername = includeSecrets ? exportSettings.SapUsername : null,
SapPassword = includeSecrets ? exportSettings.SapPassword : null,
Bi1Username = includeSecrets ? exportSettings.Bi1Username : null,
Bi1Password = includeSecrets ? exportSettings.Bi1Password : null,
SageUsername = includeSecrets ? exportSettings.SageUsername : null,
SagePassword = includeSecrets ? exportSettings.SagePassword : null
}, },
SourceSystemDefinitions = sourceSystems.Select(system => new ConfigTransferSourceSystemDefinition
{
Code = system.Code,
DisplayName = system.DisplayName,
ConnectionKind = system.ConnectionKind,
IsActive = system.IsActive,
CentralServiceUrl = system.CentralServiceUrl,
CentralUsername = includeSecrets ? system.CentralUsername : null,
CentralPassword = includeSecrets ? system.CentralPassword : null
}).ToList(),
CurrencyExchangeRates = exchangeRates.Select(rate => new ConfigTransferCurrencyExchangeRate CurrencyExchangeRates = exchangeRates.Select(rate => new ConfigTransferCurrencyExchangeRate
{ {
FromCurrency = rate.FromCurrency, FromCurrency = rate.FromCurrency,
@@ -76,11 +81,10 @@ public class ConfigTransferService : IConfigTransferService
HanaServers = hanaServers.Select(server => new ConfigTransferHanaServer HanaServers = hanaServers.Select(server => new ConfigTransferHanaServer
{ {
Key = serverKeyMap[server.Id], Key = serverKeyMap[server.Id],
SourceSystem = server.SourceSystem,
Name = server.Name, Name = server.Name,
Host = server.Host, Host = server.Host,
Port = server.Port, Port = server.Port,
Username = includeSecrets ? server.Username : null,
Password = includeSecrets ? server.Password : null,
DatabaseName = server.DatabaseName, DatabaseName = server.DatabaseName,
UseSsl = server.UseSsl, UseSsl = server.UseSsl,
ValidateCertificate = server.ValidateCertificate, ValidateCertificate = server.ValidateCertificate,
@@ -158,6 +162,7 @@ public class ConfigTransferService : IConfigTransferService
using var db = await _dbFactory.CreateDbContextAsync(); using var db = await _dbFactory.CreateDbContextAsync();
var existingSharePoint = await db.SharePointConfigs.FirstOrDefaultAsync(); var existingSharePoint = await db.SharePointConfigs.FirstOrDefaultAsync();
var existingSettings = await db.ExportSettings.FirstOrDefaultAsync(); var existingSettings = await db.ExportSettings.FirstOrDefaultAsync();
var existingSourceSystems = await db.SourceSystemDefinitions.ToListAsync();
var existingServers = await db.HanaServers.ToListAsync(); var existingServers = await db.HanaServers.ToListAsync();
var existingExchangeRates = await db.CurrencyExchangeRates.ToListAsync(); var existingExchangeRates = await db.CurrencyExchangeRates.ToListAsync();
var existingSites = await db.Sites.ToListAsync(); var existingSites = await db.Sites.ToListAsync();
@@ -168,20 +173,10 @@ public class ConfigTransferService : IConfigTransferService
var existingCentralRecords = await db.CentralSalesRecords.ToListAsync(); var existingCentralRecords = await db.CentralSalesRecords.ToListAsync();
var preservedSharePointSecret = existingSharePoint?.ClientSecret ?? string.Empty; var preservedSharePointSecret = existingSharePoint?.ClientSecret ?? string.Empty;
var preservedSecrets = existingSettings is null var preservedSourceSystemSecrets = existingSourceSystems.ToDictionary(
? new ConfigTransferExportSettings() x => x.Code,
: new ConfigTransferExportSettings x => (CentralUsername: x.CentralUsername, CentralPassword: x.CentralPassword),
{ StringComparer.OrdinalIgnoreCase);
SapUsername = existingSettings.SapUsername,
SapPassword = existingSettings.SapPassword,
Bi1Username = existingSettings.Bi1Username,
Bi1Password = existingSettings.Bi1Password,
SageUsername = existingSettings.SageUsername,
SagePassword = existingSettings.SagePassword
};
var preservedServerSecrets = existingServers.ToDictionary(
x => BuildServerSignature(x.Name, x.Host, x.Port, x.DatabaseName),
x => (x.Username, x.Password));
var preservedSiteSecrets = existingSites.ToDictionary( var preservedSiteSecrets = existingSites.ToDictionary(
x => BuildSiteSignature(x.Land, x.TSC, x.Schema, x.SourceSystem), x => BuildSiteSignature(x.Land, x.TSC, x.Schema, x.SourceSystem),
x => (x.UsernameOverride, x.PasswordOverride)); x => (x.UsernameOverride, x.PasswordOverride));
@@ -194,6 +189,7 @@ public class ConfigTransferService : IConfigTransferService
if (existingCentralRecords.Count > 0) db.CentralSalesRecords.RemoveRange(existingCentralRecords); if (existingCentralRecords.Count > 0) db.CentralSalesRecords.RemoveRange(existingCentralRecords);
if (existingSites.Count > 0) db.Sites.RemoveRange(existingSites); if (existingSites.Count > 0) db.Sites.RemoveRange(existingSites);
if (existingServers.Count > 0) db.HanaServers.RemoveRange(existingServers); if (existingServers.Count > 0) db.HanaServers.RemoveRange(existingServers);
if (existingSourceSystems.Count > 0) db.SourceSystemDefinitions.RemoveRange(existingSourceSystems);
if (existingSharePoint is not null) db.SharePointConfigs.Remove(existingSharePoint); if (existingSharePoint is not null) db.SharePointConfigs.Remove(existingSharePoint);
if (existingSettings is not null) db.ExportSettings.Remove(existingSettings); if (existingSettings is not null) db.ExportSettings.Remove(existingSettings);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
@@ -218,15 +214,28 @@ public class ConfigTransferService : IConfigTransferService
TimerEnabled = importedSettings.TimerEnabled, TimerEnabled = importedSettings.TimerEnabled,
DebugLoggingEnabled = importedSettings.DebugLoggingEnabled, DebugLoggingEnabled = importedSettings.DebugLoggingEnabled,
LocalSiteExportFolder = importedSettings.LocalSiteExportFolder, LocalSiteExportFolder = importedSettings.LocalSiteExportFolder,
LocalConsolidatedExportFolder = importedSettings.LocalConsolidatedExportFolder, LocalConsolidatedExportFolder = importedSettings.LocalConsolidatedExportFolder
SapUsername = package.IncludesSecrets ? importedSettings.SapUsername ?? string.Empty : preservedSecrets.SapUsername ?? string.Empty,
SapPassword = package.IncludesSecrets ? importedSettings.SapPassword ?? string.Empty : preservedSecrets.SapPassword ?? string.Empty,
Bi1Username = package.IncludesSecrets ? importedSettings.Bi1Username ?? string.Empty : preservedSecrets.Bi1Username ?? string.Empty,
Bi1Password = package.IncludesSecrets ? importedSettings.Bi1Password ?? string.Empty : preservedSecrets.Bi1Password ?? string.Empty,
SageUsername = package.IncludesSecrets ? importedSettings.SageUsername ?? string.Empty : preservedSecrets.SageUsername ?? string.Empty,
SagePassword = package.IncludesSecrets ? importedSettings.SagePassword ?? string.Empty : preservedSecrets.SagePassword ?? string.Empty
}); });
var importedSourceSystems = package.SourceSystemDefinitions.Count > 0
? package.SourceSystemDefinitions
: BuildDefaultSourceSystems();
foreach (var sourceSystem in importedSourceSystems)
{
preservedSourceSystemSecrets.TryGetValue(sourceSystem.Code, out var preserved);
db.SourceSystemDefinitions.Add(new SourceSystemDefinition
{
Code = sourceSystem.Code,
DisplayName = sourceSystem.DisplayName,
ConnectionKind = sourceSystem.ConnectionKind,
IsActive = sourceSystem.IsActive,
CentralServiceUrl = sourceSystem.CentralServiceUrl,
CentralUsername = package.IncludesSecrets ? sourceSystem.CentralUsername ?? string.Empty : preserved.CentralUsername ?? string.Empty,
CentralPassword = package.IncludesSecrets ? sourceSystem.CentralPassword ?? string.Empty : preserved.CentralPassword ?? string.Empty
});
}
if (package.CurrencyExchangeRates.Count > 0) if (package.CurrencyExchangeRates.Count > 0)
{ {
db.CurrencyExchangeRates.AddRange(package.CurrencyExchangeRates.Select(rate => new CurrencyExchangeRate db.CurrencyExchangeRates.AddRange(package.CurrencyExchangeRates.Select(rate => new CurrencyExchangeRate
@@ -244,14 +253,14 @@ public class ConfigTransferService : IConfigTransferService
var serverIdMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); var serverIdMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
foreach (var server in package.HanaServers) foreach (var server in package.HanaServers)
{ {
preservedServerSecrets.TryGetValue(BuildServerSignature(server.Name, server.Host, server.Port, server.DatabaseName), out var preserved);
var entity = new HanaServer var entity = new HanaServer
{ {
SourceSystem = server.SourceSystem,
Name = server.Name, Name = server.Name,
Host = server.Host, Host = server.Host,
Port = server.Port, Port = server.Port,
Username = package.IncludesSecrets ? server.Username ?? string.Empty : preserved.Username ?? string.Empty, Username = string.Empty,
Password = package.IncludesSecrets ? server.Password ?? string.Empty : preserved.Password ?? string.Empty, Password = string.Empty,
DatabaseName = server.DatabaseName, DatabaseName = server.DatabaseName,
UseSsl = server.UseSsl, UseSsl = server.UseSsl,
ValidateCertificate = server.ValidateCertificate, ValidateCertificate = server.ValidateCertificate,
@@ -355,10 +364,42 @@ public class ConfigTransferService : IConfigTransferService
await db.SaveChangesAsync(); await db.SaveChangesAsync();
} }
private static string BuildServerSignature(string name, string host, int port, string databaseName)
=> $"{name}|{host}|{port}|{databaseName}".ToUpperInvariant();
private static string BuildSiteSignature(string land, string tsc, string schema, string sourceSystem) private static string BuildSiteSignature(string land, string tsc, string schema, string sourceSystem)
=> $"{land}|{tsc}|{schema}|{sourceSystem}".ToUpperInvariant(); => $"{land}|{tsc}|{schema}|{sourceSystem}".ToUpperInvariant();
private static List<ConfigTransferSourceSystemDefinition> BuildDefaultSourceSystems()
{
return
[
new ConfigTransferSourceSystemDefinition
{
Code = "SAP",
DisplayName = "SAP",
ConnectionKind = SourceSystemConnectionKinds.SapGateway,
IsActive = true,
CentralServiceUrl = string.Empty
},
new ConfigTransferSourceSystemDefinition
{
Code = "BI1",
DisplayName = "BI1",
ConnectionKind = SourceSystemConnectionKinds.Hana,
IsActive = true
},
new ConfigTransferSourceSystemDefinition
{
Code = "SAGE",
DisplayName = "SAGE",
ConnectionKind = SourceSystemConnectionKinds.Hana,
IsActive = true
},
new ConfigTransferSourceSystemDefinition
{
Code = "MANUAL_EXCEL",
DisplayName = "Manual Excel",
ConnectionKind = SourceSystemConnectionKinds.ManualExcel,
IsActive = true
}
];
}
} }
@@ -46,7 +46,10 @@ public class DatabaseInitializationService : IDatabaseInitializationService
private static void EnsureSchema(AppDbContext db) private static void EnsureSchema(AppDbContext db)
{ {
EnsureSitesTableSupportsOptionalHanaServer(db); EnsureSitesTableSupportsOptionalHanaServer(db);
EnsureExportSettingsTableSupportsCurrentSchema(db);
EnsureHanaServersTableSupportsCurrentSchema(db);
RepairBrokenSiteForeignKeys(db); RepairBrokenSiteForeignKeys(db);
AddColumnIfMissing(db, "HanaServers", "SourceSystem", "TEXT NOT NULL DEFAULT ''");
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");
@@ -61,12 +64,6 @@ public class DatabaseInitializationService : IDatabaseInitializationService
AddColumnIfMissing(db, "Sites", "SapEntitySet", "TEXT NOT NULL DEFAULT ''"); AddColumnIfMissing(db, "Sites", "SapEntitySet", "TEXT NOT NULL DEFAULT ''");
AddColumnIfMissing(db, "Sites", "SapEntitySetsCache", "TEXT NOT NULL DEFAULT ''"); AddColumnIfMissing(db, "Sites", "SapEntitySetsCache", "TEXT NOT NULL DEFAULT ''");
AddColumnIfMissing(db, "Sites", "SapEntitySetsRefreshedAtUtc", "TEXT NULL"); AddColumnIfMissing(db, "Sites", "SapEntitySetsRefreshedAtUtc", "TEXT NULL");
AddColumnIfMissing(db, "ExportSettings", "SapUsername", "TEXT NOT NULL DEFAULT ''");
AddColumnIfMissing(db, "ExportSettings", "SapPassword", "TEXT NOT NULL DEFAULT ''");
AddColumnIfMissing(db, "ExportSettings", "Bi1Username", "TEXT NOT NULL DEFAULT ''");
AddColumnIfMissing(db, "ExportSettings", "Bi1Password", "TEXT NOT NULL DEFAULT ''");
AddColumnIfMissing(db, "ExportSettings", "SageUsername", "TEXT NOT NULL DEFAULT ''");
AddColumnIfMissing(db, "ExportSettings", "SagePassword", "TEXT NOT NULL DEFAULT ''");
AddColumnIfMissing(db, "ExportSettings", "DebugLoggingEnabled", "INTEGER NOT NULL DEFAULT 0"); AddColumnIfMissing(db, "ExportSettings", "DebugLoggingEnabled", "INTEGER NOT NULL DEFAULT 0");
AddColumnIfMissing(db, "ExportSettings", "LocalSiteExportFolder", "TEXT NOT NULL DEFAULT ''"); AddColumnIfMissing(db, "ExportSettings", "LocalSiteExportFolder", "TEXT NOT NULL DEFAULT ''");
AddColumnIfMissing(db, "ExportSettings", "LocalConsolidatedExportFolder", "TEXT NOT NULL DEFAULT ''"); AddColumnIfMissing(db, "ExportSettings", "LocalConsolidatedExportFolder", "TEXT NOT NULL DEFAULT ''");
@@ -75,11 +72,57 @@ public class DatabaseInitializationService : IDatabaseInitializationService
EnsureTransformationTable(db); EnsureTransformationTable(db);
AddColumnIfMissing(db, "FieldTransformationRules", "RuleScope", "TEXT NOT NULL DEFAULT 'Value'"); AddColumnIfMissing(db, "FieldTransformationRules", "RuleScope", "TEXT NOT NULL DEFAULT 'Value'");
EnsureCurrencyExchangeRateTable(db); EnsureCurrencyExchangeRateTable(db);
EnsureSourceSystemDefinitionTable(db);
AddColumnIfMissing(db, "SourceSystemDefinitions", "CentralServiceUrl", "TEXT NOT NULL DEFAULT ''");
EnsureSapSourceTable(db); EnsureSapSourceTable(db);
EnsureSapJoinTable(db); EnsureSapJoinTable(db);
EnsureSapFieldMappingTable(db); EnsureSapFieldMappingTable(db);
EnsureCentralSalesRecordTable(db); EnsureCentralSalesRecordTable(db);
EnsureAppEventLogTable(db); EnsureAppEventLogTable(db);
EnsureSourceSystemDefinitions(db);
EnsureCentralHanaServerRecords(db);
}
private static void EnsureExportSettingsTableSupportsCurrentSchema(AppDbContext db)
{
var conn = db.Database.GetDbConnection();
if (conn.State != ConnectionState.Open)
conn.Open();
var columns = GetTableColumns(conn, transaction: null, "ExportSettings");
if (columns.Count == 0)
return;
var legacyColumns = new[]
{
"SapUsername",
"SapPassword",
"Bi1Username",
"Bi1Password",
"SageUsername",
"SagePassword"
};
if (!legacyColumns.Any(columns.Contains))
return;
RebuildTable(conn, "ExportSettings", GetExportSettingsCreateSql());
}
private static void EnsureHanaServersTableSupportsCurrentSchema(AppDbContext db)
{
var conn = db.Database.GetDbConnection();
if (conn.State != ConnectionState.Open)
conn.Open();
var columns = GetTableColumns(conn, transaction: null, "HanaServers");
if (columns.Count == 0)
return;
if (!columns.Contains("Username") && !columns.Contains("Password"))
return;
RebuildTable(conn, "HanaServers", GetHanaServersCreateSql());
} }
private static void EnsureSitesTableSupportsOptionalHanaServer(AppDbContext db) private static void EnsureSitesTableSupportsOptionalHanaServer(AppDbContext db)
@@ -272,7 +315,7 @@ FROM Sites_old;";
enableFk.ExecuteNonQuery(); enableFk.ExecuteNonQuery();
} }
private static List<string> GetSharedColumns(System.Data.Common.DbConnection connection, System.Data.Common.DbTransaction transaction, string newTableName, string oldTableName) private static List<string> GetSharedColumns(System.Data.Common.DbConnection connection, System.Data.Common.DbTransaction? transaction, string newTableName, string oldTableName)
{ {
var newColumns = GetTableColumns(connection, transaction, newTableName); var newColumns = GetTableColumns(connection, transaction, newTableName);
var oldColumns = GetTableColumns(connection, transaction, oldTableName); var oldColumns = GetTableColumns(connection, transaction, oldTableName);
@@ -280,7 +323,7 @@ FROM Sites_old;";
return newColumns.Where(oldColumns.Contains).ToList(); return newColumns.Where(oldColumns.Contains).ToList();
} }
private static HashSet<string> GetTableColumns(System.Data.Common.DbConnection connection, System.Data.Common.DbTransaction transaction, string tableName) private static HashSet<string> GetTableColumns(System.Data.Common.DbConnection connection, System.Data.Common.DbTransaction? transaction, string tableName)
{ {
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
@@ -315,6 +358,31 @@ CREATE TABLE ExportLogs (
FOREIGN KEY (SiteId) REFERENCES Sites (Id) FOREIGN KEY (SiteId) REFERENCES Sites (Id)
);"; );";
private static string GetExportSettingsCreateSql() => @"
CREATE TABLE ExportSettings (
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
DateFilter TEXT NOT NULL,
TimerHour INTEGER NOT NULL,
TimerMinute INTEGER NOT NULL,
TimerEnabled INTEGER NOT NULL,
DebugLoggingEnabled INTEGER NOT NULL DEFAULT 0,
LocalSiteExportFolder TEXT NOT NULL DEFAULT '',
LocalConsolidatedExportFolder TEXT NOT NULL DEFAULT ''
);";
private static string GetHanaServersCreateSql() => @"
CREATE TABLE HanaServers (
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
SourceSystem TEXT NOT NULL,
Name TEXT NOT NULL,
Host TEXT NOT NULL,
Port INTEGER NOT NULL,
DatabaseName TEXT NOT NULL DEFAULT '',
UseSsl INTEGER NOT NULL DEFAULT 0,
ValidateCertificate INTEGER NOT NULL DEFAULT 0,
AdditionalParams TEXT NOT NULL DEFAULT ''
);";
private static string GetAppEventLogsCreateSql() => @" private static string GetAppEventLogsCreateSql() => @"
CREATE TABLE AppEventLogs ( CREATE TABLE AppEventLogs (
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
@@ -604,21 +672,42 @@ CREATE TABLE IF NOT EXISTS AppEventLogs (
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }
private static void EnsureSourceSystemDefinitionTable(AppDbContext db)
{
var conn = db.Database.GetDbConnection();
if (conn.State != ConnectionState.Open)
conn.Open();
using var cmd = conn.CreateCommand();
cmd.CommandText = @"
CREATE TABLE IF NOT EXISTS SourceSystemDefinitions (
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
Code TEXT NOT NULL,
DisplayName TEXT NOT NULL,
ConnectionKind TEXT NOT NULL,
IsActive INTEGER NOT NULL DEFAULT 1,
CentralServiceUrl TEXT NOT NULL DEFAULT '',
CentralUsername TEXT NOT NULL DEFAULT '',
CentralPassword TEXT NOT NULL DEFAULT ''
);";
cmd.ExecuteNonQuery();
}
private static void SeedIfEmpty(AppDbContext db) private static void SeedIfEmpty(AppDbContext db)
{ {
if (db.HanaServers.Any()) if (db.Sites.Any() || db.HanaServers.Any() || db.SharePointConfigs.Any() || db.ExportSettings.Any())
return; return;
var serverInternal = new HanaServer { Name = "Internal", Host = "travtrp0", Port = 30015, Username = "", Password = "" }; var serverBi1 = new HanaServer { SourceSystem = "BI1", Name = "BI1", Host = "travtrp0", Port = 30015, Username = "", Password = "" };
var serverIndia = new HanaServer { Name = "India", Host = "20.197.20.60", Port = 30015, Username = "", Password = "" }; var serverSage = new HanaServer { SourceSystem = "SAGE", Name = "SAGE", Host = "20.197.20.60", Port = 30015, Username = "", Password = "" };
db.HanaServers.AddRange(serverInternal, serverIndia); db.HanaServers.AddRange(serverBi1, serverSage);
db.SaveChanges(); db.SaveChanges();
db.Sites.AddRange( db.Sites.AddRange(
new Site { HanaServerId = serverInternal.Id, Schema = "fr01_p", TSC = "TRFR", Land = "Frankreich", IsActive = true }, new Site { HanaServerId = serverBi1.Id, Schema = "fr01_p", TSC = "TRFR", Land = "Frankreich", SourceSystem = "BI1", IsActive = true },
new Site { HanaServerId = serverInternal.Id, Schema = "it01_p", TSC = "TRIT", Land = "Italien", IsActive = true }, new Site { HanaServerId = serverBi1.Id, Schema = "it01_p", TSC = "TRIT", Land = "Italien", SourceSystem = "BI1", IsActive = true },
new Site { HanaServerId = serverInternal.Id, Schema = "us01_p", TSC = "TRUS", Land = "USA", IsActive = true }, new Site { HanaServerId = serverBi1.Id, Schema = "us01_p", TSC = "TRUS", Land = "USA", SourceSystem = "BI1", IsActive = true },
new Site { HanaServerId = serverIndia.Id, Schema = "TRAFAG_LIVE", TSC = "TRIN", Land = "Indien", IsActive = true } new Site { HanaServerId = serverSage.Id, Schema = "TRAFAG_LIVE", TSC = "TRIN", Land = "Indien", SourceSystem = "SAGE", IsActive = true }
); );
db.SharePointConfigs.Add(new SharePointConfig db.SharePointConfigs.Add(new SharePointConfig
@@ -695,4 +784,121 @@ CREATE TABLE IF NOT EXISTS AppEventLogs (
if (hasChanges) if (hasChanges)
db.SaveChanges(); db.SaveChanges();
} }
private static void EnsureCentralHanaServerRecords(AppDbContext db)
{
var centralSystems = db.SourceSystemDefinitions
.AsNoTracking()
.Where(x => x.ConnectionKind == SourceSystemConnectionKinds.Hana)
.OrderBy(x => x.Code)
.Select(x => x.Code)
.ToList();
var changed = false;
foreach (var sourceSystem in centralSystems)
{
var existingCentral = db.HanaServers
.OrderBy(x => x.Id)
.FirstOrDefault(x => x.SourceSystem == sourceSystem);
if (existingCentral is not null)
{
if (string.IsNullOrWhiteSpace(existingCentral.Name))
{
existingCentral.Name = sourceSystem;
changed = true;
}
continue;
}
var linkedServer = db.Sites
.Include(x => x.HanaServer)
.Where(x => x.SourceSystem == sourceSystem && x.HanaServerId != null && x.HanaServer != null)
.Select(x => x.HanaServer!)
.OrderBy(x => x.Id)
.FirstOrDefault();
if (linkedServer is not null)
{
linkedServer.SourceSystem = sourceSystem;
if (string.IsNullOrWhiteSpace(linkedServer.Name))
linkedServer.Name = sourceSystem;
changed = true;
continue;
}
db.HanaServers.Add(new HanaServer
{
SourceSystem = sourceSystem,
Name = sourceSystem,
Host = string.Empty,
Port = 30015,
Username = string.Empty,
Password = string.Empty,
DatabaseName = string.Empty,
AdditionalParams = string.Empty
});
changed = true;
}
if (changed)
db.SaveChanges();
}
private static void EnsureSourceSystemDefinitions(AppDbContext db)
{
var defaults = new[]
{
new SourceSystemDefinition { Code = "SAP", DisplayName = "SAP", ConnectionKind = SourceSystemConnectionKinds.SapGateway, IsActive = true },
new SourceSystemDefinition { Code = "BI1", DisplayName = "BI1", ConnectionKind = SourceSystemConnectionKinds.Hana, IsActive = true },
new SourceSystemDefinition { Code = "SAGE", DisplayName = "SAGE", ConnectionKind = SourceSystemConnectionKinds.Hana, IsActive = true },
new SourceSystemDefinition { Code = "MANUAL_EXCEL", DisplayName = "Manual Excel", ConnectionKind = SourceSystemConnectionKinds.ManualExcel, IsActive = true }
};
var existing = db.SourceSystemDefinitions.ToList();
var changed = false;
foreach (var item in defaults)
{
var current = existing.FirstOrDefault(x => x.Code == item.Code);
if (current is null)
{
db.SourceSystemDefinitions.Add(item);
existing.Add(item);
changed = true;
continue;
}
if (string.IsNullOrWhiteSpace(current.DisplayName))
{
current.DisplayName = item.DisplayName;
changed = true;
}
if (string.IsNullOrWhiteSpace(current.ConnectionKind))
{
current.ConnectionKind = item.ConnectionKind;
changed = true;
}
if (string.IsNullOrWhiteSpace(current.CentralServiceUrl) &&
string.Equals(current.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase))
{
var sapSite = db.Sites
.Where(x => x.SourceSystem == current.Code && !string.IsNullOrWhiteSpace(x.SapServiceUrl))
.OrderBy(x => x.Id)
.FirstOrDefault();
if (sapSite is not null)
{
current.CentralServiceUrl = sapSite.SapServiceUrl;
changed = true;
}
}
}
if (changed)
db.SaveChanges();
}
} }
@@ -65,13 +65,19 @@ public class SiteExportService : ISiteExportService
var spConfig = await db.SharePointConfigs.FirstOrDefaultAsync(); var spConfig = await db.SharePointConfigs.FirstOrDefaultAsync();
var outputDir = ResolveSiteOutputDirectory(settings, site); var outputDir = ResolveSiteOutputDirectory(settings, site);
var sourceSystem = NormalizeSourceSystem(site.SourceSystem); var sourceSystem = NormalizeSourceSystem(site.SourceSystem);
var sourceDefinition = await db.SourceSystemDefinitions
.AsNoTracking()
.OrderBy(x => x.Id)
.FirstOrDefaultAsync(x => x.Code == sourceSystem)
?? throw new InvalidOperationException($"Quellsystem '{sourceSystem}' ist nicht konfiguriert.");
var records = new List<SalesRecord>(); var records = new List<SalesRecord>();
string filePath; string filePath;
if (sourceSystem == "SAP") if (string.Equals(sourceDefinition.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase))
{ {
var credentials = ResolveCredentials(site, settings, sourceSystem); var credentials = ResolveCredentials(site, sourceDefinition);
if (string.IsNullOrWhiteSpace(site.SapServiceUrl)) var sapServiceUrl = ResolveSapServiceUrl(site, sourceDefinition);
if (string.IsNullOrWhiteSpace(sapServiceUrl))
throw new InvalidOperationException($"Standort '{site.Land}' hat keine SAP Service URL."); throw new InvalidOperationException($"Standort '{site.Land}' hat keine SAP Service URL.");
var sapSources = await db.SapSourceDefinitions.Where(s => s.SiteId == site.Id).ToListAsync(); var sapSources = await db.SapSourceDefinitions.Where(s => s.SiteId == site.Id).ToListAsync();
var sapJoins = await db.SapJoinDefinitions.Where(j => j.SiteId == site.Id).ToListAsync(); var sapJoins = await db.SapJoinDefinitions.Where(j => j.SiteId == site.Id).ToListAsync();
@@ -84,7 +90,8 @@ public class SiteExportService : ISiteExportService
updateStatus?.Invoke("SAP Quellen laden..."); updateStatus?.Invoke("SAP Quellen laden...");
await _appEventLogService.WriteAsync("Export", "SAP Quellen laden", siteId: site.Id, land: site.Land, await _appEventLogService.WriteAsync("Export", "SAP Quellen laden", siteId: site.Id, land: site.Land,
details: $"Sources={sapSources.Count} | Mappings={sapMappings.Count}"); details: $"Sources={sapSources.Count} | Mappings={sapMappings.Count}");
records = await _sapCompositionService.BuildSalesRecordsAsync(site, sapSources, sapJoins, sapMappings, credentials.Username, credentials.Password); var effectiveSite = CloneSiteWithSapServiceUrl(site, sapServiceUrl);
records = await _sapCompositionService.BuildSalesRecordsAsync(effectiveSite, sapSources, sapJoins, sapMappings, credentials.Username, credentials.Password);
updateStatus?.Invoke("Transformationen anwenden..."); updateStatus?.Invoke("Transformationen anwenden...");
await _appEventLogService.WriteAsync("Export", "Transformationen anwenden", siteId: site.Id, land: site.Land, await _appEventLogService.WriteAsync("Export", "Transformationen anwenden", siteId: site.Id, land: site.Land,
details: $"Records vor Transformation={records.Count}"); details: $"Records vor Transformation={records.Count}");
@@ -99,7 +106,7 @@ public class SiteExportService : ISiteExportService
filePath = _excelService.CreateExcelFile(outputDir, site.TSC, DateTime.UtcNow.Date, records); filePath = _excelService.CreateExcelFile(outputDir, site.TSC, DateTime.UtcNow.Date, records);
log.RowCount = records.Count; log.RowCount = records.Count;
} }
else if (sourceSystem == "MANUAL_EXCEL") else if (string.Equals(sourceDefinition.ConnectionKind, SourceSystemConnectionKinds.ManualExcel, StringComparison.OrdinalIgnoreCase))
{ {
if (string.IsNullOrWhiteSpace(site.ManualImportFilePath)) if (string.IsNullOrWhiteSpace(site.ManualImportFilePath))
throw new InvalidOperationException($"Standort '{site.Land}' hat keine manuelle Excel-Datei."); throw new InvalidOperationException($"Standort '{site.Land}' hat keine manuelle Excel-Datei.");
@@ -125,7 +132,7 @@ public class SiteExportService : ISiteExportService
} }
else else
{ {
var exportServer = BuildEffectiveServer(site, settings, sourceSystem); var exportServer = await BuildEffectiveServerAsync(db, site, sourceDefinition);
updateStatus?.Invoke("HANA Abfrage..."); updateStatus?.Invoke("HANA Abfrage...");
await _appEventLogService.WriteAsync("Export", "HANA Abfrage gestartet", siteId: site.Id, land: site.Land, await _appEventLogService.WriteAsync("Export", "HANA Abfrage gestartet", siteId: site.Id, land: site.Land,
details: exportServer.GetConnectionStringPreview()); details: exportServer.GetConnectionStringPreview());
@@ -208,45 +215,40 @@ public class SiteExportService : ISiteExportService
} }
} }
private static HanaServer BuildEffectiveServer(Site site, ExportSettings settings, string sourceSystem) private static async Task<HanaServer> BuildEffectiveServerAsync(AppDbContext db, Site site, SourceSystemDefinition sourceDefinition)
{ {
if (site.HanaServer is null) var centralServer = await db.HanaServers
throw new InvalidOperationException($"Standort '{site.Land}' hat keinen HANA-Server."); .AsNoTracking()
.OrderBy(x => x.Id)
.FirstOrDefaultAsync(x => x.SourceSystem == sourceDefinition.Code);
var credentials = ResolveCredentials(site, settings, sourceSystem); if (centralServer is null)
throw new InvalidOperationException($"Fuer Quellsystem '{sourceDefinition.Code}' ist keine zentrale HANA-Konfiguration vorhanden.");
var credentials = ResolveCredentials(site, sourceDefinition);
return new HanaServer return new HanaServer
{ {
Id = site.HanaServer.Id, Id = centralServer.Id,
Name = site.HanaServer.Name, SourceSystem = centralServer.SourceSystem,
Host = site.HanaServer.Host, Name = centralServer.Name,
Port = site.HanaServer.Port, Host = centralServer.Host,
Username = FirstNonEmpty(credentials.Username, site.HanaServer.Username), Port = centralServer.Port,
Password = FirstNonEmpty(credentials.Password, site.HanaServer.Password), Username = credentials.Username,
DatabaseName = site.HanaServer.DatabaseName, Password = credentials.Password,
UseSsl = site.HanaServer.UseSsl, DatabaseName = centralServer.DatabaseName,
ValidateCertificate = site.HanaServer.ValidateCertificate, UseSsl = centralServer.UseSsl,
AdditionalParams = site.HanaServer.AdditionalParams ValidateCertificate = centralServer.ValidateCertificate,
AdditionalParams = centralServer.AdditionalParams
}; };
} }
private static (string Username, string Password) ResolveCredentials(Site site, ExportSettings settings, string sourceSystem) private static (string Username, string Password) ResolveCredentials(Site site, SourceSystemDefinition sourceDefinition)
=> (FirstNonEmpty(site.UsernameOverride, GetCentralUsername(sourceSystem, settings)), => (FirstNonEmpty(site.UsernameOverride, sourceDefinition.CentralUsername),
FirstNonEmpty(site.PasswordOverride, GetCentralPassword(sourceSystem, settings))); FirstNonEmpty(site.PasswordOverride, sourceDefinition.CentralPassword));
private static string GetCentralUsername(string sourceSystem, ExportSettings settings) => sourceSystem switch private static string ResolveSapServiceUrl(Site site, SourceSystemDefinition sourceDefinition)
{ => FirstNonEmpty(site.SapServiceUrl, sourceDefinition.CentralServiceUrl);
"BI1" => settings.Bi1Username,
"SAGE" => settings.SageUsername,
_ => settings.SapUsername
};
private static string GetCentralPassword(string sourceSystem, ExportSettings settings) => sourceSystem switch
{
"BI1" => settings.Bi1Password,
"SAGE" => settings.SagePassword,
_ => settings.SapPassword
};
private static string NormalizeSourceSystem(string? sourceSystem) private static string NormalizeSourceSystem(string? sourceSystem)
=> string.IsNullOrWhiteSpace(sourceSystem) ? "SAP" : sourceSystem.Trim().ToUpperInvariant(); => string.IsNullOrWhiteSpace(sourceSystem) ? "SAP" : sourceSystem.Trim().ToUpperInvariant();
@@ -269,4 +271,28 @@ public class SiteExportService : ISiteExportService
? Path.Combine(AppContext.BaseDirectory, "output") ? Path.Combine(AppContext.BaseDirectory, "output")
: configured; : configured;
} }
private static Site CloneSiteWithSapServiceUrl(Site site, string sapServiceUrl)
{
return new Site
{
Id = site.Id,
HanaServerId = site.HanaServerId,
HanaServer = site.HanaServer,
Schema = site.Schema,
TSC = site.TSC,
Land = site.Land,
SourceSystem = site.SourceSystem,
UsernameOverride = site.UsernameOverride,
PasswordOverride = site.PasswordOverride,
LocalExportFolderOverride = site.LocalExportFolderOverride,
ManualImportFilePath = site.ManualImportFilePath,
ManualImportLastUploadedAtUtc = site.ManualImportLastUploadedAtUtc,
SapServiceUrl = sapServiceUrl,
SapEntitySet = site.SapEntitySet,
SapEntitySetsCache = site.SapEntitySetsCache,
SapEntitySetsRefreshedAtUtc = site.SapEntitySetsRefreshedAtUtc,
IsActive = site.IsActive
};
}
} }
@@ -47,14 +47,16 @@ public class ConfigTransferServiceTests : IDisposable
Assert.False(package.IncludesSecrets); Assert.False(package.IncludesSecrets);
Assert.NotNull(package.ExportSettings); Assert.NotNull(package.ExportSettings);
Assert.Null(package.ExportSettings.SapUsername);
Assert.Null(package.ExportSettings.SapPassword);
Assert.NotNull(package.SharePointConfig); Assert.NotNull(package.SharePointConfig);
Assert.Null(package.SharePointConfig.ClientSecret); Assert.Null(package.SharePointConfig.ClientSecret);
Assert.NotEmpty(package.SourceSystemDefinitions);
Assert.All(package.SourceSystemDefinitions, system =>
{
Assert.Null(system.CentralUsername);
Assert.Null(system.CentralPassword);
});
var server = Assert.Single(package.HanaServers); Assert.Single(package.HanaServers);
Assert.Null(server.Username);
Assert.Null(server.Password);
var site = Assert.Single(package.Sites); var site = Assert.Single(package.Sites);
Assert.Null(site.UsernameOverride); Assert.Null(site.UsernameOverride);
@@ -90,14 +92,47 @@ public class ConfigTransferServiceTests : IDisposable
TimerEnabled = false, TimerEnabled = false,
DebugLoggingEnabled = true, DebugLoggingEnabled = true,
LocalSiteExportFolder = "D:\\site", LocalSiteExportFolder = "D:\\site",
LocalConsolidatedExportFolder = "D:\\consolidated", LocalConsolidatedExportFolder = "D:\\consolidated"
SapUsername = null,
SapPassword = null,
Bi1Username = null,
Bi1Password = null,
SageUsername = null,
SagePassword = null
}, },
SourceSystemDefinitions =
[
new ConfigTransferSourceSystemDefinition
{
Code = "SAP",
DisplayName = "SAP",
ConnectionKind = SourceSystemConnectionKinds.SapGateway,
IsActive = true,
CentralUsername = null,
CentralPassword = null
},
new ConfigTransferSourceSystemDefinition
{
Code = "BI1",
DisplayName = "BI1",
ConnectionKind = SourceSystemConnectionKinds.Hana,
IsActive = true,
CentralUsername = null,
CentralPassword = null
},
new ConfigTransferSourceSystemDefinition
{
Code = "SAGE",
DisplayName = "SAGE",
ConnectionKind = SourceSystemConnectionKinds.Hana,
IsActive = true,
CentralUsername = null,
CentralPassword = null
},
new ConfigTransferSourceSystemDefinition
{
Code = "MANUAL_EXCEL",
DisplayName = "Manual Excel",
ConnectionKind = SourceSystemConnectionKinds.ManualExcel,
IsActive = true,
CentralUsername = null,
CentralPassword = null
}
],
HanaServers = HanaServers =
[ [
new ConfigTransferHanaServer new ConfigTransferHanaServer
@@ -106,8 +141,6 @@ public class ConfigTransferServiceTests : IDisposable
Name = "Server A", Name = "Server A",
Host = "hana-a", Host = "hana-a",
Port = 30015, Port = 30015,
Username = null,
Password = null,
DatabaseName = "DB1", DatabaseName = "DB1",
UseSsl = true, UseSsl = true,
ValidateCertificate = false, ValidateCertificate = false,
@@ -152,20 +185,34 @@ public class ConfigTransferServiceTests : IDisposable
await using var db = await _dbFactory.CreateDbContextAsync(); await using var db = await _dbFactory.CreateDbContextAsync();
var settings = await db.ExportSettings.SingleAsync(); var settings = await db.ExportSettings.SingleAsync();
var sharePoint = await db.SharePointConfigs.SingleAsync(); var sharePoint = await db.SharePointConfigs.SingleAsync();
var systems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync();
var server = await db.HanaServers.SingleAsync(); var server = await db.HanaServers.SingleAsync();
var site = await db.Sites.SingleAsync(); var site = await db.Sites.SingleAsync();
var rule = await db.FieldTransformationRules.SingleAsync(); var rule = await db.FieldTransformationRules.SingleAsync();
Assert.Equal("preserved-sap-user", settings.SapUsername); Assert.Equal("2026-01-01", settings.DateFilter);
Assert.Equal("preserved-sap-password", settings.SapPassword); Assert.Equal(5, settings.TimerHour);
Assert.Equal("preserved-bi1-user", settings.Bi1Username); Assert.Equal(30, settings.TimerMinute);
Assert.Equal("preserved-sage-password", settings.SagePassword); Assert.False(settings.TimerEnabled);
Assert.True(settings.DebugLoggingEnabled);
Assert.Equal("D:\\site", settings.LocalSiteExportFolder);
Assert.Equal("D:\\consolidated", settings.LocalConsolidatedExportFolder);
Assert.Equal("preserved-sharepoint-secret", sharePoint.ClientSecret); Assert.Equal("preserved-sharepoint-secret", sharePoint.ClientSecret);
Assert.Equal("new-tenant", sharePoint.TenantId); Assert.Equal("new-tenant", sharePoint.TenantId);
Assert.Equal("preserved-server-user", server.Username); var sapSystem = Assert.Single(systems, x => x.Code == "SAP");
Assert.Equal("preserved-server-password", server.Password); Assert.Equal("preserved-sap-user", sapSystem.CentralUsername);
Assert.Equal("preserved-sap-password", sapSystem.CentralPassword);
var bi1System = Assert.Single(systems, x => x.Code == "BI1");
Assert.Equal("preserved-bi1-user", bi1System.CentralUsername);
Assert.Equal("preserved-bi1-password", bi1System.CentralPassword);
var sageSystem = Assert.Single(systems, x => x.Code == "SAGE");
Assert.Equal("preserved-sage-user", sageSystem.CentralUsername);
Assert.Equal("preserved-sage-password", sageSystem.CentralPassword);
Assert.Equal(string.Empty, server.Username);
Assert.Equal(string.Empty, server.Password);
Assert.True(server.UseSsl); Assert.True(server.UseSsl);
Assert.Equal("preserved-site-user", site.UsernameOverride); Assert.Equal("preserved-site-user", site.UsernameOverride);
@@ -188,14 +235,41 @@ public class ConfigTransferServiceTests : IDisposable
ClientId = "client", ClientId = "client",
ClientSecret = "secret" ClientSecret = "secret"
}); });
db.ExportSettings.Add(new ExportSettings db.ExportSettings.Add(new ExportSettings());
db.SourceSystemDefinitions.AddRange(
new SourceSystemDefinition
{ {
SapUsername = "sap-user", Code = "SAP",
SapPassword = "sap-password", DisplayName = "SAP",
Bi1Username = "bi1-user", ConnectionKind = SourceSystemConnectionKinds.SapGateway,
Bi1Password = "bi1-password", IsActive = true,
SageUsername = "sage-user", CentralUsername = "sap-user",
SagePassword = "sage-password" CentralPassword = "sap-password"
},
new SourceSystemDefinition
{
Code = "BI1",
DisplayName = "BI1",
ConnectionKind = SourceSystemConnectionKinds.Hana,
IsActive = true,
CentralUsername = "bi1-user",
CentralPassword = "bi1-password"
},
new SourceSystemDefinition
{
Code = "SAGE",
DisplayName = "SAGE",
ConnectionKind = SourceSystemConnectionKinds.Hana,
IsActive = true,
CentralUsername = "sage-user",
CentralPassword = "sage-password"
},
new SourceSystemDefinition
{
Code = "MANUAL_EXCEL",
DisplayName = "Manual Excel",
ConnectionKind = SourceSystemConnectionKinds.ManualExcel,
IsActive = true
}); });
db.HanaServers.Add(new HanaServer db.HanaServers.Add(new HanaServer
{ {
@@ -203,8 +277,8 @@ public class ConfigTransferServiceTests : IDisposable
Name = "Server A", Name = "Server A",
Host = "hana-a", Host = "hana-a",
Port = 30015, Port = 30015,
Username = "server-user", Username = string.Empty,
Password = "server-password", Password = string.Empty,
DatabaseName = "DB1" DatabaseName = "DB1"
}); });
db.Sites.Add(new Site db.Sites.Add(new Site
@@ -246,14 +320,41 @@ public class ConfigTransferServiceTests : IDisposable
ClientId = "old-client", ClientId = "old-client",
ClientSecret = "preserved-sharepoint-secret" ClientSecret = "preserved-sharepoint-secret"
}); });
db.ExportSettings.Add(new ExportSettings db.ExportSettings.Add(new ExportSettings());
db.SourceSystemDefinitions.AddRange(
new SourceSystemDefinition
{ {
SapUsername = "preserved-sap-user", Code = "SAP",
SapPassword = "preserved-sap-password", DisplayName = "SAP",
Bi1Username = "preserved-bi1-user", ConnectionKind = SourceSystemConnectionKinds.SapGateway,
Bi1Password = "preserved-bi1-password", IsActive = true,
SageUsername = "preserved-sage-user", CentralUsername = "preserved-sap-user",
SagePassword = "preserved-sage-password" CentralPassword = "preserved-sap-password"
},
new SourceSystemDefinition
{
Code = "BI1",
DisplayName = "BI1",
ConnectionKind = SourceSystemConnectionKinds.Hana,
IsActive = true,
CentralUsername = "preserved-bi1-user",
CentralPassword = "preserved-bi1-password"
},
new SourceSystemDefinition
{
Code = "SAGE",
DisplayName = "SAGE",
ConnectionKind = SourceSystemConnectionKinds.Hana,
IsActive = true,
CentralUsername = "preserved-sage-user",
CentralPassword = "preserved-sage-password"
},
new SourceSystemDefinition
{
Code = "MANUAL_EXCEL",
DisplayName = "Manual Excel",
ConnectionKind = SourceSystemConnectionKinds.ManualExcel,
IsActive = true
}); });
db.HanaServers.Add(new HanaServer db.HanaServers.Add(new HanaServer
{ {