import exxport settings, join over sap hana tables

This commit is contained in:
2026-04-14 11:34:43 +02:00
parent 36a22202bf
commit 59e195af71
21 changed files with 1369 additions and 16 deletions
@@ -116,7 +116,9 @@
Land = s.Land,
TSC = s.TSC,
Schema = s.Schema,
ServerName = s.HanaServer?.Name ?? "",
ServerName = string.Equals(s.SourceSystem, "SAP", StringComparison.OrdinalIgnoreCase)
? (string.IsNullOrWhiteSpace(s.SapServiceUrl) ? "SAP Gateway" : s.SapServiceUrl)
: s.HanaServer?.Name ?? "",
LastStatus = log?.Status ?? "",
RowCount = log?.RowCount ?? 0,
LastRun = log?.Timestamp,
@@ -8,12 +8,39 @@
@inject TimerBackgroundService TimerService
@inject IHanaQueryService HanaService
@inject ISapGatewayService SapGatewayService
@inject IConfigTransferService ConfigTransferService
@inject IJSRuntime JS
@inject ISnackbar Snackbar
<PageTitle>Settings</PageTitle>
<MudText Typo="Typo.h4" Class="mb-4">Settings</MudText>
<MudText Typo="Typo.h5" Class="mb-2">Konfiguration Import/Export</MudText>
<MudPaper Class="pa-4 mb-6" Elevation="1">
<MudGrid>
<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.
</MudText>
</MudItem>
<MudItem xs="12" md="6">
<MudStack Row Spacing="2">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ExportConfiguration"
StartIcon="@Icons.Material.Filled.Download" Disabled="_exportingConfig">
@(_exportingConfig ? "Exportiere..." : "Konfiguration exportieren")
</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Warning" HtmlTag="label"
StartIcon="@Icons.Material.Filled.UploadFile" Disabled="_importingConfig">
@(_importingConfig ? "Importiere..." : "Konfiguration importieren")
<InputFile OnChange="ImportConfiguration" accept=".json,application/json" style="display:none" />
</MudButton>
</MudStack>
</MudItem>
</MudGrid>
</MudPaper>
@* SharePoint Config *@
<MudText Typo="Typo.h5" Class="mb-2">SharePoint Konfiguration</MudText>
<MudPaper Class="pa-4 mb-6" Elevation="1">
@@ -166,6 +193,9 @@
private SharePointConfig _spConfig = new();
private ExportSettings _exportSettings = new();
private bool _testingSp;
private bool _includeSecretsInExport;
private bool _exportingConfig;
private bool _importingConfig;
private readonly HashSet<string> _testingSystems = [];
protected override async Task OnInitializedAsync()
@@ -240,6 +270,60 @@
Snackbar.Add("Export Einstellungen gespeichert", Severity.Success);
}
private async Task ExportConfiguration()
{
if (_exportingConfig)
return;
_exportingConfig = true;
try
{
var json = await ConfigTransferService.ExportJsonAsync(_includeSecretsInExport);
var suffix = _includeSecretsInExport ? "with-secrets" : "without-secrets";
var fileName = $"trafag-config-{DateTime.UtcNow:yyyyMMdd-HHmmss}-{suffix}.json";
await JS.InvokeVoidAsync("trafagDownload.saveTextFile", fileName, json, "application/json;charset=utf-8");
Snackbar.Add("Konfiguration exportiert", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"Export fehlgeschlagen: {ex.Message}", Severity.Error);
}
finally
{
_exportingConfig = false;
}
}
private async Task ImportConfiguration(InputFileChangeEventArgs args)
{
if (_importingConfig)
return;
_importingConfig = true;
try
{
var file = args.File;
await using var stream = file.OpenReadStream(5 * 1024 * 1024);
using var reader = new StreamReader(stream);
var json = await reader.ReadToEndAsync();
await ConfigTransferService.ImportJsonAsync(json);
using var db = await DbFactory.CreateDbContextAsync();
_spConfig = await db.SharePointConfigs.FirstOrDefaultAsync() ?? new SharePointConfig();
_exportSettings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings();
TimerService.Recalculate();
Snackbar.Add("Konfiguration importiert", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"Import fehlgeschlagen: {ex.Message}", Severity.Error);
}
finally
{
_importingConfig = false;
}
}
private async Task TestCentralCredentials(string sourceSystem)
{
if (sourceSystem == "SAP")
@@ -1,6 +1,7 @@
@page "/standorte"
@using Microsoft.EntityFrameworkCore
@using System.Text.Json
@using System.Reflection
@using TrafagSalesExporter.Data
@using TrafagSalesExporter.Models
@using TrafagSalesExporter.Services
@@ -180,12 +181,110 @@
</MudText>
}
</MudStack>
<MudSelect @bind-Value="_editingSite.SapEntitySet" Label="SAP Entity Set" Required>
@foreach (var entitySet in _sapEntitySetsCache)
{
<MudSelectItem Value="@entitySet">@entitySet</MudSelectItem>
}
</MudSelect>
<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>
</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`.
</MudText>
<MudTable Items="_sapSources" Dense Hover Striped>
<HeaderContent>
<MudTh>Alias</MudTh>
<MudTh>Entity Set</MudTh>
<MudTh>Primär</MudTh>
<MudTh>Aktiv</MudTh>
<MudTh>Aktionen</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd><MudTextField @bind-Value="context.Alias" Dense /></MudTd>
<MudTd>
<MudSelect @bind-Value="context.EntitySet" Dense>
@foreach (var entitySet in _sapEntitySetsCache)
{
<MudSelectItem Value="@entitySet">@entitySet</MudSelectItem>
}
</MudSelect>
</MudTd>
<MudTd><MudCheckBox @bind-Value="context.IsPrimary" Dense /></MudTd>
<MudTd><MudCheckBox @bind-Value="context.IsActive" Dense /></MudTd>
<MudTd><MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small" OnClick="() => RemoveSapSource(context)" /></MudTd>
</RowTemplate>
</MudTable>
<MudDivider Class="my-4" />
<MudStack Row Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-2">
<MudText Typo="Typo.h6">SAP Joins</MudText>
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddSapJoin">Join hinzufügen</MudButton>
</MudStack>
<MudTable Items="_sapJoins" Dense Hover Striped>
<HeaderContent>
<MudTh>Links</MudTh>
<MudTh>Left Keys</MudTh>
<MudTh>Rechts</MudTh>
<MudTh>Right Keys</MudTh>
<MudTh>Typ</MudTh>
<MudTh>Aktiv</MudTh>
<MudTh>Aktionen</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudSelect @bind-Value="context.LeftAlias" Dense>
@foreach (var alias in GetSapAliases())
{
<MudSelectItem Value="@alias">@alias</MudSelectItem>
}
</MudSelect>
</MudTd>
<MudTd><MudTextField @bind-Value="context.LeftKeys" Dense Placeholder="VBELN,POSNR" /></MudTd>
<MudTd>
<MudSelect @bind-Value="context.RightAlias" Dense>
@foreach (var alias in GetSapAliases())
{
<MudSelectItem Value="@alias">@alias</MudSelectItem>
}
</MudSelect>
</MudTd>
<MudTd><MudTextField @bind-Value="context.RightKeys" Dense Placeholder="VBELN,POSNR" /></MudTd>
<MudTd>
<MudSelect @bind-Value="context.JoinType" Dense>
<MudSelectItem Value="@("Left")">Left</MudSelectItem>
</MudSelect>
</MudTd>
<MudTd><MudCheckBox @bind-Value="context.IsActive" Dense /></MudTd>
<MudTd><MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small" OnClick="() => RemoveSapJoin(context)" /></MudTd>
</RowTemplate>
</MudTable>
<MudDivider Class="my-4" />
<MudStack Row Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-2">
<MudText Typo="Typo.h6">Feldmappings ins zentrale Schema</MudText>
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddSapMapping">Mapping hinzufügen</MudButton>
</MudStack>
<MudTable Items="_sapMappings" Dense Hover Striped>
<HeaderContent>
<MudTh>Zielfeld</MudTh>
<MudTh>Source Expression</MudTh>
<MudTh>Pflicht</MudTh>
<MudTh>Aktiv</MudTh>
<MudTh>Aktionen</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudSelect @bind-Value="context.TargetField" Dense>
@foreach (var field in _salesRecordFields)
{
<MudSelectItem Value="@field">@field</MudSelectItem>
}
</MudSelect>
</MudTd>
<MudTd><MudTextField @bind-Value="context.SourceExpression" Dense Placeholder="VBAK.VBELN oder =SAP" /></MudTd>
<MudTd><MudCheckBox @bind-Value="context.IsRequired" Dense /></MudTd>
<MudTd><MudCheckBox @bind-Value="context.IsActive" Dense /></MudTd>
<MudTd><MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small" OnClick="() => RemoveSapMapping(context)" /></MudTd>
</RowTemplate>
</MudTable>
}
else
{
@@ -222,6 +321,13 @@
private List<HanaServer> _servers = new();
private List<Site> _sites = new();
private List<string> _sapEntitySetsCache = [];
private List<SapSourceDefinition> _sapSources = [];
private List<SapJoinDefinition> _sapJoins = [];
private List<SapFieldMapping> _sapMappings = [];
private readonly string[] _salesRecordFields = typeof(SalesRecord)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Select(p => p.Name)
.ToArray();
private HanaServer _editingServer = new();
private Site _editingSite = new();
private HanaServer _editingSiteServer = new();
@@ -348,9 +454,12 @@
{
IsActive = true,
SourceSystem = "SAP",
HanaServerId = 0
HanaServerId = null
};
_sapEntitySetsCache = [];
_sapSources = [];
_sapJoins = [];
_sapMappings = [];
_editingSiteServer = CreateDefaultSiteServer();
_siteDialogVisible = true;
}
@@ -374,6 +483,10 @@
IsActive = site.IsActive
};
_sapEntitySetsCache = ParseSapEntitySets(site.SapEntitySetsCache);
using var db = DbFactory.CreateDbContext();
_sapSources = db.SapSourceDefinitions.Where(s => s.SiteId == site.Id).OrderBy(s => s.SortOrder).ThenBy(s => s.Id).ToList();
_sapJoins = db.SapJoinDefinitions.Where(j => j.SiteId == site.Id).OrderBy(j => j.SortOrder).ThenBy(j => j.Id).ToList();
_sapMappings = db.SapFieldMappings.Where(m => m.SiteId == site.Id).OrderBy(m => m.SortOrder).ThenBy(m => m.Id).ToList();
_editingSiteServer = site.HanaServer is null
? CreateDefaultSiteServer(site)
: CloneServer(site.HanaServer);
@@ -418,6 +531,7 @@
}
await db.SaveChangesAsync();
await SaveSapConfigurationAsync(db, _editingSite.Id);
_siteDialogVisible = false;
await LoadDataAsync();
Snackbar.Add("Standort gespeichert", Severity.Success);
@@ -445,6 +559,14 @@
var entity = await db.Sites.FindAsync(site.Id);
if (entity is not null)
{
var sources = await db.SapSourceDefinitions.Where(s => s.SiteId == site.Id).ToListAsync();
var joins = await db.SapJoinDefinitions.Where(j => j.SiteId == site.Id).ToListAsync();
var mappings = await db.SapFieldMappings.Where(m => m.SiteId == site.Id).ToListAsync();
var centralRows = await db.CentralSalesRecords.Where(r => r.SiteId == site.Id).ToListAsync();
if (sources.Count > 0) db.SapSourceDefinitions.RemoveRange(sources);
if (joins.Count > 0) db.SapJoinDefinitions.RemoveRange(joins);
if (mappings.Count > 0) db.SapFieldMappings.RemoveRange(mappings);
if (centralRows.Count > 0) db.CentralSalesRecords.RemoveRange(centralRows);
db.Sites.Remove(entity);
await db.SaveChangesAsync();
}
@@ -632,4 +754,97 @@
private static string SerializeSapEntitySets(List<string> entitySets)
=> JsonSerializer.Serialize(entitySets);
private void AddSapSource()
{
_sapSources.Add(new SapSourceDefinition
{
Alias = $"SRC{_sapSources.Count + 1}",
EntitySet = _sapEntitySetsCache.FirstOrDefault() ?? string.Empty,
IsActive = true,
IsPrimary = _sapSources.Count == 0,
SortOrder = _sapSources.Count
});
}
private void RemoveSapSource(SapSourceDefinition source)
{
_sapSources.Remove(source);
}
private void AddSapJoin()
{
_sapJoins.Add(new SapJoinDefinition
{
JoinType = "Left",
IsActive = true,
SortOrder = _sapJoins.Count
});
}
private void RemoveSapJoin(SapJoinDefinition join)
{
_sapJoins.Remove(join);
}
private void AddSapMapping()
{
_sapMappings.Add(new SapFieldMapping
{
TargetField = _salesRecordFields.First(),
IsActive = true,
SortOrder = _sapMappings.Count
});
}
private void RemoveSapMapping(SapFieldMapping mapping)
{
_sapMappings.Remove(mapping);
}
private IEnumerable<string> GetSapAliases()
=> _sapSources.Where(s => !string.IsNullOrWhiteSpace(s.Alias)).Select(s => s.Alias).Distinct(StringComparer.OrdinalIgnoreCase);
private async Task SaveSapConfigurationAsync(AppDbContext db, int siteId)
{
var oldSources = await db.SapSourceDefinitions.Where(s => s.SiteId == siteId).ToListAsync();
var oldJoins = await db.SapJoinDefinitions.Where(j => j.SiteId == siteId).ToListAsync();
var oldMappings = await db.SapFieldMappings.Where(m => m.SiteId == siteId).ToListAsync();
if (oldSources.Count > 0) db.SapSourceDefinitions.RemoveRange(oldSources);
if (oldJoins.Count > 0) db.SapJoinDefinitions.RemoveRange(oldJoins);
if (oldMappings.Count > 0) db.SapFieldMappings.RemoveRange(oldMappings);
if (IsSapSite())
{
NormalizeSapConfigCollections();
foreach (var source in _sapSources)
source.SiteId = siteId;
foreach (var join in _sapJoins)
join.SiteId = siteId;
foreach (var mapping in _sapMappings)
mapping.SiteId = siteId;
db.SapSourceDefinitions.AddRange(_sapSources);
db.SapJoinDefinitions.AddRange(_sapJoins);
db.SapFieldMappings.AddRange(_sapMappings);
}
await db.SaveChangesAsync();
}
private void NormalizeSapConfigCollections()
{
for (var i = 0; i < _sapSources.Count; i++)
_sapSources[i].SortOrder = i;
for (var i = 0; i < _sapJoins.Count; i++)
_sapJoins[i].SortOrder = i;
for (var i = 0; i < _sapMappings.Count; i++)
_sapMappings[i].SortOrder = i;
var selectedPrimaryIndex = _sapSources.FindIndex(s => s.IsPrimary);
var primarySource = selectedPrimaryIndex >= 0 ? _sapSources[selectedPrimaryIndex] : _sapSources.FirstOrDefault();
foreach (var source in _sapSources)
source.IsPrimary = primarySource is not null && ReferenceEquals(source, primarySource);
if (_sapSources.Count > 0 && _sapSources.All(s => !s.IsPrimary))
_sapSources[0].IsPrimary = true;
}
}