refactoring
This commit is contained in:
@@ -173,6 +173,7 @@
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
|
||||
var sites = await db.Sites.Include(s => s.HanaServer).Where(s => s.IsActive).ToListAsync();
|
||||
var sourceSystems = await db.SourceSystemDefinitions.AsNoTracking().ToListAsync();
|
||||
var logs = await db.ExportLogs
|
||||
.GroupBy(l => l.SiteId)
|
||||
.Select(g => g.OrderByDescending(l => l.Timestamp).First())
|
||||
@@ -190,14 +191,15 @@
|
||||
{
|
||||
var log = logs.FirstOrDefault(l => l.SiteId == s.Id);
|
||||
latestAppLogsBySite.TryGetValue(s.Id, out var appLog);
|
||||
var sourceSystem = sourceSystems.FirstOrDefault(x => string.Equals(x.Code, s.SourceSystem, StringComparison.OrdinalIgnoreCase));
|
||||
return new DashboardRow
|
||||
{
|
||||
SiteId = s.Id,
|
||||
Land = s.Land,
|
||||
TSC = s.TSC,
|
||||
Schema = s.Schema,
|
||||
ServerName = string.Equals(s.SourceSystem, "SAP", StringComparison.OrdinalIgnoreCase)
|
||||
? (string.IsNullOrWhiteSpace(s.SapServiceUrl) ? "SAP Gateway" : s.SapServiceUrl)
|
||||
ServerName = string.Equals(sourceSystem?.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase)
|
||||
? ResolveDashboardSapServiceUrl(s, sourceSystems)
|
||||
: s.HanaServer?.Name ?? "",
|
||||
LastStatus = log?.Status ?? "",
|
||||
RowCount = log?.RowCount ?? 0,
|
||||
@@ -319,6 +321,15 @@
|
||||
OpenFile(row.FilePath);
|
||||
}
|
||||
|
||||
private static string ResolveDashboardSapServiceUrl(Site site, List<SourceSystemDefinition> sourceSystems)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(site.SapServiceUrl))
|
||||
return site.SapServiceUrl;
|
||||
|
||||
var sourceSystem = sourceSystems.FirstOrDefault(x => string.Equals(x.Code, site.SourceSystem, StringComparison.OrdinalIgnoreCase));
|
||||
return string.IsNullOrWhiteSpace(sourceSystem?.CentralServiceUrl) ? "SAP Gateway" : sourceSystem.CentralServiceUrl;
|
||||
}
|
||||
|
||||
private void OpenFile(string filePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@page "/settings"
|
||||
@page "/settings"
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using TrafagSalesExporter.Data
|
||||
@using TrafagSalesExporter.Models
|
||||
@@ -23,7 +23,7 @@
|
||||
<MudItem xs="12" md="6">
|
||||
<MudCheckBox @bind-Value="_includeSecretsInExport" Label="Mit Secrets exportieren" />
|
||||
<MudText Typo="Typo.caption">
|
||||
Wenn deaktiviert, bleiben Passwörter und Secrets beim Export leer. Beim Import ohne Secrets werden bestehende Secrets auf dem Zielsystem beibehalten.
|
||||
Wenn deaktiviert, bleiben Passwörter und Secrets beim Export leer. Beim Import ohne Secrets werden bestehende Secrets auf dem Zielsystem beibehalten.
|
||||
</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
@@ -98,74 +98,102 @@
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
<MudText Typo="Typo.h5" Class="mb-2">Zentrale Quellsystem-Zugangsdaten</MudText>
|
||||
<MudText Typo="Typo.h5" Class="mb-2">Quellsysteme</MudText>
|
||||
<MudPaper Class="pa-4 mb-6" Elevation="1">
|
||||
<MudGrid>
|
||||
<MudItem xs="12">
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined">
|
||||
Diese Zugangsdaten werden pro Quellsystem als Standard verwendet. Ein Standort kann sie bei Bedarf mit eigenen Overrides überschreiben.
|
||||
Diese Zugangsdaten werden pro Quellsystem als Standard verwendet. Ein Standort kann sie bei Bedarf mit eigenen Overrides überschreiben.
|
||||
</MudAlert>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudText Typo="Typo.h6" Class="mb-2">SAP</MudText>
|
||||
<MudTextField @bind-Value="_exportSettings.SapUsername" Label="SAP Username" />
|
||||
<MudTextField @bind-Value="_exportSettings.SapPassword" Label="SAP Password" InputType="InputType.Password" />
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" OnClick='@(() => TestCentralCredentials("SAP"))'
|
||||
StartIcon="@Icons.Material.Filled.NetworkCheck" Disabled='@_testingSystems.Contains("SAP")' Class="mt-2">
|
||||
@if (_testingSystems.Contains("SAP"))
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate Class="mr-2" />
|
||||
@("Teste...")
|
||||
}
|
||||
else
|
||||
{
|
||||
@("SAP testen")
|
||||
}
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudText Typo="Typo.h6" Class="mb-2">BI1</MudText>
|
||||
<MudTextField @bind-Value="_exportSettings.Bi1Username" Label="BI1 Username" />
|
||||
<MudTextField @bind-Value="_exportSettings.Bi1Password" Label="BI1 Password" InputType="InputType.Password" />
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" OnClick='@(() => TestCentralCredentials("BI1"))'
|
||||
StartIcon="@Icons.Material.Filled.NetworkCheck" Disabled='@_testingSystems.Contains("BI1")' Class="mt-2">
|
||||
@if (_testingSystems.Contains("BI1"))
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate Class="mr-2" />
|
||||
@("Teste...")
|
||||
}
|
||||
else
|
||||
{
|
||||
@("BI1 testen")
|
||||
}
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudText Typo="Typo.h6" Class="mb-2">SAGE</MudText>
|
||||
<MudTextField @bind-Value="_exportSettings.SageUsername" Label="SAGE Username" />
|
||||
<MudTextField @bind-Value="_exportSettings.SagePassword" Label="SAGE Password" InputType="InputType.Password" />
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" OnClick='@(() => TestCentralCredentials("SAGE"))'
|
||||
StartIcon="@Icons.Material.Filled.NetworkCheck" Disabled='@_testingSystems.Contains("SAGE")' Class="mt-2">
|
||||
@if (_testingSystems.Contains("SAGE"))
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate Class="mr-2" />
|
||||
@("Teste...")
|
||||
}
|
||||
else
|
||||
{
|
||||
@("SAGE testen")
|
||||
}
|
||||
<MudItem xs="12">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" OnClick="AddSourceSystem"
|
||||
StartIcon="@Icons.Material.Filled.Add" Class="mb-3">
|
||||
Quellsystem hinzufuegen
|
||||
</MudButton>
|
||||
<MudTable Items="_sourceSystems" Dense Hover Striped Breakpoint="Breakpoint.Md">
|
||||
<HeaderContent>
|
||||
<MudTh>Code</MudTh>
|
||||
<MudTh>Name</MudTh>
|
||||
<MudTh>Anschlussart</MudTh>
|
||||
<MudTh>Zentrale URL</MudTh>
|
||||
<MudTh>User</MudTh>
|
||||
<MudTh>Aktiv</MudTh>
|
||||
<MudTh>Test</MudTh>
|
||||
<MudTh></MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Code</MudTd>
|
||||
<MudTd>@context.DisplayName</MudTd>
|
||||
<MudTd>@GetConnectionKindLabel(context.ConnectionKind)</MudTd>
|
||||
<MudTd>@GetServiceUrlSummary(context)</MudTd>
|
||||
<MudTd>@GetUsernameSummary(context)</MudTd>
|
||||
<MudTd>
|
||||
@if (context.IsActive)
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.Cancel" Color="Color.Default" Size="Size.Small" />
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
@if (!UsesManualImport(context))
|
||||
{
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" Size="Size.Small"
|
||||
OnClick='@(() => TestCentralCredentials(context.Code))'
|
||||
Disabled='@_testingSystems.Contains(context.Code)'>
|
||||
@(_testingSystems.Contains(context.Code) ? "Teste..." : "Testen")
|
||||
</MudButton>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Primary" Size="Size.Small"
|
||||
OnClick="() => EditSourceSystem(context)" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small"
|
||||
OnClick="() => RemoveSourceSystem(context)" />
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveExportSettings"
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveSourceSystems"
|
||||
StartIcon="@Icons.Material.Filled.Save">
|
||||
Speichern
|
||||
Quellsysteme speichern
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
<MudDialog @bind-Visible="_sourceSystemDialogVisible" Options="_sourceSystemDialogOptions">
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6">@(_editingSourceSystem.Id == 0 ? "Quellsystem hinzufuegen" : "Quellsystem bearbeiten")</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudTextField @bind-Value="_editingSourceSystem.Code" Label="Code" Required />
|
||||
<MudTextField @bind-Value="_editingSourceSystem.DisplayName" Label="Name" Required />
|
||||
<MudSelect T="string" @bind-Value="_editingSourceSystem.ConnectionKind" Label="Anschlussart" Required>
|
||||
@foreach (var kind in SourceSystemConnectionKinds.All)
|
||||
{
|
||||
<MudSelectItem Value="@kind">@GetConnectionKindLabel(kind)</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@if (UsesSapGateway(_editingSourceSystem))
|
||||
{
|
||||
<MudTextField @bind-Value="_editingSourceSystem.CentralServiceUrl" Label="Zentrale SAP Service URL"
|
||||
HelperText="Zentrale Standard-URL fuer SAP Gateway. Ein Standort darf sie nur bei Bedarf ueberschreiben." />
|
||||
}
|
||||
<MudTextField @bind-Value="_editingSourceSystem.CentralUsername" Label="Zentraler Username" />
|
||||
<MudTextField @bind-Value="_editingSourceSystem.CentralPassword" Label="Zentrales Passwort" InputType="InputType.Password" />
|
||||
<MudCheckBox @bind-Value="_editingSourceSystem.IsActive" Label="Aktiv" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="CloseSourceSystemDialog">Abbrechen</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveSourceSystemEdit">Uebernehmen</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
<MudText Typo="Typo.h5" Class="mb-2">Wechselkurse</MudText>
|
||||
<MudPaper Class="pa-4 mb-6" Elevation="1">
|
||||
<MudText Typo="Typo.body2" Class="mb-3">
|
||||
@@ -250,7 +278,7 @@
|
||||
<MudItem xs="12" md="4">
|
||||
<MudSwitch @bind-Value="_exportSettings.DebugLoggingEnabled" Label="Debug Live-Logging" Color="Color.Warning" />
|
||||
<MudText Typo="Typo.caption">
|
||||
Schreibt zusätzliche technische Fortschrittsmeldungen für HANA- und SAP-Lesevorgänge ins Dashboard und in die Logs.
|
||||
Schreibt zusätzliche technische Fortschrittsmeldungen für HANA- und SAP-Lesevorgänge ins Dashboard und in die Logs.
|
||||
</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
@@ -285,6 +313,8 @@
|
||||
@code {
|
||||
private SharePointConfig _spConfig = new();
|
||||
private ExportSettings _exportSettings = new();
|
||||
private List<SourceSystemDefinition> _sourceSystems = [];
|
||||
private SourceSystemDefinition _editingSourceSystem = new();
|
||||
private bool _testingSp;
|
||||
private bool _includeSecretsInExport;
|
||||
private bool _exportingConfig;
|
||||
@@ -293,12 +323,15 @@
|
||||
private string _sharePointTestPreview = string.Empty;
|
||||
private List<CurrencyExchangeRate> _exchangeRates = [];
|
||||
private readonly HashSet<string> _testingSystems = [];
|
||||
private bool _sourceSystemDialogVisible;
|
||||
private readonly DialogOptions _sourceSystemDialogOptions = new() { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
_spConfig = await db.SharePointConfigs.FirstOrDefaultAsync() ?? new SharePointConfig();
|
||||
_exportSettings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings();
|
||||
_sourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync();
|
||||
_exchangeRates = await db.CurrencyExchangeRates
|
||||
.OrderBy(x => x.FromCurrency)
|
||||
.ThenBy(x => x.ToCurrency)
|
||||
@@ -370,18 +403,138 @@
|
||||
existing.DebugLoggingEnabled = _exportSettings.DebugLoggingEnabled;
|
||||
existing.LocalSiteExportFolder = _exportSettings.LocalSiteExportFolder;
|
||||
existing.LocalConsolidatedExportFolder = _exportSettings.LocalConsolidatedExportFolder;
|
||||
existing.SapUsername = _exportSettings.SapUsername;
|
||||
existing.SapPassword = _exportSettings.SapPassword;
|
||||
existing.Bi1Username = _exportSettings.Bi1Username;
|
||||
existing.Bi1Password = _exportSettings.Bi1Password;
|
||||
existing.SageUsername = _exportSettings.SageUsername;
|
||||
existing.SagePassword = _exportSettings.SagePassword;
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
TimerService.Recalculate();
|
||||
Snackbar.Add("Export Einstellungen gespeichert", Severity.Success);
|
||||
}
|
||||
|
||||
private void AddSourceSystem()
|
||||
{
|
||||
_editingSourceSystem = new SourceSystemDefinition
|
||||
{
|
||||
Code = string.Empty,
|
||||
DisplayName = string.Empty,
|
||||
ConnectionKind = SourceSystemConnectionKinds.Hana,
|
||||
IsActive = true
|
||||
};
|
||||
_sourceSystemDialogVisible = true;
|
||||
}
|
||||
|
||||
private void EditSourceSystem(SourceSystemDefinition definition)
|
||||
{
|
||||
_editingSourceSystem = new SourceSystemDefinition
|
||||
{
|
||||
Id = definition.Id,
|
||||
Code = definition.Code,
|
||||
DisplayName = definition.DisplayName,
|
||||
ConnectionKind = definition.ConnectionKind,
|
||||
IsActive = definition.IsActive,
|
||||
CentralServiceUrl = definition.CentralServiceUrl,
|
||||
CentralUsername = definition.CentralUsername,
|
||||
CentralPassword = definition.CentralPassword
|
||||
};
|
||||
_sourceSystemDialogVisible = true;
|
||||
}
|
||||
|
||||
private void SaveSourceSystemEdit()
|
||||
{
|
||||
_editingSourceSystem.Code = NormalizeSourceSystemCode(_editingSourceSystem.Code);
|
||||
_editingSourceSystem.DisplayName = NormalizeConfigValue(_editingSourceSystem.DisplayName);
|
||||
_editingSourceSystem.ConnectionKind = NormalizeConnectionKind(_editingSourceSystem.ConnectionKind);
|
||||
_editingSourceSystem.CentralServiceUrl = NormalizeConfigValue(_editingSourceSystem.CentralServiceUrl);
|
||||
_editingSourceSystem.CentralUsername = NormalizeConfigValue(_editingSourceSystem.CentralUsername);
|
||||
_editingSourceSystem.CentralPassword = _editingSourceSystem.CentralPassword ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_editingSourceSystem.Code) || string.IsNullOrWhiteSpace(_editingSourceSystem.DisplayName))
|
||||
{
|
||||
Snackbar.Add("Code und Name fuer das Quellsystem sind Pflicht.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_sourceSystems.Any(x => x.Id != _editingSourceSystem.Id && x.Code == _editingSourceSystem.Code))
|
||||
{
|
||||
Snackbar.Add($"Quellsystem-Code doppelt vorhanden: {_editingSourceSystem.Code}", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_editingSourceSystem.Id == 0)
|
||||
{
|
||||
_sourceSystems.Add(_editingSourceSystem);
|
||||
}
|
||||
else
|
||||
{
|
||||
var existing = _sourceSystems.FirstOrDefault(x => x.Id == _editingSourceSystem.Id);
|
||||
if (existing is not null)
|
||||
{
|
||||
existing.Code = _editingSourceSystem.Code;
|
||||
existing.DisplayName = _editingSourceSystem.DisplayName;
|
||||
existing.ConnectionKind = _editingSourceSystem.ConnectionKind;
|
||||
existing.IsActive = _editingSourceSystem.IsActive;
|
||||
existing.CentralServiceUrl = _editingSourceSystem.CentralServiceUrl;
|
||||
existing.CentralUsername = _editingSourceSystem.CentralUsername;
|
||||
existing.CentralPassword = _editingSourceSystem.CentralPassword;
|
||||
}
|
||||
}
|
||||
|
||||
_sourceSystems = _sourceSystems.OrderBy(x => x.Code).ToList();
|
||||
_sourceSystemDialogVisible = false;
|
||||
}
|
||||
|
||||
private void CloseSourceSystemDialog()
|
||||
{
|
||||
_sourceSystemDialogVisible = false;
|
||||
}
|
||||
|
||||
private void RemoveSourceSystem(SourceSystemDefinition definition)
|
||||
{
|
||||
_sourceSystems.Remove(definition);
|
||||
}
|
||||
|
||||
private async Task SaveSourceSystems()
|
||||
{
|
||||
var normalized = _sourceSystems
|
||||
.Select(x => new SourceSystemDefinition
|
||||
{
|
||||
Id = x.Id,
|
||||
Code = NormalizeSourceSystemCode(x.Code),
|
||||
DisplayName = NormalizeConfigValue(x.DisplayName),
|
||||
ConnectionKind = NormalizeConnectionKind(x.ConnectionKind),
|
||||
IsActive = x.IsActive,
|
||||
CentralServiceUrl = NormalizeConfigValue(x.CentralServiceUrl),
|
||||
CentralUsername = NormalizeConfigValue(x.CentralUsername),
|
||||
CentralPassword = x.CentralPassword ?? string.Empty
|
||||
})
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.Code))
|
||||
.ToList();
|
||||
|
||||
if (normalized.Any(x => string.IsNullOrWhiteSpace(x.DisplayName)))
|
||||
{
|
||||
Snackbar.Add("Jedes Quellsystem braucht einen Anzeigenamen.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var duplicates = normalized
|
||||
.GroupBy(x => x.Code)
|
||||
.FirstOrDefault(g => g.Count() > 1);
|
||||
if (duplicates is not null)
|
||||
{
|
||||
Snackbar.Add($"Quellsystem-Code doppelt vorhanden: {duplicates.Key}", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
var existing = await db.SourceSystemDefinitions.ToListAsync();
|
||||
if (existing.Count > 0)
|
||||
db.SourceSystemDefinitions.RemoveRange(existing);
|
||||
|
||||
db.SourceSystemDefinitions.AddRange(normalized);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_sourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync();
|
||||
Snackbar.Add("Quellsysteme gespeichert", Severity.Success);
|
||||
}
|
||||
|
||||
private void AddExchangeRate()
|
||||
{
|
||||
_exchangeRates.Add(new CurrencyExchangeRate
|
||||
@@ -493,6 +646,7 @@
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
_spConfig = await db.SharePointConfigs.FirstOrDefaultAsync() ?? new SharePointConfig();
|
||||
_exportSettings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings();
|
||||
_sourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync();
|
||||
_exchangeRates = await db.CurrencyExchangeRates
|
||||
.OrderBy(x => x.FromCurrency)
|
||||
.ThenBy(x => x.ToCurrency)
|
||||
@@ -513,61 +667,72 @@
|
||||
|
||||
private async Task TestCentralCredentials(string sourceSystem)
|
||||
{
|
||||
if (sourceSystem == "SAP")
|
||||
var definition = _sourceSystems.FirstOrDefault(x => string.Equals(x.Code, sourceSystem, StringComparison.OrdinalIgnoreCase));
|
||||
if (definition is null)
|
||||
{
|
||||
await TestCentralSapCredentials();
|
||||
Snackbar.Add($"Quellsystem '{sourceSystem}' nicht gefunden.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
await TestCentralHanaCredentials(sourceSystem);
|
||||
if (string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await TestCentralSapCredentials(definition);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.Hana, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await TestCentralHanaCredentials(definition);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TestCentralHanaCredentials(string sourceSystem)
|
||||
private async Task TestCentralHanaCredentials(SourceSystemDefinition definition)
|
||||
{
|
||||
var sourceSystem = definition.Code;
|
||||
if (!_testingSystems.Add(sourceSystem))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var username = GetCentralUsername(sourceSystem);
|
||||
var password = GetCentralPassword(sourceSystem);
|
||||
var username = definition.CentralUsername;
|
||||
var password = definition.CentralPassword;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
Snackbar.Add($"Für {sourceSystem} sind keine zentralen Zugangsdaten gepflegt.", Severity.Warning);
|
||||
Snackbar.Add($"Für {sourceSystem} sind keine zentralen Zugangsdaten gepflegt.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
var site = await db.Sites
|
||||
.Include(s => s.HanaServer)
|
||||
.Where(s => (string.IsNullOrWhiteSpace(s.SourceSystem) ? "SAP" : s.SourceSystem) == sourceSystem)
|
||||
.OrderBy(s => s.Land)
|
||||
var centralServer = await db.HanaServers
|
||||
.Where(s => s.SourceSystem == sourceSystem)
|
||||
.OrderBy(s => s.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (site?.HanaServer is null)
|
||||
if (centralServer is null || string.IsNullOrWhiteSpace(centralServer.Host))
|
||||
{
|
||||
Snackbar.Add($"Kein Standort mit Quellsystem {sourceSystem} und HANA-Verbindung gefunden.", Severity.Warning);
|
||||
Snackbar.Add($"Keine zentrale HANA-Konfiguration fuer {sourceSystem} gefunden.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var testServer = new HanaServer
|
||||
{
|
||||
SourceSystem = sourceSystem,
|
||||
Name = $"{sourceSystem} Central Test",
|
||||
Host = site.HanaServer.Host,
|
||||
Port = site.HanaServer.Port,
|
||||
Host = centralServer.Host,
|
||||
Port = centralServer.Port,
|
||||
Username = username.Trim(),
|
||||
Password = password.Trim(),
|
||||
DatabaseName = site.HanaServer.DatabaseName,
|
||||
UseSsl = site.HanaServer.UseSsl,
|
||||
ValidateCertificate = site.HanaServer.ValidateCertificate,
|
||||
AdditionalParams = site.HanaServer.AdditionalParams
|
||||
DatabaseName = centralServer.DatabaseName,
|
||||
UseSsl = centralServer.UseSsl,
|
||||
ValidateCertificate = centralServer.ValidateCertificate,
|
||||
AdditionalParams = centralServer.AdditionalParams
|
||||
};
|
||||
|
||||
var result = await Task.Run(() => HanaService.TestConnectionDetailed(testServer));
|
||||
if (result.Success)
|
||||
{
|
||||
Snackbar.Add($"{sourceSystem}: Verbindung erfolgreich über Standort '{site.Land}'.", Severity.Success);
|
||||
Snackbar.Add($"{sourceSystem}: Zentrale HANA-Verbindung erfolgreich.", Severity.Success);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -580,42 +745,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TestCentralSapCredentials()
|
||||
private async Task TestCentralSapCredentials(SourceSystemDefinition definition)
|
||||
{
|
||||
const string sourceSystem = "SAP";
|
||||
var sourceSystem = definition.Code;
|
||||
if (!_testingSystems.Add(sourceSystem))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var username = GetCentralUsername(sourceSystem);
|
||||
var password = GetCentralPassword(sourceSystem);
|
||||
var username = definition.CentralUsername;
|
||||
var password = definition.CentralPassword;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
Snackbar.Add("Für SAP sind keine zentralen Gateway-Zugangsdaten gepflegt.", Severity.Warning);
|
||||
Snackbar.Add("Für SAP sind keine zentralen Gateway-Zugangsdaten gepflegt.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
var site = await db.Sites
|
||||
.Where(s => (string.IsNullOrWhiteSpace(s.SourceSystem) ? "SAP" : s.SourceSystem) == sourceSystem
|
||||
&& !string.IsNullOrWhiteSpace(s.SapServiceUrl))
|
||||
.OrderBy(s => s.Land)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (site is null)
|
||||
if (string.IsNullOrWhiteSpace(definition.CentralServiceUrl))
|
||||
{
|
||||
Snackbar.Add("Kein SAP-Standort mit Service URL gefunden.", Severity.Warning);
|
||||
Snackbar.Add($"Fuer {sourceSystem} ist keine zentrale SAP Service URL gepflegt.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
await SapGatewayService.TestConnectionAsync(site.SapServiceUrl, username.Trim(), password.Trim());
|
||||
Snackbar.Add($"SAP: Gateway-Verbindung erfolgreich über Standort '{site.Land}'.", Severity.Success);
|
||||
await SapGatewayService.TestConnectionAsync(definition.CentralServiceUrl, username.Trim(), password.Trim());
|
||||
Snackbar.Add($"{sourceSystem}: Zentrale SAP Gateway-Verbindung erfolgreich.", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"SAP: {ex.Message}", Severity.Error);
|
||||
Snackbar.Add($"{sourceSystem}: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -623,19 +781,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
private string GetCentralUsername(string sourceSystem) => sourceSystem switch
|
||||
private static string NormalizeSourceSystemCode(string? code) => NormalizeConfigValue(code).ToUpperInvariant();
|
||||
|
||||
private static string NormalizeConnectionKind(string? connectionKind)
|
||||
=> SourceSystemConnectionKinds.All.Contains(connectionKind ?? string.Empty, StringComparer.OrdinalIgnoreCase)
|
||||
? (connectionKind ?? string.Empty).Trim().ToUpperInvariant()
|
||||
: SourceSystemConnectionKinds.Hana;
|
||||
|
||||
private static string GetConnectionKindLabel(string connectionKind) => connectionKind switch
|
||||
{
|
||||
"BI1" => _exportSettings.Bi1Username,
|
||||
"SAGE" => _exportSettings.SageUsername,
|
||||
_ => _exportSettings.SapUsername
|
||||
SourceSystemConnectionKinds.Hana => "HANA",
|
||||
SourceSystemConnectionKinds.SapGateway => "SAP Gateway",
|
||||
SourceSystemConnectionKinds.ManualExcel => "Manual Excel",
|
||||
_ => connectionKind
|
||||
};
|
||||
|
||||
private string GetCentralPassword(string sourceSystem) => sourceSystem switch
|
||||
{
|
||||
"BI1" => _exportSettings.Bi1Password,
|
||||
"SAGE" => _exportSettings.SagePassword,
|
||||
_ => _exportSettings.SapPassword
|
||||
};
|
||||
private static bool UsesManualImport(SourceSystemDefinition definition)
|
||||
=> string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.ManualExcel, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool UsesSapGateway(SourceSystemDefinition definition)
|
||||
=> string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static string GetServiceUrlSummary(SourceSystemDefinition definition)
|
||||
=> string.IsNullOrWhiteSpace(definition.CentralServiceUrl) ? "-" : definition.CentralServiceUrl;
|
||||
|
||||
private static string GetUsernameSummary(SourceSystemDefinition definition)
|
||||
=> string.IsNullOrWhiteSpace(definition.CentralUsername) ? "-" : definition.CentralUsername;
|
||||
|
||||
private static string NormalizeConfigValue(string? value) => value?.Trim() ?? string.Empty;
|
||||
|
||||
@@ -664,3 +835,4 @@
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@page "/standorte"
|
||||
@page "/standorte"
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using System.Text.Json
|
||||
@@ -17,27 +17,30 @@
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Standorte</MudText>
|
||||
|
||||
<MudText Typo="Typo.h5" Class="mb-2">HANA Server</MudText>
|
||||
<MudText Typo="Typo.h5" Class="mb-2">Zentrale HANA-Technik</MudText>
|
||||
<MudPaper Class="pa-4 mb-6" Elevation="1">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="AddServer" Class="mb-3">
|
||||
Server hinzufügen
|
||||
</MudButton>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Class="mb-3">
|
||||
Hier erscheinen nur Quellsysteme mit Anschlussart HANA. SAP wird zentral unter Settings -> Quellsysteme gepflegt.
|
||||
Standorte mit `BI1` oder `SAGE` verwenden diese technischen HANA-Werte automatisch. Im Standort selbst bleiben nur Schema, TSC, Land und optionale Username-/Password-Overrides.
|
||||
</MudAlert>
|
||||
<MudText Typo="Typo.body2" Class="mb-3">
|
||||
Neue HANA-Zeilen entstehen aus den zentral gepflegten Quellsystemen. Falls hier etwas fehlt, lege das Quellsystem in Settings -> Quellsysteme mit Anschlussart `HANA` an.
|
||||
</MudText>
|
||||
|
||||
<MudTable Items="_servers" Dense Hover Striped>
|
||||
<HeaderContent>
|
||||
<MudTh>Quellsystem</MudTh>
|
||||
<MudTh>Name</MudTh>
|
||||
<MudTh>Host</MudTh>
|
||||
<MudTh>Port</MudTh>
|
||||
<MudTh>Username</MudTh>
|
||||
<MudTh>Verbindungsstatus</MudTh>
|
||||
<MudTh>Aktionen</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.SourceSystem</MudTd>
|
||||
<MudTd>@context.Name</MudTd>
|
||||
<MudTd>@context.Host</MudTd>
|
||||
<MudTd>@context.Port</MudTd>
|
||||
<MudTd>@context.Username</MudTd>
|
||||
<MudTd>
|
||||
@if (_connectionStatus.TryGetValue(context.Id, out var status))
|
||||
{
|
||||
@@ -68,7 +71,7 @@
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="AddSite" Class="mb-3">
|
||||
Neuen Standort hinzufügen
|
||||
Neuen Standort hinzufügen
|
||||
</MudButton>
|
||||
|
||||
<MudTable Items="_sites" Dense Hover Striped>
|
||||
@@ -109,22 +112,21 @@
|
||||
|
||||
<MudDialog @bind-Visible="_serverDialogVisible" Options="_dialogOptions">
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6">@(_editingServer.Id == 0 ? "Server hinzufügen" : "Server bearbeiten")</MudText>
|
||||
<MudText Typo="Typo.h6">Zentrale HANA-Technik bearbeiten</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudTextField Value="_editingServer.SourceSystem" Label="Quellsystem" ReadOnly />
|
||||
<MudTextField @bind-Value="_editingServer.Name" Label="Name" Required />
|
||||
<MudTextField @bind-Value="_editingServer.Host" Label="Host" Required
|
||||
HelperText="IP oder Hostname (ohne Protokoll)" />
|
||||
<MudNumericField @bind-Value="_editingServer.Port" Label="Port"
|
||||
HelperText="Typisch 30015 (Tenant), 30013 (SystemDB), 3xx15 für Instanz xx" />
|
||||
<MudTextField @bind-Value="_editingServer.Username" Label="Username" />
|
||||
<MudTextField @bind-Value="_editingServer.Password" Label="Password" InputType="InputType.Password" />
|
||||
HelperText="Typisch 30015 (Tenant), 30013 (SystemDB), 3xx15 für Instanz xx" />
|
||||
<MudTextField @bind-Value="_editingServer.DatabaseName" Label="Database Name (MDC)"
|
||||
HelperText="Nur bei Multi-Tenant Setup angeben, sonst leer lassen" />
|
||||
<MudSwitch @bind-Value="_editingServer.UseSsl" Label="SSL/TLS verwenden (encrypt=true)" Color="Color.Primary" />
|
||||
<MudSwitch @bind-Value="_editingServer.ValidateCertificate" Label="SSL-Zertifikat validieren" Color="Color.Primary"
|
||||
Disabled="!_editingServer.UseSsl" />
|
||||
<MudTextField @bind-Value="_editingServer.AdditionalParams" Label="Zusätzliche Parameter"
|
||||
<MudTextField @bind-Value="_editingServer.AdditionalParams" Label="Zusätzliche Parameter"
|
||||
HelperText="Optional, z.B. sslCryptoProvider=openssl;communicationTimeout=0" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
@@ -135,16 +137,16 @@
|
||||
|
||||
<MudDialog @bind-Visible="_siteDialogVisible" Options="_dialogOptions">
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6">@(_editingSite.Id == 0 ? "Standort hinzufügen" : "Standort bearbeiten")</MudText>
|
||||
<MudText Typo="Typo.h6">@(_editingSite.Id == 0 ? "Standort hinzufügen" : "Standort bearbeiten")</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudTextField @bind-Value="_editingSite.Schema" Label="Schema" Required />
|
||||
<MudTextField @bind-Value="_editingSite.TSC" Label="TSC" Required />
|
||||
<MudTextField @bind-Value="_editingSite.Land" Label="Land" Required />
|
||||
<MudSelect @bind-Value="_editingSite.SourceSystem" Label="Quellsystem" Required>
|
||||
@foreach (var system in _sourceSystems)
|
||||
@foreach (var system in GetAvailableSourceSystems())
|
||||
{
|
||||
<MudSelectItem Value="@system">@system</MudSelectItem>
|
||||
<MudSelectItem Value="@system.Code">@GetSourceSystemLabel(system)</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudTextField @bind-Value="_editingSite.UsernameOverride" Label="Username Override"
|
||||
@@ -152,7 +154,7 @@
|
||||
<MudTextField @bind-Value="_editingSite.PasswordOverride" Label="Password Override" InputType="InputType.Password"
|
||||
HelperText="Optional. Wenn leer, wird das zentrale Passwort des Quellsystems verwendet." />
|
||||
<MudTextField @bind-Value="_editingSite.LocalExportFolderOverride" Label="Lokaler Exportpfad Override"
|
||||
HelperText="Optional. Wenn leer, wird der zentrale Standardpfad für Standort-Dateien verwendet." />
|
||||
HelperText="Optional. Wenn leer, wird der zentrale Standardpfad für Standort-Dateien verwendet." />
|
||||
<MudCheckBox @bind-Value="_editingSite.IsActive" Label="Aktiv" />
|
||||
|
||||
<MudDivider Class="my-4" />
|
||||
@@ -161,10 +163,11 @@
|
||||
{
|
||||
<MudText Typo="Typo.h6" Class="mb-2">SAP Gateway</MudText>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Class="mb-3">
|
||||
Die Service-URL zeigt auf den OData-Service. Die verfügbaren Entity Sets werden nur per Knopfdruck aktualisiert und lokal zwischengespeichert.
|
||||
Die Service-URL zeigt auf den OData-Service. Die verfügbaren Entity Sets werden nur per Knopfdruck aktualisiert und lokal zwischengespeichert.
|
||||
</MudAlert>
|
||||
<MudTextField @bind-Value="_editingSite.SapServiceUrl" Label="SAP Service URL" Required
|
||||
HelperText="z.B. http://server:8000/sap/opu/odata/sap/ZPOWERBI_EINKAUF_SRV/" />
|
||||
<MudText Typo="Typo.body2">Zentrale SAP Service URL: @GetCentralSapServiceUrlSummary(_editingSite.SourceSystem)</MudText>
|
||||
<MudTextField @bind-Value="_editingSite.SapServiceUrl" Label="SAP Service URL Override"
|
||||
HelperText="Optional. Wenn leer, wird die zentrale SAP Service URL des Quellsystems verwendet." />
|
||||
<MudStack Row Spacing="2" Class="mb-3">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" OnClick="RefreshSapEntitySets"
|
||||
StartIcon="@Icons.Material.Filled.Refresh" Disabled="_refreshingSapEntitySets">
|
||||
@@ -188,16 +191,16 @@
|
||||
<MudDivider Class="my-4" />
|
||||
<MudStack Row Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-2">
|
||||
<MudText Typo="Typo.h6">SAP Quellen</MudText>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddSapSource">Quelle hinzufügen</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddSapSource">Quelle hinzufügen</MudButton>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.caption" Class="mb-2">
|
||||
Pro Quelle Alias und Entity Set definieren. Joins verwenden links/rechts kommagetrennte Schlüsselfelder wie `VBELN,POSNR`. Feldmappings erwarten `Alias.Feldname` oder Konstanten wie `=SAP`.
|
||||
Pro Quelle Alias und Entity Set definieren. Joins verwenden links/rechts kommagetrennte Schlüsselfelder wie `VBELN,POSNR`. Feldmappings erwarten `Alias.Feldname` oder Konstanten wie `=SAP`.
|
||||
</MudText>
|
||||
<MudTable Items="_sapSources" Dense Hover Striped>
|
||||
<HeaderContent>
|
||||
<MudTh>Alias</MudTh>
|
||||
<MudTh>Entity Set</MudTh>
|
||||
<MudTh>Primär</MudTh>
|
||||
<MudTh>Primär</MudTh>
|
||||
<MudTh>Aktiv</MudTh>
|
||||
<MudTh>Aktionen</MudTh>
|
||||
</HeaderContent>
|
||||
@@ -225,7 +228,7 @@
|
||||
OnClick="AutoMatchSapJoins">
|
||||
Auto-Match
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddSapJoin">Join hinzufügen</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddSapJoin">Join hinzufügen</MudButton>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
<MudTable Items="_sapJoins" Dense Hover Striped>
|
||||
@@ -305,11 +308,11 @@
|
||||
@("Felder aus Quellen laden")
|
||||
}
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddSapMapping">Mapping hinzufügen</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddSapMapping">Mapping hinzufügen</MudButton>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.caption" Class="mb-2">
|
||||
Source Expressions werden aus den hinzugefügten SAP-Quellen als `Alias.Feldname` gelesen. Vorhandene manuelle Werte bleiben auswählbar.
|
||||
Source Expressions werden aus den hinzugefügten SAP-Quellen als `Alias.Feldname` gelesen. Vorhandene manuelle Werte bleiben auswählbar.
|
||||
</MudText>
|
||||
<MudTable Items="_sapMappings" Dense Hover Striped>
|
||||
<HeaderContent>
|
||||
@@ -346,7 +349,7 @@
|
||||
{
|
||||
<MudText Typo="Typo.h6" Class="mb-2">Manueller Excel-Import</MudText>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Class="mb-3">
|
||||
Für diesen Standort wird keine SAP- oder HANA-Verbindung verwendet. Es wird die hier hinterlegte Excel-Datei gelesen und in `CentralSalesRecords` übernommen.
|
||||
Für diesen Standort wird keine SAP- oder HANA-Verbindung verwendet. Es wird die hier hinterlegte Excel-Datei gelesen und in `CentralSalesRecords` übernommen.
|
||||
</MudAlert>
|
||||
<InputFile OnChange="UploadManualImportFileAsync" accept=".xlsx" />
|
||||
@if (_uploadingManualImport)
|
||||
@@ -371,23 +374,12 @@
|
||||
{
|
||||
<MudText Typo="Typo.h6" Class="mb-2">HANA-Verbindung</MudText>
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Class="mb-3">
|
||||
Host, Port und technische HANA-Parameter kommen von dieser Verbindung. Username und Password hier dienen nur noch als Fallback für bestehende Einträge.
|
||||
Die technische HANA-Verbindung kommt aus der zentralen HANA-Konfiguration des Quellsystems. Im Standort selbst pflegst du nur fachliche Standortdaten und optionale Username-/Password-Overrides.
|
||||
</MudAlert>
|
||||
<MudTextField @bind-Value="_editingSiteServer.Name" Label="Verbindungsname" Required
|
||||
HelperText="Interner Anzeigename für diesen Standort" />
|
||||
<MudTextField @bind-Value="_editingSiteServer.Host" Label="Host oder ServerNode" Required
|
||||
HelperText="z.B. hana01 oder hana01:30015 oder derselbe HanaServer-Wert wie in Power BI" />
|
||||
<MudNumericField @bind-Value="_editingSiteServer.Port" Label="Port"
|
||||
HelperText="Wird ignoriert, wenn im Host bereits ein Port enthalten ist" />
|
||||
<MudTextField @bind-Value="_editingSiteServer.Username" Label="Username" />
|
||||
<MudTextField @bind-Value="_editingSiteServer.Password" Label="Password" InputType="InputType.Password" />
|
||||
<MudTextField @bind-Value="_editingSiteServer.DatabaseName" Label="Database Name (MDC)"
|
||||
HelperText="Nur bei Multi-Tenant Setup angeben, sonst leer lassen" />
|
||||
<MudSwitch @bind-Value="_editingSiteServer.UseSsl" Label="SSL/TLS verwenden (encrypt=true)" Color="Color.Primary" />
|
||||
<MudSwitch @bind-Value="_editingSiteServer.ValidateCertificate" Label="SSL-Zertifikat validieren" Color="Color.Primary"
|
||||
Disabled="!_editingSiteServer.UseSsl" />
|
||||
<MudTextField @bind-Value="_editingSiteServer.AdditionalParams" Label="Zusätzliche Parameter"
|
||||
HelperText="Optional, z.B. sslCryptoProvider=openssl;communicationTimeout=0" />
|
||||
<MudText Typo="Typo.body2">Aktive Zentralverbindung: @GetCentralHanaSummary(_editingSite.SourceSystem)</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mt-2">
|
||||
Host, Port, SSL und technische Parameter bearbeitest du oben in der zentralen HANA-Konfiguration.
|
||||
</MudText>
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
@@ -397,10 +389,10 @@
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
private readonly string[] _sourceSystems = ["SAP", "BI1", "SAGE", "MANUAL_EXCEL"];
|
||||
private readonly Dictionary<int, ConnectionTestResult> _connectionStatus = new();
|
||||
private List<HanaServer> _servers = new();
|
||||
private List<Site> _sites = new();
|
||||
private List<SourceSystemDefinition> _sourceSystemDefinitions = new();
|
||||
private List<string> _sapEntitySetsCache = [];
|
||||
private List<string> _sapAvailableSourceExpressions = [];
|
||||
private Dictionary<string, List<string>> _sapSourceFieldMap = new(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -413,7 +405,6 @@
|
||||
.ToArray();
|
||||
private HanaServer _editingServer = new();
|
||||
private Site _editingSite = new();
|
||||
private HanaServer _editingSiteServer = new();
|
||||
private bool _serverDialogVisible;
|
||||
private bool _siteDialogVisible;
|
||||
private bool _refreshingSapEntitySets;
|
||||
@@ -431,16 +422,17 @@
|
||||
private async Task LoadDataAsync()
|
||||
{
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
_servers = await db.HanaServers.OrderBy(s => s.Name).ToListAsync();
|
||||
_sourceSystemDefinitions = await db.SourceSystemDefinitions
|
||||
.OrderBy(x => x.Code)
|
||||
.ToListAsync();
|
||||
_servers = await db.HanaServers
|
||||
.Where(s => GetHanaSourceSystemCodes().Contains(s.SourceSystem))
|
||||
.OrderBy(s => s.SourceSystem)
|
||||
.ThenBy(s => s.Name)
|
||||
.ToListAsync();
|
||||
_sites = await db.Sites.Include(s => s.HanaServer).OrderBy(s => s.Land).ToListAsync();
|
||||
}
|
||||
|
||||
private void AddServer()
|
||||
{
|
||||
_editingServer = new HanaServer { Port = 30015 };
|
||||
_serverDialogVisible = true;
|
||||
}
|
||||
|
||||
private void EditServer(HanaServer server)
|
||||
{
|
||||
_editingServer = CloneServer(server);
|
||||
@@ -455,21 +447,50 @@
|
||||
_savingServer = true;
|
||||
try
|
||||
{
|
||||
_editingServer.SourceSystem = string.IsNullOrWhiteSpace(_editingServer.SourceSystem)
|
||||
? GetHanaSourceSystemCodes().FirstOrDefault() ?? string.Empty
|
||||
: _editingServer.SourceSystem.Trim().ToUpperInvariant();
|
||||
_editingServer.Name = string.IsNullOrWhiteSpace(_editingServer.Name) ? _editingServer.SourceSystem : _editingServer.Name.Trim();
|
||||
_editingServer.Host = _editingServer.Host.Trim();
|
||||
_editingServer.DatabaseName = _editingServer.DatabaseName.Trim();
|
||||
_editingServer.AdditionalParams = _editingServer.AdditionalParams.Trim();
|
||||
_editingServer.Username = string.Empty;
|
||||
_editingServer.Password = string.Empty;
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
if (_editingServer.Id == 0)
|
||||
{
|
||||
db.HanaServers.Add(_editingServer);
|
||||
var existingForSourceSystem = await db.HanaServers
|
||||
.OrderBy(x => x.Id)
|
||||
.FirstOrDefaultAsync(x => x.SourceSystem == _editingServer.SourceSystem);
|
||||
|
||||
if (existingForSourceSystem is null)
|
||||
{
|
||||
db.HanaServers.Add(_editingServer);
|
||||
}
|
||||
else
|
||||
{
|
||||
existingForSourceSystem.Name = _editingServer.Name;
|
||||
existingForSourceSystem.Host = _editingServer.Host;
|
||||
existingForSourceSystem.Port = _editingServer.Port;
|
||||
existingForSourceSystem.Username = string.Empty;
|
||||
existingForSourceSystem.Password = string.Empty;
|
||||
existingForSourceSystem.DatabaseName = _editingServer.DatabaseName;
|
||||
existingForSourceSystem.UseSsl = _editingServer.UseSsl;
|
||||
existingForSourceSystem.ValidateCertificate = _editingServer.ValidateCertificate;
|
||||
existingForSourceSystem.AdditionalParams = _editingServer.AdditionalParams;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var existing = await db.HanaServers.FindAsync(_editingServer.Id);
|
||||
if (existing is not null)
|
||||
{
|
||||
existing.SourceSystem = _editingServer.SourceSystem;
|
||||
existing.Name = _editingServer.Name;
|
||||
existing.Host = _editingServer.Host;
|
||||
existing.Port = _editingServer.Port;
|
||||
existing.Username = _editingServer.Username;
|
||||
existing.Password = _editingServer.Password;
|
||||
existing.Username = string.Empty;
|
||||
existing.Password = string.Empty;
|
||||
existing.DatabaseName = _editingServer.DatabaseName;
|
||||
existing.UseSsl = _editingServer.UseSsl;
|
||||
existing.ValidateCertificate = _editingServer.ValidateCertificate;
|
||||
@@ -490,10 +511,16 @@
|
||||
|
||||
private async Task DeleteServer(HanaServer server)
|
||||
{
|
||||
if (IsHanaSourceSystem(server.SourceSystem))
|
||||
{
|
||||
Snackbar.Add($"Die zentrale HANA-Konfiguration fuer {server.SourceSystem} kann nicht geloescht werden.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await DialogService.ShowMessageBox(
|
||||
"Server löschen",
|
||||
$"Server '{server.Name}' wirklich löschen?",
|
||||
yesText: "Löschen", cancelText: "Abbrechen");
|
||||
"Server löschen",
|
||||
$"Server '{server.Name}' wirklich löschen?",
|
||||
yesText: "Löschen", cancelText: "Abbrechen");
|
||||
|
||||
if (result != true) return;
|
||||
|
||||
@@ -509,7 +536,7 @@
|
||||
if (linkedSites.Count > 0)
|
||||
{
|
||||
Snackbar.Add(
|
||||
$"Server kann nicht gelöscht werden. Noch verknüpfte Standorte: {string.Join(", ", linkedSites)}",
|
||||
$"Server kann nicht gelöscht werden. Noch verknüpfte Standorte: {string.Join(", ", linkedSites)}",
|
||||
Severity.Warning);
|
||||
return;
|
||||
}
|
||||
@@ -523,19 +550,51 @@
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Server konnte nicht gelöscht werden: {ex.Message}", Severity.Error);
|
||||
Snackbar.Add($"Server konnte nicht gelöscht werden: {ex.Message}", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
await LoadDataAsync();
|
||||
Snackbar.Add("Server gelöscht", Severity.Info);
|
||||
Snackbar.Add("Server gelöscht", Severity.Info);
|
||||
}
|
||||
|
||||
private async Task TestServerConnection(HanaServer server)
|
||||
{
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
var sourceDefinition = await db.SourceSystemDefinitions
|
||||
.OrderBy(x => x.Id)
|
||||
.FirstOrDefaultAsync(x => x.Code == server.SourceSystem);
|
||||
|
||||
if (sourceDefinition is null)
|
||||
{
|
||||
Snackbar.Add($"Quellsystem '{server.SourceSystem}' nicht gefunden.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(sourceDefinition.CentralUsername) || string.IsNullOrWhiteSpace(sourceDefinition.CentralPassword))
|
||||
{
|
||||
Snackbar.Add($"Fuer {server.SourceSystem} sind keine zentralen Zugangsdaten im Quellsystem gepflegt.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var testServer = new HanaServer
|
||||
{
|
||||
Id = server.Id,
|
||||
SourceSystem = server.SourceSystem,
|
||||
Name = server.Name,
|
||||
Host = server.Host,
|
||||
Port = server.Port,
|
||||
Username = sourceDefinition.CentralUsername.Trim(),
|
||||
Password = sourceDefinition.CentralPassword,
|
||||
DatabaseName = server.DatabaseName,
|
||||
UseSsl = server.UseSsl,
|
||||
ValidateCertificate = server.ValidateCertificate,
|
||||
AdditionalParams = server.AdditionalParams
|
||||
};
|
||||
|
||||
await AppEventLogService.WriteAsync("HANA", "Server-Test aus UI gestartet",
|
||||
details: server.GetConnectionStringPreview());
|
||||
var result = await Task.Run(() => HanaService.TestConnectionDetailed(server));
|
||||
details: testServer.GetConnectionStringPreview());
|
||||
var result = await Task.Run(() => HanaService.TestConnectionDetailed(testServer));
|
||||
_connectionStatus[server.Id] = result;
|
||||
|
||||
if (result.Success)
|
||||
@@ -562,7 +621,7 @@
|
||||
_editingSite = new Site
|
||||
{
|
||||
IsActive = true,
|
||||
SourceSystem = "SAP",
|
||||
SourceSystem = GetAvailableSourceSystems().FirstOrDefault()?.Code ?? "SAP",
|
||||
HanaServerId = null,
|
||||
ManualImportFilePath = string.Empty
|
||||
};
|
||||
@@ -572,7 +631,6 @@
|
||||
_sapSources = [];
|
||||
_sapJoins = [];
|
||||
_sapMappings = [];
|
||||
_editingSiteServer = CreateDefaultSiteServer();
|
||||
_siteDialogVisible = true;
|
||||
}
|
||||
|
||||
@@ -585,7 +643,9 @@
|
||||
Schema = site.Schema,
|
||||
TSC = site.TSC,
|
||||
Land = site.Land,
|
||||
SourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) ? "SAP" : site.SourceSystem,
|
||||
SourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem)
|
||||
? GetAvailableSourceSystems().FirstOrDefault()?.Code ?? "SAP"
|
||||
: site.SourceSystem,
|
||||
UsernameOverride = site.UsernameOverride,
|
||||
PasswordOverride = site.PasswordOverride,
|
||||
LocalExportFolderOverride = site.LocalExportFolderOverride,
|
||||
@@ -604,9 +664,6 @@
|
||||
_sapMappings = db.SapFieldMappings.Where(m => m.SiteId == site.Id).OrderBy(m => m.SortOrder).ThenBy(m => m.Id).ToList();
|
||||
_sapAvailableSourceExpressions = BuildSourceExpressionsFromMappings();
|
||||
_sapSourceFieldMap = BuildSourceFieldMapFromJoins();
|
||||
_editingSiteServer = site.HanaServer is null
|
||||
? CreateDefaultSiteServer(site)
|
||||
: CloneServer(site.HanaServer);
|
||||
_siteDialogVisible = true;
|
||||
}
|
||||
|
||||
@@ -619,7 +676,7 @@
|
||||
try
|
||||
{
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
var serverId = UsesHanaConnection() ? await SaveOrCreateSiteServerAsync(db) : (int?)null;
|
||||
var serverId = UsesHanaConnection() ? await ResolveCentralHanaServerIdAsync(db, _editingSite.SourceSystem) : (int?)null;
|
||||
_editingSite.HanaServerId = serverId;
|
||||
_editingSite.SapEntitySetsCache = SerializeSapEntitySets(_sapEntitySetsCache);
|
||||
|
||||
@@ -669,9 +726,9 @@
|
||||
private async Task DeleteSite(Site site)
|
||||
{
|
||||
var result = await DialogService.ShowMessageBox(
|
||||
"Standort löschen",
|
||||
$"Standort '{site.Land}' wirklich löschen?",
|
||||
yesText: "Löschen", cancelText: "Abbrechen");
|
||||
"Standort löschen",
|
||||
$"Standort '{site.Land}' wirklich löschen?",
|
||||
yesText: "Löschen", cancelText: "Abbrechen");
|
||||
|
||||
if (result != true) return;
|
||||
|
||||
@@ -692,7 +749,7 @@
|
||||
}
|
||||
|
||||
await LoadDataAsync();
|
||||
Snackbar.Add("Standort gelöscht", Severity.Info);
|
||||
Snackbar.Add("Standort gelöscht", Severity.Info);
|
||||
}
|
||||
|
||||
private static string GetServerNode(HanaServer? server)
|
||||
@@ -703,40 +760,17 @@
|
||||
return server.Host.Contains(':', StringComparison.Ordinal) ? server.Host : $"{server.Host}:{server.Port}";
|
||||
}
|
||||
|
||||
private static string GetConnectionTarget(Site site)
|
||||
{
|
||||
var sourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) ? "SAP" : site.SourceSystem;
|
||||
if (string.Equals(sourceSystem, "SAP", StringComparison.OrdinalIgnoreCase))
|
||||
return string.IsNullOrWhiteSpace(site.SapServiceUrl) ? "-" : site.SapServiceUrl;
|
||||
if (string.Equals(sourceSystem, "MANUAL_EXCEL", StringComparison.OrdinalIgnoreCase))
|
||||
return string.IsNullOrWhiteSpace(site.ManualImportFilePath) ? "-" : Path.GetFileName(site.ManualImportFilePath);
|
||||
|
||||
return GetServerNode(site.HanaServer);
|
||||
}
|
||||
|
||||
private HanaServer CreateDefaultSiteServer(Site? site = null)
|
||||
{
|
||||
var label = !string.IsNullOrWhiteSpace(site?.Land) ? site!.Land : site?.TSC;
|
||||
if (string.IsNullOrWhiteSpace(label))
|
||||
label = "Neuer Standort";
|
||||
|
||||
return new HanaServer
|
||||
{
|
||||
Name = $"{label} HANA",
|
||||
Port = 30015
|
||||
};
|
||||
}
|
||||
|
||||
private static HanaServer CloneServer(HanaServer server)
|
||||
{
|
||||
return new HanaServer
|
||||
{
|
||||
Id = server.Id,
|
||||
SourceSystem = server.SourceSystem,
|
||||
Name = server.Name,
|
||||
Host = server.Host,
|
||||
Port = server.Port,
|
||||
Username = server.Username,
|
||||
Password = server.Password,
|
||||
Username = string.Empty,
|
||||
Password = string.Empty,
|
||||
DatabaseName = server.DatabaseName,
|
||||
UseSsl = server.UseSsl,
|
||||
ValidateCertificate = server.ValidateCertificate,
|
||||
@@ -744,66 +778,98 @@
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<int> SaveOrCreateSiteServerAsync(AppDbContext db)
|
||||
private async Task<int> ResolveCentralHanaServerIdAsync(AppDbContext db, string sourceSystem)
|
||||
{
|
||||
_editingSiteServer.Name = string.IsNullOrWhiteSpace(_editingSiteServer.Name)
|
||||
? $"{_editingSite.Land} HANA".Trim()
|
||||
: _editingSiteServer.Name.Trim();
|
||||
_editingSite.UsernameOverride = _editingSite.UsernameOverride.Trim();
|
||||
_editingSite.PasswordOverride = _editingSite.PasswordOverride.Trim();
|
||||
_editingSite.LocalExportFolderOverride = _editingSite.LocalExportFolderOverride.Trim();
|
||||
_editingSite.ManualImportFilePath = _editingSite.ManualImportFilePath.Trim();
|
||||
_editingSite.SapServiceUrl = _editingSite.SapServiceUrl.Trim();
|
||||
_editingSite.SapEntitySet = _editingSite.SapEntitySet.Trim();
|
||||
_editingSiteServer.Host = _editingSiteServer.Host.Trim();
|
||||
_editingSiteServer.Username = _editingSiteServer.Username.Trim();
|
||||
_editingSiteServer.DatabaseName = _editingSiteServer.DatabaseName.Trim();
|
||||
_editingSiteServer.AdditionalParams = _editingSiteServer.AdditionalParams.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_editingSiteServer.Host))
|
||||
throw new InvalidOperationException("Host oder ServerNode muss gesetzt sein.");
|
||||
var normalizedSourceSystem = string.IsNullOrWhiteSpace(sourceSystem) ? string.Empty : sourceSystem.Trim().ToUpperInvariant();
|
||||
var centralServer = await db.HanaServers
|
||||
.OrderBy(x => x.Id)
|
||||
.FirstOrDefaultAsync(x => x.SourceSystem == normalizedSourceSystem);
|
||||
|
||||
if (_editingSite.HanaServerId == 0)
|
||||
{
|
||||
db.HanaServers.Add(_editingSiteServer);
|
||||
await db.SaveChangesAsync();
|
||||
return _editingSiteServer.Id;
|
||||
}
|
||||
if (centralServer is null || string.IsNullOrWhiteSpace(centralServer.Host))
|
||||
throw new InvalidOperationException($"Fuer Quellsystem '{normalizedSourceSystem}' ist keine gueltige zentrale HANA-Konfiguration vorhanden.");
|
||||
|
||||
var sharedUseCount = await db.Sites.CountAsync(s => s.HanaServerId == _editingSite.HanaServerId && s.Id != _editingSite.Id);
|
||||
if (sharedUseCount > 0)
|
||||
{
|
||||
var dedicatedServer = CloneServer(_editingSiteServer);
|
||||
dedicatedServer.Id = 0;
|
||||
db.HanaServers.Add(dedicatedServer);
|
||||
await db.SaveChangesAsync();
|
||||
return dedicatedServer.Id;
|
||||
}
|
||||
|
||||
var existingServer = await db.HanaServers.FindAsync(_editingSite.HanaServerId);
|
||||
if (existingServer is null)
|
||||
{
|
||||
db.HanaServers.Add(_editingSiteServer);
|
||||
await db.SaveChangesAsync();
|
||||
return _editingSiteServer.Id;
|
||||
}
|
||||
|
||||
existingServer.Name = _editingSiteServer.Name;
|
||||
existingServer.Host = _editingSiteServer.Host;
|
||||
existingServer.Port = _editingSiteServer.Port;
|
||||
existingServer.Username = _editingSiteServer.Username;
|
||||
existingServer.Password = _editingSiteServer.Password;
|
||||
existingServer.DatabaseName = _editingSiteServer.DatabaseName;
|
||||
existingServer.UseSsl = _editingSiteServer.UseSsl;
|
||||
existingServer.ValidateCertificate = _editingSiteServer.ValidateCertificate;
|
||||
existingServer.AdditionalParams = _editingSiteServer.AdditionalParams;
|
||||
await db.SaveChangesAsync();
|
||||
return existingServer.Id;
|
||||
return centralServer.Id;
|
||||
}
|
||||
|
||||
private bool IsSapSite() => string.Equals(_editingSite.SourceSystem, "SAP", StringComparison.OrdinalIgnoreCase);
|
||||
private bool IsManualExcelSite() => string.Equals(_editingSite.SourceSystem, "MANUAL_EXCEL", StringComparison.OrdinalIgnoreCase);
|
||||
private bool UsesHanaConnection() => !IsSapSite() && !IsManualExcelSite();
|
||||
private IEnumerable<SourceSystemDefinition> GetAvailableSourceSystems()
|
||||
=> _sourceSystemDefinitions
|
||||
.Where(x => x.IsActive || string.Equals(x.Code, _editingSite.SourceSystem, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(x => x.DisplayName)
|
||||
.ThenBy(x => x.Code);
|
||||
|
||||
private List<string> GetHanaSourceSystemCodes()
|
||||
=> _sourceSystemDefinitions
|
||||
.Where(x => string.Equals(x.ConnectionKind, SourceSystemConnectionKinds.Hana, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(x => x.Code)
|
||||
.OrderBy(x => x)
|
||||
.ToList();
|
||||
|
||||
private string GetSourceSystemConnectionKind(string? sourceSystem)
|
||||
=> _sourceSystemDefinitions
|
||||
.FirstOrDefault(x => string.Equals(x.Code, sourceSystem, StringComparison.OrdinalIgnoreCase))
|
||||
?.ConnectionKind
|
||||
?? SourceSystemConnectionKinds.SapGateway;
|
||||
|
||||
private bool IsHanaSourceSystem(string? sourceSystem)
|
||||
=> string.Equals(GetSourceSystemConnectionKind(sourceSystem), SourceSystemConnectionKinds.Hana, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private bool IsSapSite()
|
||||
=> string.Equals(GetSourceSystemConnectionKind(_editingSite.SourceSystem), SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private bool IsManualExcelSite()
|
||||
=> string.Equals(GetSourceSystemConnectionKind(_editingSite.SourceSystem), SourceSystemConnectionKinds.ManualExcel, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private bool UsesHanaConnection() => IsHanaSourceSystem(_editingSite.SourceSystem);
|
||||
|
||||
private string GetSourceSystemLabel(SourceSystemDefinition definition)
|
||||
=> string.IsNullOrWhiteSpace(definition.DisplayName) ? definition.Code : $"{definition.DisplayName} ({definition.Code})";
|
||||
|
||||
private string GetConnectionTarget(Site site)
|
||||
{
|
||||
var connectionKind = GetSourceSystemConnectionKind(site.SourceSystem);
|
||||
if (string.Equals(connectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase))
|
||||
return GetEffectiveSapServiceUrl(site);
|
||||
if (string.Equals(connectionKind, SourceSystemConnectionKinds.ManualExcel, StringComparison.OrdinalIgnoreCase))
|
||||
return string.IsNullOrWhiteSpace(site.ManualImportFilePath) ? "-" : Path.GetFileName(site.ManualImportFilePath);
|
||||
|
||||
return GetServerNode(site.HanaServer);
|
||||
}
|
||||
|
||||
private string GetEffectiveSapServiceUrl(Site site)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(site.SapServiceUrl))
|
||||
return site.SapServiceUrl;
|
||||
|
||||
var sourceDefinition = _sourceSystemDefinitions
|
||||
.FirstOrDefault(x => string.Equals(x.Code, site.SourceSystem, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
return string.IsNullOrWhiteSpace(sourceDefinition?.CentralServiceUrl) ? "-" : sourceDefinition.CentralServiceUrl;
|
||||
}
|
||||
|
||||
private string GetCentralSapServiceUrlSummary(string sourceSystem)
|
||||
{
|
||||
var sourceDefinition = _sourceSystemDefinitions
|
||||
.FirstOrDefault(x => string.Equals(x.Code, sourceSystem, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
return string.IsNullOrWhiteSpace(sourceDefinition?.CentralServiceUrl) ? "-" : sourceDefinition.CentralServiceUrl;
|
||||
}
|
||||
|
||||
private string GetCentralHanaSummary(string sourceSystem)
|
||||
{
|
||||
var normalizedSourceSystem = string.IsNullOrWhiteSpace(sourceSystem) ? string.Empty : sourceSystem.Trim().ToUpperInvariant();
|
||||
var centralServer = _servers.FirstOrDefault(x => x.SourceSystem == normalizedSourceSystem);
|
||||
if (centralServer is null || string.IsNullOrWhiteSpace(centralServer.Host))
|
||||
return $"keine zentrale HANA-Konfiguration fuer {normalizedSourceSystem}";
|
||||
|
||||
return $"{centralServer.Name} | {GetServerNode(centralServer)}";
|
||||
}
|
||||
|
||||
private async Task RefreshSapEntitySets()
|
||||
{
|
||||
@@ -813,20 +879,28 @@
|
||||
_refreshingSapEntitySets = true;
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_editingSite.SapServiceUrl))
|
||||
throw new InvalidOperationException("SAP Service URL muss gesetzt sein.");
|
||||
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
var settings = await db.ExportSettings.FirstOrDefaultAsync() ?? new();
|
||||
var username = string.IsNullOrWhiteSpace(_editingSite.UsernameOverride) ? settings.SapUsername : _editingSite.UsernameOverride;
|
||||
var password = string.IsNullOrWhiteSpace(_editingSite.PasswordOverride) ? settings.SapPassword : _editingSite.PasswordOverride;
|
||||
var sourceDefinition = await db.SourceSystemDefinitions
|
||||
.OrderBy(x => x.Id)
|
||||
.FirstOrDefaultAsync(x => x.Code == _editingSite.SourceSystem);
|
||||
var serviceUrl = string.IsNullOrWhiteSpace(_editingSite.SapServiceUrl)
|
||||
? sourceDefinition?.CentralServiceUrl ?? string.Empty
|
||||
: _editingSite.SapServiceUrl;
|
||||
if (string.IsNullOrWhiteSpace(serviceUrl))
|
||||
throw new InvalidOperationException("Es ist weder eine zentrale SAP Service URL noch ein Standort-Override gesetzt.");
|
||||
var username = string.IsNullOrWhiteSpace(_editingSite.UsernameOverride)
|
||||
? sourceDefinition?.CentralUsername ?? string.Empty
|
||||
: _editingSite.UsernameOverride;
|
||||
var password = string.IsNullOrWhiteSpace(_editingSite.PasswordOverride)
|
||||
? sourceDefinition?.CentralPassword ?? string.Empty
|
||||
: _editingSite.PasswordOverride;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
throw new InvalidOperationException("Für SAP sind weder zentrale Zugangsdaten noch Standort-Overrides gesetzt.");
|
||||
throw new InvalidOperationException("Für SAP sind weder zentrale Zugangsdaten noch Standort-Overrides gesetzt.");
|
||||
|
||||
await AppEventLogService.WriteAsync("SAP", "Refresh aus UI gestartet", siteId: _editingSite.Id, land: _editingSite.Land,
|
||||
details: _editingSite.SapServiceUrl);
|
||||
var entitySets = await SapGatewayService.GetEntitySetsAsync(_editingSite.SapServiceUrl, username.Trim(), password.Trim());
|
||||
details: serviceUrl);
|
||||
var entitySets = await SapGatewayService.GetEntitySetsAsync(serviceUrl, username.Trim(), password.Trim());
|
||||
_sapEntitySetsCache = entitySets;
|
||||
_editingSite.SapEntitySetsCache = SerializeSapEntitySets(entitySets);
|
||||
_editingSite.SapEntitySetsRefreshedAtUtc = DateTime.UtcNow;
|
||||
@@ -884,7 +958,7 @@
|
||||
var extension = Path.GetExtension(file.Name);
|
||||
if (!string.Equals(extension, ".xlsx", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException("Bitte eine Excel-Datei mit Endung .xlsx auswählen.");
|
||||
throw new InvalidOperationException("Bitte eine Excel-Datei mit Endung .xlsx auswählen.");
|
||||
}
|
||||
|
||||
var uploadDirectory = Path.Combine(AppContext.BaseDirectory, "manual-imports");
|
||||
@@ -974,13 +1048,13 @@
|
||||
|
||||
if (activeSources.Count < 2)
|
||||
{
|
||||
Snackbar.Add("Für Auto-Match werden mindestens zwei aktive SAP-Quellen benötigt.", Severity.Warning);
|
||||
Snackbar.Add("Für Auto-Match werden mindestens zwei aktive SAP-Quellen benötigt.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_sapSourceFieldMap.Count == 0)
|
||||
{
|
||||
Snackbar.Add("Bitte zuerst 'Felder aus Quellen laden' ausführen.", Severity.Warning);
|
||||
Snackbar.Add("Bitte zuerst 'Felder aus Quellen laden' ausführen.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1038,7 +1112,7 @@
|
||||
}
|
||||
|
||||
NormalizeSapConfigCollections();
|
||||
Snackbar.Add($"{createdOrUpdated} Join-Vorschläge gesetzt.", Severity.Success);
|
||||
Snackbar.Add($"{createdOrUpdated} Join-Vorschläge gesetzt.", Severity.Success);
|
||||
}
|
||||
|
||||
private void RemoveSapJoin(SapJoinDefinition join)
|
||||
@@ -1116,9 +1190,6 @@
|
||||
_refreshingSapSourceFields = true;
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_editingSite.SapServiceUrl))
|
||||
throw new InvalidOperationException("SAP Service URL muss gesetzt sein.");
|
||||
|
||||
var activeSources = _sapSources
|
||||
.Where(s => s.IsActive && !string.IsNullOrWhiteSpace(s.Alias) && !string.IsNullOrWhiteSpace(s.EntitySet))
|
||||
.OrderBy(s => s.SortOrder)
|
||||
@@ -1129,18 +1200,29 @@
|
||||
throw new InvalidOperationException("Es gibt keine aktiven SAP-Quellen mit Alias und Entity Set.");
|
||||
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
var settings = await db.ExportSettings.FirstOrDefaultAsync() ?? new();
|
||||
var username = string.IsNullOrWhiteSpace(_editingSite.UsernameOverride) ? settings.SapUsername : _editingSite.UsernameOverride;
|
||||
var password = string.IsNullOrWhiteSpace(_editingSite.PasswordOverride) ? settings.SapPassword : _editingSite.PasswordOverride;
|
||||
var sourceDefinition = await db.SourceSystemDefinitions
|
||||
.OrderBy(x => x.Id)
|
||||
.FirstOrDefaultAsync(x => x.Code == _editingSite.SourceSystem);
|
||||
var serviceUrl = string.IsNullOrWhiteSpace(_editingSite.SapServiceUrl)
|
||||
? sourceDefinition?.CentralServiceUrl ?? string.Empty
|
||||
: _editingSite.SapServiceUrl;
|
||||
if (string.IsNullOrWhiteSpace(serviceUrl))
|
||||
throw new InvalidOperationException("Es ist weder eine zentrale SAP Service URL noch ein Standort-Override gesetzt.");
|
||||
var username = string.IsNullOrWhiteSpace(_editingSite.UsernameOverride)
|
||||
? sourceDefinition?.CentralUsername ?? string.Empty
|
||||
: _editingSite.UsernameOverride;
|
||||
var password = string.IsNullOrWhiteSpace(_editingSite.PasswordOverride)
|
||||
? sourceDefinition?.CentralPassword ?? string.Empty
|
||||
: _editingSite.PasswordOverride;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
throw new InvalidOperationException("Für SAP sind weder zentrale Zugangsdaten noch Standort-Overrides gesetzt.");
|
||||
throw new InvalidOperationException("Für SAP sind weder zentrale Zugangsdaten noch Standort-Overrides gesetzt.");
|
||||
|
||||
var expressions = new List<string> { "=SAP" };
|
||||
var sourceFieldMap = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var source in activeSources)
|
||||
{
|
||||
var fieldNames = await SapGatewayService.GetEntityFieldNamesAsync(_editingSite.SapServiceUrl, source.EntitySet, username.Trim(), password.Trim());
|
||||
var fieldNames = await SapGatewayService.GetEntityFieldNamesAsync(serviceUrl, source.EntitySet, username.Trim(), password.Trim());
|
||||
sourceFieldMap[source.Alias] = fieldNames;
|
||||
expressions.AddRange(fieldNames.Select(field => $"{source.Alias}.{field}"));
|
||||
}
|
||||
@@ -1251,3 +1333,5 @@
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase)
|
||||
?? [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -45,9 +45,9 @@
|
||||
<MudTd><MudCheckBox @bind-Value="context.IsActive" /></MudTd>
|
||||
<MudTd>
|
||||
<MudSelect T="string" Value="@context.SourceSystem" ValueChanged="@(v => context.SourceSystem = v)" Dense>
|
||||
@foreach (var system in _systems)
|
||||
@foreach (var system in _sourceSystems.Where(x => x.IsActive))
|
||||
{
|
||||
<MudSelectItem Value="@system">@system</MudSelectItem>
|
||||
<MudSelectItem Value="@system.Code">@system.DisplayName (@system.Code)</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudTd>
|
||||
@@ -176,7 +176,6 @@
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
private readonly string[] _systems = ["SAP", "BI1", "SAGE", "MANUAL_EXCEL"];
|
||||
private readonly string[] _ruleScopes = ["Value", "Record"];
|
||||
private readonly string[] _recordFields = typeof(SalesRecord)
|
||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
@@ -185,6 +184,7 @@
|
||||
.ToArray();
|
||||
|
||||
private List<FieldTransformationRule> _rules = new();
|
||||
private List<SourceSystemDefinition> _sourceSystems = [];
|
||||
private IReadOnlyList<TransformationCatalogItem> _catalogItems = [];
|
||||
private bool _codeDialogVisible;
|
||||
private FieldTransformationRule? _selectedRule;
|
||||
@@ -200,6 +200,7 @@
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
_sourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync();
|
||||
_rules = await db.FieldTransformationRules.OrderBy(r => r.SortOrder).ThenBy(r => r.Id).ToListAsync();
|
||||
|
||||
foreach (var rule in _rules)
|
||||
@@ -217,7 +218,7 @@
|
||||
var nextSort = _rules.Count == 0 ? 10 : _rules.Max(r => r.SortOrder) + 10;
|
||||
_rules.Add(new FieldTransformationRule
|
||||
{
|
||||
SourceSystem = "SAP",
|
||||
SourceSystem = _sourceSystems.FirstOrDefault(x => x.IsActive)?.Code ?? "SAP",
|
||||
RuleScope = "Value",
|
||||
SourceField = nameof(SalesRecord.Material),
|
||||
TargetField = nameof(SalesRecord.Material),
|
||||
|
||||
@@ -8,6 +8,7 @@ public class AppDbContext : DbContext
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
||||
|
||||
public DbSet<HanaServer> HanaServers => Set<HanaServer>();
|
||||
public DbSet<SourceSystemDefinition> SourceSystemDefinitions => Set<SourceSystemDefinition>();
|
||||
public DbSet<Site> Sites => Set<Site>();
|
||||
public DbSet<SharePointConfig> SharePointConfigs => Set<SharePointConfig>();
|
||||
public DbSet<ExportSettings> ExportSettings => Set<ExportSettings>();
|
||||
|
||||
@@ -43,6 +43,426 @@ Ergebnis:
|
||||
- bekannte Warnung bleibt:
|
||||
- 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
|
||||
|
||||
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:
|
||||
- SAP HANA Architekturwarnung `MSB3270`
|
||||
- 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 ConfigTransferSharePoint? SharePointConfig { get; set; }
|
||||
public ConfigTransferExportSettings? ExportSettings { get; set; }
|
||||
public List<ConfigTransferSourceSystemDefinition> SourceSystemDefinitions { get; set; } = [];
|
||||
public List<ConfigTransferCurrencyExchangeRate> CurrencyExchangeRates { get; set; } = [];
|
||||
public List<ConfigTransferHanaServer> HanaServers { get; set; } = [];
|
||||
public List<ConfigTransferSite> Sites { get; set; } = [];
|
||||
@@ -16,6 +17,17 @@ public class ConfigTransferPackage
|
||||
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 string SiteUrl { get; set; } = string.Empty;
|
||||
@@ -35,12 +47,6 @@ public class ConfigTransferExportSettings
|
||||
public bool DebugLoggingEnabled { get; set; }
|
||||
public string LocalSiteExportFolder { 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
|
||||
@@ -57,11 +63,10 @@ public class ConfigTransferCurrencyExchangeRate
|
||||
public class ConfigTransferHanaServer
|
||||
{
|
||||
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 Host { get; set; } = string.Empty;
|
||||
public int Port { get; set; } = 30015;
|
||||
public string? Username { get; set; }
|
||||
public string? Password { get; set; }
|
||||
public string DatabaseName { get; set; } = string.Empty;
|
||||
public bool UseSsl { get; set; }
|
||||
public bool ValidateCertificate { get; set; }
|
||||
@@ -75,7 +80,7 @@ public class ConfigTransferSite
|
||||
public string Schema { get; set; } = string.Empty;
|
||||
public string TSC { 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? PasswordOverride { get; set; }
|
||||
public string LocalExportFolderOverride { get; set; } = string.Empty;
|
||||
|
||||
@@ -10,10 +10,4 @@ public class ExportSettings
|
||||
public bool DebugLoggingEnabled { get; set; }
|
||||
public string LocalSiteExportFolder { 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; }
|
||||
|
||||
[Required]
|
||||
public string SourceSystem { get; set; } = "SAP";
|
||||
public string SourceSystem { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string SourceField { get; set; } = nameof(SalesRecord.Material);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Data.Common;
|
||||
|
||||
namespace TrafagSalesExporter.Models;
|
||||
@@ -7,6 +8,9 @@ public class HanaServer
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public string SourceSystem { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
@@ -15,8 +19,10 @@ public class HanaServer
|
||||
|
||||
public int Port { get; set; } = 30015;
|
||||
|
||||
[NotMapped]
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
[NotMapped]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
@@ -66,6 +72,7 @@ public class HanaServer
|
||||
var pwdMasked = string.IsNullOrEmpty(Password) ? "" : "***";
|
||||
var copy = new HanaServer
|
||||
{
|
||||
SourceSystem = SourceSystem,
|
||||
Host = Host,
|
||||
Port = Port,
|
||||
Username = Username,
|
||||
|
||||
@@ -22,7 +22,7 @@ public class Site
|
||||
public string Land { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string SourceSystem { get; set; } = "SAP";
|
||||
public string SourceSystem { 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
|
||||
- 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
|
||||
|
||||
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:
|
||||
|
||||
- `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();
|
||||
var sharePoint = await db.SharePointConfigs.FirstOrDefaultAsync();
|
||||
var exportSettings = await db.ExportSettings.FirstOrDefaultAsync();
|
||||
var sourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync();
|
||||
var exchangeRates = await db.CurrencyExchangeRates
|
||||
.OrderBy(x => x.FromCurrency)
|
||||
.ThenBy(x => x.ToCurrency)
|
||||
@@ -55,14 +56,18 @@ public class ConfigTransferService : IConfigTransferService
|
||||
TimerEnabled = exportSettings.TimerEnabled,
|
||||
DebugLoggingEnabled = exportSettings.DebugLoggingEnabled,
|
||||
LocalSiteExportFolder = exportSettings.LocalSiteExportFolder,
|
||||
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
|
||||
LocalConsolidatedExportFolder = exportSettings.LocalConsolidatedExportFolder
|
||||
},
|
||||
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
|
||||
{
|
||||
FromCurrency = rate.FromCurrency,
|
||||
@@ -76,11 +81,10 @@ public class ConfigTransferService : IConfigTransferService
|
||||
HanaServers = hanaServers.Select(server => new ConfigTransferHanaServer
|
||||
{
|
||||
Key = serverKeyMap[server.Id],
|
||||
SourceSystem = server.SourceSystem,
|
||||
Name = server.Name,
|
||||
Host = server.Host,
|
||||
Port = server.Port,
|
||||
Username = includeSecrets ? server.Username : null,
|
||||
Password = includeSecrets ? server.Password : null,
|
||||
DatabaseName = server.DatabaseName,
|
||||
UseSsl = server.UseSsl,
|
||||
ValidateCertificate = server.ValidateCertificate,
|
||||
@@ -158,6 +162,7 @@ public class ConfigTransferService : IConfigTransferService
|
||||
using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var existingSharePoint = await db.SharePointConfigs.FirstOrDefaultAsync();
|
||||
var existingSettings = await db.ExportSettings.FirstOrDefaultAsync();
|
||||
var existingSourceSystems = await db.SourceSystemDefinitions.ToListAsync();
|
||||
var existingServers = await db.HanaServers.ToListAsync();
|
||||
var existingExchangeRates = await db.CurrencyExchangeRates.ToListAsync();
|
||||
var existingSites = await db.Sites.ToListAsync();
|
||||
@@ -168,20 +173,10 @@ public class ConfigTransferService : IConfigTransferService
|
||||
var existingCentralRecords = await db.CentralSalesRecords.ToListAsync();
|
||||
|
||||
var preservedSharePointSecret = existingSharePoint?.ClientSecret ?? string.Empty;
|
||||
var preservedSecrets = existingSettings is null
|
||||
? new ConfigTransferExportSettings()
|
||||
: new ConfigTransferExportSettings
|
||||
{
|
||||
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 preservedSourceSystemSecrets = existingSourceSystems.ToDictionary(
|
||||
x => x.Code,
|
||||
x => (CentralUsername: x.CentralUsername, CentralPassword: x.CentralPassword),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
var preservedSiteSecrets = existingSites.ToDictionary(
|
||||
x => BuildSiteSignature(x.Land, x.TSC, x.Schema, x.SourceSystem),
|
||||
x => (x.UsernameOverride, x.PasswordOverride));
|
||||
@@ -194,6 +189,7 @@ public class ConfigTransferService : IConfigTransferService
|
||||
if (existingCentralRecords.Count > 0) db.CentralSalesRecords.RemoveRange(existingCentralRecords);
|
||||
if (existingSites.Count > 0) db.Sites.RemoveRange(existingSites);
|
||||
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 (existingSettings is not null) db.ExportSettings.Remove(existingSettings);
|
||||
await db.SaveChangesAsync();
|
||||
@@ -218,15 +214,28 @@ public class ConfigTransferService : IConfigTransferService
|
||||
TimerEnabled = importedSettings.TimerEnabled,
|
||||
DebugLoggingEnabled = importedSettings.DebugLoggingEnabled,
|
||||
LocalSiteExportFolder = importedSettings.LocalSiteExportFolder,
|
||||
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
|
||||
LocalConsolidatedExportFolder = importedSettings.LocalConsolidatedExportFolder
|
||||
});
|
||||
|
||||
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)
|
||||
{
|
||||
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);
|
||||
foreach (var server in package.HanaServers)
|
||||
{
|
||||
preservedServerSecrets.TryGetValue(BuildServerSignature(server.Name, server.Host, server.Port, server.DatabaseName), out var preserved);
|
||||
var entity = new HanaServer
|
||||
{
|
||||
SourceSystem = server.SourceSystem,
|
||||
Name = server.Name,
|
||||
Host = server.Host,
|
||||
Port = server.Port,
|
||||
Username = package.IncludesSecrets ? server.Username ?? string.Empty : preserved.Username ?? string.Empty,
|
||||
Password = package.IncludesSecrets ? server.Password ?? string.Empty : preserved.Password ?? string.Empty,
|
||||
Username = string.Empty,
|
||||
Password = string.Empty,
|
||||
DatabaseName = server.DatabaseName,
|
||||
UseSsl = server.UseSsl,
|
||||
ValidateCertificate = server.ValidateCertificate,
|
||||
@@ -355,10 +364,42 @@ public class ConfigTransferService : IConfigTransferService
|
||||
|
||||
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)
|
||||
=> $"{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)
|
||||
{
|
||||
EnsureSitesTableSupportsOptionalHanaServer(db);
|
||||
EnsureExportSettingsTableSupportsCurrentSchema(db);
|
||||
EnsureHanaServersTableSupportsCurrentSchema(db);
|
||||
RepairBrokenSiteForeignKeys(db);
|
||||
AddColumnIfMissing(db, "HanaServers", "SourceSystem", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "HanaServers", "DatabaseName", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "HanaServers", "UseSsl", "INTEGER NOT NULL DEFAULT 0");
|
||||
AddColumnIfMissing(db, "HanaServers", "ValidateCertificate", "INTEGER NOT NULL DEFAULT 0");
|
||||
@@ -61,12 +64,6 @@ public class DatabaseInitializationService : IDatabaseInitializationService
|
||||
AddColumnIfMissing(db, "Sites", "SapEntitySet", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "SapEntitySetsCache", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "SapEntitySetsRefreshedAtUtc", "TEXT NULL");
|
||||
AddColumnIfMissing(db, "ExportSettings", "SapUsername", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "ExportSettings", "SapPassword", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "ExportSettings", "Bi1Username", "TEXT NOT NULL DEFAULT ''");
|
||||
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", "LocalSiteExportFolder", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "ExportSettings", "LocalConsolidatedExportFolder", "TEXT NOT NULL DEFAULT ''");
|
||||
@@ -75,11 +72,57 @@ public class DatabaseInitializationService : IDatabaseInitializationService
|
||||
EnsureTransformationTable(db);
|
||||
AddColumnIfMissing(db, "FieldTransformationRules", "RuleScope", "TEXT NOT NULL DEFAULT 'Value'");
|
||||
EnsureCurrencyExchangeRateTable(db);
|
||||
EnsureSourceSystemDefinitionTable(db);
|
||||
AddColumnIfMissing(db, "SourceSystemDefinitions", "CentralServiceUrl", "TEXT NOT NULL DEFAULT ''");
|
||||
EnsureSapSourceTable(db);
|
||||
EnsureSapJoinTable(db);
|
||||
EnsureSapFieldMappingTable(db);
|
||||
EnsureCentralSalesRecordTable(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)
|
||||
@@ -272,7 +315,7 @@ FROM Sites_old;";
|
||||
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 oldColumns = GetTableColumns(connection, transaction, oldTableName);
|
||||
@@ -280,7 +323,7 @@ FROM Sites_old;";
|
||||
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);
|
||||
|
||||
@@ -315,6 +358,31 @@ CREATE TABLE ExportLogs (
|
||||
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() => @"
|
||||
CREATE TABLE AppEventLogs (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -604,21 +672,42 @@ CREATE TABLE IF NOT EXISTS AppEventLogs (
|
||||
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)
|
||||
{
|
||||
if (db.HanaServers.Any())
|
||||
if (db.Sites.Any() || db.HanaServers.Any() || db.SharePointConfigs.Any() || db.ExportSettings.Any())
|
||||
return;
|
||||
|
||||
var serverInternal = new HanaServer { Name = "Internal", Host = "travtrp0", Port = 30015, Username = "", Password = "" };
|
||||
var serverIndia = new HanaServer { Name = "India", Host = "20.197.20.60", Port = 30015, Username = "", Password = "" };
|
||||
db.HanaServers.AddRange(serverInternal, serverIndia);
|
||||
var serverBi1 = new HanaServer { SourceSystem = "BI1", Name = "BI1", Host = "travtrp0", Port = 30015, Username = "", Password = "" };
|
||||
var serverSage = new HanaServer { SourceSystem = "SAGE", Name = "SAGE", Host = "20.197.20.60", Port = 30015, Username = "", Password = "" };
|
||||
db.HanaServers.AddRange(serverBi1, serverSage);
|
||||
db.SaveChanges();
|
||||
|
||||
db.Sites.AddRange(
|
||||
new Site { HanaServerId = serverInternal.Id, Schema = "fr01_p", TSC = "TRFR", Land = "Frankreich", IsActive = true },
|
||||
new Site { HanaServerId = serverInternal.Id, Schema = "it01_p", TSC = "TRIT", Land = "Italien", IsActive = true },
|
||||
new Site { HanaServerId = serverInternal.Id, Schema = "us01_p", TSC = "TRUS", Land = "USA", IsActive = true },
|
||||
new Site { HanaServerId = serverIndia.Id, Schema = "TRAFAG_LIVE", TSC = "TRIN", Land = "Indien", IsActive = true }
|
||||
new Site { HanaServerId = serverBi1.Id, Schema = "fr01_p", TSC = "TRFR", Land = "Frankreich", SourceSystem = "BI1", IsActive = true },
|
||||
new Site { HanaServerId = serverBi1.Id, Schema = "it01_p", TSC = "TRIT", Land = "Italien", SourceSystem = "BI1", IsActive = true },
|
||||
new Site { HanaServerId = serverBi1.Id, Schema = "us01_p", TSC = "TRUS", Land = "USA", SourceSystem = "BI1", IsActive = true },
|
||||
new Site { HanaServerId = serverSage.Id, Schema = "TRAFAG_LIVE", TSC = "TRIN", Land = "Indien", SourceSystem = "SAGE", IsActive = true }
|
||||
);
|
||||
|
||||
db.SharePointConfigs.Add(new SharePointConfig
|
||||
@@ -695,4 +784,121 @@ CREATE TABLE IF NOT EXISTS AppEventLogs (
|
||||
if (hasChanges)
|
||||
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 outputDir = ResolveSiteOutputDirectory(settings, site);
|
||||
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>();
|
||||
string filePath;
|
||||
|
||||
if (sourceSystem == "SAP")
|
||||
if (string.Equals(sourceDefinition.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var credentials = ResolveCredentials(site, settings, sourceSystem);
|
||||
if (string.IsNullOrWhiteSpace(site.SapServiceUrl))
|
||||
var credentials = ResolveCredentials(site, sourceDefinition);
|
||||
var sapServiceUrl = ResolveSapServiceUrl(site, sourceDefinition);
|
||||
if (string.IsNullOrWhiteSpace(sapServiceUrl))
|
||||
throw new InvalidOperationException($"Standort '{site.Land}' hat keine SAP Service URL.");
|
||||
var sapSources = await db.SapSourceDefinitions.Where(s => s.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...");
|
||||
await _appEventLogService.WriteAsync("Export", "SAP Quellen laden", siteId: site.Id, land: site.Land,
|
||||
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...");
|
||||
await _appEventLogService.WriteAsync("Export", "Transformationen anwenden", siteId: site.Id, land: site.Land,
|
||||
details: $"Records vor Transformation={records.Count}");
|
||||
@@ -99,7 +106,7 @@ public class SiteExportService : ISiteExportService
|
||||
filePath = _excelService.CreateExcelFile(outputDir, site.TSC, DateTime.UtcNow.Date, records);
|
||||
log.RowCount = records.Count;
|
||||
}
|
||||
else if (sourceSystem == "MANUAL_EXCEL")
|
||||
else if (string.Equals(sourceDefinition.ConnectionKind, SourceSystemConnectionKinds.ManualExcel, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(site.ManualImportFilePath))
|
||||
throw new InvalidOperationException($"Standort '{site.Land}' hat keine manuelle Excel-Datei.");
|
||||
@@ -125,7 +132,7 @@ public class SiteExportService : ISiteExportService
|
||||
}
|
||||
else
|
||||
{
|
||||
var exportServer = BuildEffectiveServer(site, settings, sourceSystem);
|
||||
var exportServer = await BuildEffectiveServerAsync(db, site, sourceDefinition);
|
||||
updateStatus?.Invoke("HANA Abfrage...");
|
||||
await _appEventLogService.WriteAsync("Export", "HANA Abfrage gestartet", siteId: site.Id, land: site.Land,
|
||||
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)
|
||||
throw new InvalidOperationException($"Standort '{site.Land}' hat keinen HANA-Server.");
|
||||
var centralServer = await db.HanaServers
|
||||
.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
|
||||
{
|
||||
Id = site.HanaServer.Id,
|
||||
Name = site.HanaServer.Name,
|
||||
Host = site.HanaServer.Host,
|
||||
Port = site.HanaServer.Port,
|
||||
Username = FirstNonEmpty(credentials.Username, site.HanaServer.Username),
|
||||
Password = FirstNonEmpty(credentials.Password, site.HanaServer.Password),
|
||||
DatabaseName = site.HanaServer.DatabaseName,
|
||||
UseSsl = site.HanaServer.UseSsl,
|
||||
ValidateCertificate = site.HanaServer.ValidateCertificate,
|
||||
AdditionalParams = site.HanaServer.AdditionalParams
|
||||
Id = centralServer.Id,
|
||||
SourceSystem = centralServer.SourceSystem,
|
||||
Name = centralServer.Name,
|
||||
Host = centralServer.Host,
|
||||
Port = centralServer.Port,
|
||||
Username = credentials.Username,
|
||||
Password = credentials.Password,
|
||||
DatabaseName = centralServer.DatabaseName,
|
||||
UseSsl = centralServer.UseSsl,
|
||||
ValidateCertificate = centralServer.ValidateCertificate,
|
||||
AdditionalParams = centralServer.AdditionalParams
|
||||
};
|
||||
}
|
||||
|
||||
private static (string Username, string Password) ResolveCredentials(Site site, ExportSettings settings, string sourceSystem)
|
||||
=> (FirstNonEmpty(site.UsernameOverride, GetCentralUsername(sourceSystem, settings)),
|
||||
FirstNonEmpty(site.PasswordOverride, GetCentralPassword(sourceSystem, settings)));
|
||||
private static (string Username, string Password) ResolveCredentials(Site site, SourceSystemDefinition sourceDefinition)
|
||||
=> (FirstNonEmpty(site.UsernameOverride, sourceDefinition.CentralUsername),
|
||||
FirstNonEmpty(site.PasswordOverride, sourceDefinition.CentralPassword));
|
||||
|
||||
private static string GetCentralUsername(string sourceSystem, ExportSettings settings) => sourceSystem switch
|
||||
{
|
||||
"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 ResolveSapServiceUrl(Site site, SourceSystemDefinition sourceDefinition)
|
||||
=> FirstNonEmpty(site.SapServiceUrl, sourceDefinition.CentralServiceUrl);
|
||||
|
||||
private static string NormalizeSourceSystem(string? sourceSystem)
|
||||
=> string.IsNullOrWhiteSpace(sourceSystem) ? "SAP" : sourceSystem.Trim().ToUpperInvariant();
|
||||
@@ -269,4 +271,28 @@ public class SiteExportService : ISiteExportService
|
||||
? Path.Combine(AppContext.BaseDirectory, "output")
|
||||
: 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.NotNull(package.ExportSettings);
|
||||
Assert.Null(package.ExportSettings.SapUsername);
|
||||
Assert.Null(package.ExportSettings.SapPassword);
|
||||
Assert.NotNull(package.SharePointConfig);
|
||||
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.Null(server.Username);
|
||||
Assert.Null(server.Password);
|
||||
Assert.Single(package.HanaServers);
|
||||
|
||||
var site = Assert.Single(package.Sites);
|
||||
Assert.Null(site.UsernameOverride);
|
||||
@@ -90,14 +92,47 @@ public class ConfigTransferServiceTests : IDisposable
|
||||
TimerEnabled = false,
|
||||
DebugLoggingEnabled = true,
|
||||
LocalSiteExportFolder = "D:\\site",
|
||||
LocalConsolidatedExportFolder = "D:\\consolidated",
|
||||
SapUsername = null,
|
||||
SapPassword = null,
|
||||
Bi1Username = null,
|
||||
Bi1Password = null,
|
||||
SageUsername = null,
|
||||
SagePassword = null
|
||||
LocalConsolidatedExportFolder = "D:\\consolidated"
|
||||
},
|
||||
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 =
|
||||
[
|
||||
new ConfigTransferHanaServer
|
||||
@@ -106,8 +141,6 @@ public class ConfigTransferServiceTests : IDisposable
|
||||
Name = "Server A",
|
||||
Host = "hana-a",
|
||||
Port = 30015,
|
||||
Username = null,
|
||||
Password = null,
|
||||
DatabaseName = "DB1",
|
||||
UseSsl = true,
|
||||
ValidateCertificate = false,
|
||||
@@ -152,20 +185,34 @@ public class ConfigTransferServiceTests : IDisposable
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var settings = await db.ExportSettings.SingleAsync();
|
||||
var sharePoint = await db.SharePointConfigs.SingleAsync();
|
||||
var systems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync();
|
||||
var server = await db.HanaServers.SingleAsync();
|
||||
var site = await db.Sites.SingleAsync();
|
||||
var rule = await db.FieldTransformationRules.SingleAsync();
|
||||
|
||||
Assert.Equal("preserved-sap-user", settings.SapUsername);
|
||||
Assert.Equal("preserved-sap-password", settings.SapPassword);
|
||||
Assert.Equal("preserved-bi1-user", settings.Bi1Username);
|
||||
Assert.Equal("preserved-sage-password", settings.SagePassword);
|
||||
Assert.Equal("2026-01-01", settings.DateFilter);
|
||||
Assert.Equal(5, settings.TimerHour);
|
||||
Assert.Equal(30, settings.TimerMinute);
|
||||
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("new-tenant", sharePoint.TenantId);
|
||||
|
||||
Assert.Equal("preserved-server-user", server.Username);
|
||||
Assert.Equal("preserved-server-password", server.Password);
|
||||
var sapSystem = Assert.Single(systems, x => x.Code == "SAP");
|
||||
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.Equal("preserved-site-user", site.UsernameOverride);
|
||||
@@ -188,23 +235,50 @@ public class ConfigTransferServiceTests : IDisposable
|
||||
ClientId = "client",
|
||||
ClientSecret = "secret"
|
||||
});
|
||||
db.ExportSettings.Add(new ExportSettings
|
||||
{
|
||||
SapUsername = "sap-user",
|
||||
SapPassword = "sap-password",
|
||||
Bi1Username = "bi1-user",
|
||||
Bi1Password = "bi1-password",
|
||||
SageUsername = "sage-user",
|
||||
SagePassword = "sage-password"
|
||||
});
|
||||
db.ExportSettings.Add(new ExportSettings());
|
||||
db.SourceSystemDefinitions.AddRange(
|
||||
new SourceSystemDefinition
|
||||
{
|
||||
Code = "SAP",
|
||||
DisplayName = "SAP",
|
||||
ConnectionKind = SourceSystemConnectionKinds.SapGateway,
|
||||
IsActive = true,
|
||||
CentralUsername = "sap-user",
|
||||
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
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Server A",
|
||||
Host = "hana-a",
|
||||
Port = 30015,
|
||||
Username = "server-user",
|
||||
Password = "server-password",
|
||||
Username = string.Empty,
|
||||
Password = string.Empty,
|
||||
DatabaseName = "DB1"
|
||||
});
|
||||
db.Sites.Add(new Site
|
||||
@@ -246,15 +320,42 @@ public class ConfigTransferServiceTests : IDisposable
|
||||
ClientId = "old-client",
|
||||
ClientSecret = "preserved-sharepoint-secret"
|
||||
});
|
||||
db.ExportSettings.Add(new ExportSettings
|
||||
{
|
||||
SapUsername = "preserved-sap-user",
|
||||
SapPassword = "preserved-sap-password",
|
||||
Bi1Username = "preserved-bi1-user",
|
||||
Bi1Password = "preserved-bi1-password",
|
||||
SageUsername = "preserved-sage-user",
|
||||
SagePassword = "preserved-sage-password"
|
||||
});
|
||||
db.ExportSettings.Add(new ExportSettings());
|
||||
db.SourceSystemDefinitions.AddRange(
|
||||
new SourceSystemDefinition
|
||||
{
|
||||
Code = "SAP",
|
||||
DisplayName = "SAP",
|
||||
ConnectionKind = SourceSystemConnectionKinds.SapGateway,
|
||||
IsActive = true,
|
||||
CentralUsername = "preserved-sap-user",
|
||||
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
|
||||
{
|
||||
Id = 1,
|
||||
|
||||
Reference in New Issue
Block a user