Add connection diagnostics and visual field transformation mapping
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
# Build artifacts
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
|
||||||
|
# Visual Studio user/IDE files
|
||||||
|
.vs/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
@@ -5,6 +5,9 @@
|
|||||||
<MudNavLink Href="/standorte" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.LocationOn">
|
<MudNavLink Href="/standorte" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.LocationOn">
|
||||||
Standorte
|
Standorte
|
||||||
</MudNavLink>
|
</MudNavLink>
|
||||||
|
<MudNavLink Href="/transformations" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Transform">
|
||||||
|
Transformationen
|
||||||
|
</MudNavLink>
|
||||||
<MudNavLink Href="/settings" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Settings">
|
<MudNavLink Href="/settings" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Settings">
|
||||||
Settings
|
Settings
|
||||||
</MudNavLink>
|
</MudNavLink>
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
|
|
||||||
<MudText Typo="Typo.h4" Class="mb-4">Standorte</MudText>
|
<MudText Typo="Typo.h4" Class="mb-4">Standorte</MudText>
|
||||||
|
|
||||||
@* HANA Server Section *@
|
|
||||||
<MudText Typo="Typo.h5" Class="mb-2">HANA Server</MudText>
|
<MudText Typo="Typo.h5" Class="mb-2">HANA Server</MudText>
|
||||||
<MudPaper Class="pa-4 mb-6" Elevation="1">
|
<MudPaper Class="pa-4 mb-6" Elevation="1">
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add"
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add"
|
||||||
@@ -25,6 +24,7 @@
|
|||||||
<MudTh>Host</MudTh>
|
<MudTh>Host</MudTh>
|
||||||
<MudTh>Port</MudTh>
|
<MudTh>Port</MudTh>
|
||||||
<MudTh>Username</MudTh>
|
<MudTh>Username</MudTh>
|
||||||
|
<MudTh>Verbindungsstatus</MudTh>
|
||||||
<MudTh>Aktionen</MudTh>
|
<MudTh>Aktionen</MudTh>
|
||||||
</HeaderContent>
|
</HeaderContent>
|
||||||
<RowTemplate>
|
<RowTemplate>
|
||||||
@@ -32,6 +32,20 @@
|
|||||||
<MudTd>@context.Host</MudTd>
|
<MudTd>@context.Host</MudTd>
|
||||||
<MudTd>@context.Port</MudTd>
|
<MudTd>@context.Port</MudTd>
|
||||||
<MudTd>@context.Username</MudTd>
|
<MudTd>@context.Username</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
@if (_connectionStatus.TryGetValue(context.Id, out var status))
|
||||||
|
{
|
||||||
|
<MudTooltip Text="@BuildStatusTooltip(status)">
|
||||||
|
<MudChip Color="@(status.Success ? Color.Success : Color.Error)" Variant="Variant.Filled" Size="Size.Small">
|
||||||
|
@(status.Success ? "OK" : "Fehler") - @status.Stage
|
||||||
|
</MudChip>
|
||||||
|
</MudTooltip>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudChip Color="Color.Default" Variant="Variant.Outlined" Size="Size.Small">Nicht getestet</MudChip>
|
||||||
|
}
|
||||||
|
</MudTd>
|
||||||
<MudTd>
|
<MudTd>
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Small"
|
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Small"
|
||||||
OnClick="() => EditServer(context)" />
|
OnClick="() => EditServer(context)" />
|
||||||
@@ -44,7 +58,6 @@
|
|||||||
</MudTable>
|
</MudTable>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
@* Sites Section *@
|
|
||||||
<MudText Typo="Typo.h5" Class="mb-2">Standorte (Sites)</MudText>
|
<MudText Typo="Typo.h5" Class="mb-2">Standorte (Sites)</MudText>
|
||||||
<MudPaper Class="pa-4" Elevation="1">
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add"
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add"
|
||||||
@@ -57,6 +70,7 @@
|
|||||||
<MudTh>Land</MudTh>
|
<MudTh>Land</MudTh>
|
||||||
<MudTh>TSC</MudTh>
|
<MudTh>TSC</MudTh>
|
||||||
<MudTh>Schema</MudTh>
|
<MudTh>Schema</MudTh>
|
||||||
|
<MudTh>Quellsystem</MudTh>
|
||||||
<MudTh>Server</MudTh>
|
<MudTh>Server</MudTh>
|
||||||
<MudTh>Aktiv</MudTh>
|
<MudTh>Aktiv</MudTh>
|
||||||
<MudTh>Aktionen</MudTh>
|
<MudTh>Aktionen</MudTh>
|
||||||
@@ -65,6 +79,7 @@
|
|||||||
<MudTd>@context.Land</MudTd>
|
<MudTd>@context.Land</MudTd>
|
||||||
<MudTd>@context.TSC</MudTd>
|
<MudTd>@context.TSC</MudTd>
|
||||||
<MudTd>@context.Schema</MudTd>
|
<MudTd>@context.Schema</MudTd>
|
||||||
|
<MudTd>@context.SourceSystem</MudTd>
|
||||||
<MudTd>@(context.HanaServer?.Name ?? "-")</MudTd>
|
<MudTd>@(context.HanaServer?.Name ?? "-")</MudTd>
|
||||||
<MudTd>
|
<MudTd>
|
||||||
@if (context.IsActive)
|
@if (context.IsActive)
|
||||||
@@ -86,7 +101,6 @@
|
|||||||
</MudTable>
|
</MudTable>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
@* Server Dialog *@
|
|
||||||
<MudDialog @bind-Visible="_serverDialogVisible" Options="_dialogOptions">
|
<MudDialog @bind-Visible="_serverDialogVisible" Options="_dialogOptions">
|
||||||
<TitleContent>
|
<TitleContent>
|
||||||
<MudText Typo="Typo.h6">@(_editingServer.Id == 0 ? "Server hinzufügen" : "Server bearbeiten")</MudText>
|
<MudText Typo="Typo.h6">@(_editingServer.Id == 0 ? "Server hinzufügen" : "Server bearbeiten")</MudText>
|
||||||
@@ -113,7 +127,6 @@
|
|||||||
</DialogActions>
|
</DialogActions>
|
||||||
</MudDialog>
|
</MudDialog>
|
||||||
|
|
||||||
@* Site Dialog *@
|
|
||||||
<MudDialog @bind-Visible="_siteDialogVisible" Options="_dialogOptions">
|
<MudDialog @bind-Visible="_siteDialogVisible" Options="_dialogOptions">
|
||||||
<TitleContent>
|
<TitleContent>
|
||||||
<MudText Typo="Typo.h6">@(_editingSite.Id == 0 ? "Standort hinzufügen" : "Standort bearbeiten")</MudText>
|
<MudText Typo="Typo.h6">@(_editingSite.Id == 0 ? "Standort hinzufügen" : "Standort bearbeiten")</MudText>
|
||||||
@@ -128,6 +141,12 @@
|
|||||||
<MudTextField @bind-Value="_editingSite.Schema" Label="Schema" Required />
|
<MudTextField @bind-Value="_editingSite.Schema" Label="Schema" Required />
|
||||||
<MudTextField @bind-Value="_editingSite.TSC" Label="TSC" Required />
|
<MudTextField @bind-Value="_editingSite.TSC" Label="TSC" Required />
|
||||||
<MudTextField @bind-Value="_editingSite.Land" Label="Land" Required />
|
<MudTextField @bind-Value="_editingSite.Land" Label="Land" Required />
|
||||||
|
<MudSelect @bind-Value="_editingSite.SourceSystem" Label="Quellsystem" Required>
|
||||||
|
@foreach (var system in _sourceSystems)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="system">@system</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
<MudCheckBox @bind-Value="_editingSite.IsActive" Label="Aktiv" />
|
<MudCheckBox @bind-Value="_editingSite.IsActive" Label="Aktiv" />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
@@ -137,6 +156,8 @@
|
|||||||
</MudDialog>
|
</MudDialog>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
private readonly string[] _sourceSystems = ["SAP", "BI1", "SAGE"];
|
||||||
|
private readonly Dictionary<int, ConnectionTestResult> _connectionStatus = new();
|
||||||
private List<HanaServer> _servers = new();
|
private List<HanaServer> _servers = new();
|
||||||
private List<Site> _sites = new();
|
private List<Site> _sites = new();
|
||||||
private HanaServer _editingServer = new();
|
private HanaServer _editingServer = new();
|
||||||
@@ -157,7 +178,6 @@
|
|||||||
_sites = await db.Sites.Include(s => s.HanaServer).OrderBy(s => s.Land).ToListAsync();
|
_sites = await db.Sites.Include(s => s.HanaServer).OrderBy(s => s.Land).ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server CRUD
|
|
||||||
private void AddServer()
|
private void AddServer()
|
||||||
{
|
{
|
||||||
_editingServer = new HanaServer { Port = 30015 };
|
_editingServer = new HanaServer { Port = 30015 };
|
||||||
@@ -205,6 +225,7 @@
|
|||||||
existing.AdditionalParams = _editingServer.AdditionalParams;
|
existing.AdditionalParams = _editingServer.AdditionalParams;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
_serverDialogVisible = false;
|
_serverDialogVisible = false;
|
||||||
await LoadDataAsync();
|
await LoadDataAsync();
|
||||||
@@ -227,29 +248,41 @@
|
|||||||
db.HanaServers.Remove(entity);
|
db.HanaServers.Remove(entity);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
await LoadDataAsync();
|
await LoadDataAsync();
|
||||||
Snackbar.Add("Server gelöscht", Severity.Info);
|
Snackbar.Add("Server gelöscht", Severity.Info);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task TestServerConnection(HanaServer server)
|
private async Task TestServerConnection(HanaServer server)
|
||||||
{
|
{
|
||||||
try
|
var result = await Task.Run(() => HanaService.TestConnectionDetailed(server));
|
||||||
|
_connectionStatus[server.Id] = result;
|
||||||
|
|
||||||
|
if (result.Success)
|
||||||
{
|
{
|
||||||
await Task.Run(() => HanaService.TestConnection(server));
|
Snackbar.Add($"Verbindung zu '{server.Name}' erfolgreich.", Severity.Success);
|
||||||
Snackbar.Add($"Verbindung zu '{server.Name}' erfolgreich!", Severity.Success);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
else
|
||||||
{
|
{
|
||||||
Snackbar.Add($"Verbindung fehlgeschlagen: {ex.Message}", Severity.Error);
|
Snackbar.Add($"{server.Name}: {result.ExceptionType} - {result.ErrorMessage}", Severity.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Site CRUD
|
private static string BuildStatusTooltip(ConnectionTestResult status)
|
||||||
|
{
|
||||||
|
var stamp = status.TestedAtUtc.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
|
||||||
|
if (status.Success)
|
||||||
|
return $"Letzter Test: {stamp}\nStage: {status.Stage}\n{status.ConnectionStringPreview}";
|
||||||
|
|
||||||
|
return $"Letzter Test: {stamp}\nStage: {status.Stage}\nFehler: {status.ErrorMessage}\n{status.ConnectionStringPreview}";
|
||||||
|
}
|
||||||
|
|
||||||
private void AddSite()
|
private void AddSite()
|
||||||
{
|
{
|
||||||
_editingSite = new Site
|
_editingSite = new Site
|
||||||
{
|
{
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
|
SourceSystem = "SAP",
|
||||||
HanaServerId = _servers.FirstOrDefault()?.Id ?? 0
|
HanaServerId = _servers.FirstOrDefault()?.Id ?? 0
|
||||||
};
|
};
|
||||||
_siteDialogVisible = true;
|
_siteDialogVisible = true;
|
||||||
@@ -264,6 +297,7 @@
|
|||||||
Schema = site.Schema,
|
Schema = site.Schema,
|
||||||
TSC = site.TSC,
|
TSC = site.TSC,
|
||||||
Land = site.Land,
|
Land = site.Land,
|
||||||
|
SourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) ? "SAP" : site.SourceSystem,
|
||||||
IsActive = site.IsActive
|
IsActive = site.IsActive
|
||||||
};
|
};
|
||||||
_siteDialogVisible = true;
|
_siteDialogVisible = true;
|
||||||
@@ -285,9 +319,11 @@
|
|||||||
existing.Schema = _editingSite.Schema;
|
existing.Schema = _editingSite.Schema;
|
||||||
existing.TSC = _editingSite.TSC;
|
existing.TSC = _editingSite.TSC;
|
||||||
existing.Land = _editingSite.Land;
|
existing.Land = _editingSite.Land;
|
||||||
|
existing.SourceSystem = _editingSite.SourceSystem;
|
||||||
existing.IsActive = _editingSite.IsActive;
|
existing.IsActive = _editingSite.IsActive;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
_siteDialogVisible = false;
|
_siteDialogVisible = false;
|
||||||
await LoadDataAsync();
|
await LoadDataAsync();
|
||||||
@@ -310,6 +346,7 @@
|
|||||||
db.Sites.Remove(entity);
|
db.Sites.Remove(entity);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
await LoadDataAsync();
|
await LoadDataAsync();
|
||||||
Snackbar.Add("Standort gelöscht", Severity.Info);
|
Snackbar.Add("Standort gelöscht", Severity.Info);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
@page "/transformations"
|
||||||
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
@using System.Reflection
|
||||||
|
@using TrafagSalesExporter.Data
|
||||||
|
@using TrafagSalesExporter.Models
|
||||||
|
@inject IDbContextFactory<AppDbContext> DbFactory
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
|
<PageTitle>Transformationen</PageTitle>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h4" Class="mb-4">Transformer Ansicht</MudText>
|
||||||
|
<MudText Typo="Typo.body1" Class="mb-4">Definiere pro Quellsystem (SAP, BI1, SAGE) Feld-Remapping und Transformationen.</MudText>
|
||||||
|
|
||||||
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudStack Row="true" Spacing="2" Class="mb-3">
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" OnClick="AddRule">
|
||||||
|
Regel hinzufügen
|
||||||
|
</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" StartIcon="@Icons.Material.Filled.Save" OnClick="SaveAllAsync">
|
||||||
|
Alle speichern
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudTable Items="_rules" Dense Hover Striped>
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>Aktiv</MudTh>
|
||||||
|
<MudTh>System</MudTh>
|
||||||
|
<MudTh>Source</MudTh>
|
||||||
|
<MudTh>Target</MudTh>
|
||||||
|
<MudTh>Typ</MudTh>
|
||||||
|
<MudTh>Argument</MudTh>
|
||||||
|
<MudTh>Sort</MudTh>
|
||||||
|
<MudTh>Aktionen</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<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)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="system">@system</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudSelect T="string" Value="@context.SourceField" ValueChanged="@(v => context.SourceField = v)" Dense>
|
||||||
|
@foreach (var field in _recordFields)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="field">@field</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudSelect T="string" Value="@context.TargetField" ValueChanged="@(v => context.TargetField = v)" Dense>
|
||||||
|
@foreach (var field in _recordFields)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="field">@field</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudSelect T="string" Value="@context.TransformationType" ValueChanged="@(v => context.TransformationType = v)" Dense>
|
||||||
|
@foreach (var type in _types)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="type">@type</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudTextField Value="@context.Argument" ValueChanged="@(v => context.Argument = v)" Dense
|
||||||
|
HelperText="Replace: alt=>neu" />
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudNumericField T="int" Value="@context.SortOrder" ValueChanged="@(v => context.SortOrder = v)" Dense />
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small"
|
||||||
|
OnClick="() => RemoveRule(context)" />
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
</MudTable>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private readonly string[] _systems = ["SAP", "BI1", "SAGE"];
|
||||||
|
private readonly string[] _types = ["Copy", "Uppercase", "Lowercase", "Prefix", "Suffix", "Replace", "Constant"];
|
||||||
|
private readonly string[] _recordFields = typeof(SalesRecord)
|
||||||
|
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||||
|
.Select(p => p.Name)
|
||||||
|
.OrderBy(n => n)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
private List<FieldTransformationRule> _rules = new();
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await LoadAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadAsync()
|
||||||
|
{
|
||||||
|
using var db = await DbFactory.CreateDbContextAsync();
|
||||||
|
_rules = await db.FieldTransformationRules.OrderBy(r => r.SortOrder).ThenBy(r => r.Id).ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddRule()
|
||||||
|
{
|
||||||
|
var nextSort = _rules.Count == 0 ? 10 : _rules.Max(r => r.SortOrder) + 10;
|
||||||
|
_rules.Add(new FieldTransformationRule
|
||||||
|
{
|
||||||
|
SourceSystem = "SAP",
|
||||||
|
SourceField = nameof(SalesRecord.Material),
|
||||||
|
TargetField = nameof(SalesRecord.Material),
|
||||||
|
TransformationType = "Copy",
|
||||||
|
SortOrder = nextSort,
|
||||||
|
IsActive = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveRule(FieldTransformationRule rule)
|
||||||
|
{
|
||||||
|
_rules.Remove(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveAllAsync()
|
||||||
|
{
|
||||||
|
using var db = await DbFactory.CreateDbContextAsync();
|
||||||
|
db.FieldTransformationRules.RemoveRange(db.FieldTransformationRules);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
db.FieldTransformationRules.AddRange(_rules);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
Snackbar.Add("Transformationsregeln gespeichert.", Severity.Success);
|
||||||
|
await LoadAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ public class AppDbContext : DbContext
|
|||||||
public DbSet<SharePointConfig> SharePointConfigs => Set<SharePointConfig>();
|
public DbSet<SharePointConfig> SharePointConfigs => Set<SharePointConfig>();
|
||||||
public DbSet<ExportSettings> ExportSettings => Set<ExportSettings>();
|
public DbSet<ExportSettings> ExportSettings => Set<ExportSettings>();
|
||||||
public DbSet<ExportLog> ExportLogs => Set<ExportLog>();
|
public DbSet<ExportLog> ExportLogs => Set<ExportLog>();
|
||||||
|
public DbSet<FieldTransformationRule> FieldTransformationRules => Set<FieldTransformationRule>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fügt Spalten zu existierenden Tabellen hinzu, die bei neueren Versionen
|
/// Fügt Spalten zu existierenden Tabellen hinzu, die bei neueren Versionen
|
||||||
@@ -24,6 +25,8 @@ public class AppDbContext : DbContext
|
|||||||
AddColumnIfMissing(db, "HanaServers", "UseSsl", "INTEGER NOT NULL DEFAULT 0");
|
AddColumnIfMissing(db, "HanaServers", "UseSsl", "INTEGER NOT NULL DEFAULT 0");
|
||||||
AddColumnIfMissing(db, "HanaServers", "ValidateCertificate", "INTEGER NOT NULL DEFAULT 0");
|
AddColumnIfMissing(db, "HanaServers", "ValidateCertificate", "INTEGER NOT NULL DEFAULT 0");
|
||||||
AddColumnIfMissing(db, "HanaServers", "AdditionalParams", "TEXT NOT NULL DEFAULT ''");
|
AddColumnIfMissing(db, "HanaServers", "AdditionalParams", "TEXT NOT NULL DEFAULT ''");
|
||||||
|
AddColumnIfMissing(db, "Sites", "SourceSystem", "TEXT NOT NULL DEFAULT 'SAP'");
|
||||||
|
EnsureTransformationTable(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddColumnIfMissing(AppDbContext db, string table, string column, string type)
|
private static void AddColumnIfMissing(AppDbContext db, string table, string column, string type)
|
||||||
@@ -54,6 +57,26 @@ public class AppDbContext : DbContext
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void EnsureTransformationTable(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 FieldTransformationRules (
|
||||||
|
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
SourceSystem TEXT NOT NULL DEFAULT 'SAP',
|
||||||
|
SourceField TEXT NOT NULL,
|
||||||
|
TargetField TEXT NOT NULL,
|
||||||
|
TransformationType TEXT NOT NULL,
|
||||||
|
Argument TEXT NOT NULL DEFAULT '',
|
||||||
|
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||||
|
IsActive INTEGER NOT NULL DEFAULT 1
|
||||||
|
);";
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
public static void SeedIfEmpty(AppDbContext db)
|
public static void SeedIfEmpty(AppDbContext db)
|
||||||
{
|
{
|
||||||
if (db.HanaServers.Any()) return;
|
if (db.HanaServers.Any()) return;
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace TrafagSalesExporter.Models;
|
||||||
|
|
||||||
|
public class FieldTransformationRule
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string SourceSystem { get; set; } = "SAP";
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string SourceField { get; set; } = nameof(SalesRecord.Material);
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string TargetField { get; set; } = nameof(SalesRecord.Material);
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string TransformationType { get; set; } = "Copy";
|
||||||
|
|
||||||
|
public string Argument { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
}
|
||||||
@@ -62,4 +62,23 @@ public class HanaServer
|
|||||||
|
|
||||||
return string.Join(";", parts);
|
return string.Join(";", parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string GetConnectionStringPreview()
|
||||||
|
{
|
||||||
|
var pwdMasked = string.IsNullOrEmpty(Password) ? "" : "***";
|
||||||
|
var copy = new HanaServer
|
||||||
|
{
|
||||||
|
Host = Host,
|
||||||
|
Port = Port,
|
||||||
|
Username = Username,
|
||||||
|
Password = pwdMasked,
|
||||||
|
DatabaseName = DatabaseName,
|
||||||
|
UseSsl = UseSsl,
|
||||||
|
ValidateCertificate = ValidateCertificate,
|
||||||
|
AdditionalParams = AdditionalParams
|
||||||
|
};
|
||||||
|
|
||||||
|
return copy.BuildConnectionString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,5 +21,8 @@ public class Site
|
|||||||
[Required]
|
[Required]
|
||||||
public string Land { get; set; } = string.Empty;
|
public string Land { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string SourceSystem { get; set; } = "SAP";
|
||||||
|
|
||||||
public bool IsActive { get; set; } = true;
|
public bool IsActive { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ builder.Services.AddDbContextFactory<AppDbContext>(options =>
|
|||||||
builder.Services.AddSingleton<HanaQueryService>();
|
builder.Services.AddSingleton<HanaQueryService>();
|
||||||
builder.Services.AddSingleton<ExcelExportService>();
|
builder.Services.AddSingleton<ExcelExportService>();
|
||||||
builder.Services.AddSingleton<SharePointUploadService>();
|
builder.Services.AddSingleton<SharePointUploadService>();
|
||||||
|
builder.Services.AddSingleton<RecordTransformationService>();
|
||||||
builder.Services.AddSingleton<ExportOrchestrationService>();
|
builder.Services.AddSingleton<ExportOrchestrationService>();
|
||||||
builder.Services.AddSingleton<TimerBackgroundService>();
|
builder.Services.AddSingleton<TimerBackgroundService>();
|
||||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<TimerBackgroundService>());
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<TimerBackgroundService>());
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ public class ExportOrchestrationService
|
|||||||
private readonly HanaQueryService _hanaService;
|
private readonly HanaQueryService _hanaService;
|
||||||
private readonly ExcelExportService _excelService;
|
private readonly ExcelExportService _excelService;
|
||||||
private readonly SharePointUploadService _sharePointService;
|
private readonly SharePointUploadService _sharePointService;
|
||||||
|
private readonly RecordTransformationService _transformationService;
|
||||||
private readonly ILogger<ExportOrchestrationService> _logger;
|
private readonly ILogger<ExportOrchestrationService> _logger;
|
||||||
|
|
||||||
public event Action? OnExportStatusChanged;
|
public event Action? OnExportStatusChanged;
|
||||||
@@ -23,12 +24,14 @@ public class ExportOrchestrationService
|
|||||||
HanaQueryService hanaService,
|
HanaQueryService hanaService,
|
||||||
ExcelExportService excelService,
|
ExcelExportService excelService,
|
||||||
SharePointUploadService sharePointService,
|
SharePointUploadService sharePointService,
|
||||||
|
RecordTransformationService transformationService,
|
||||||
ILogger<ExportOrchestrationService> logger)
|
ILogger<ExportOrchestrationService> logger)
|
||||||
{
|
{
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
_hanaService = hanaService;
|
_hanaService = hanaService;
|
||||||
_excelService = excelService;
|
_excelService = excelService;
|
||||||
_sharePointService = sharePointService;
|
_sharePointService = sharePointService;
|
||||||
|
_transformationService = transformationService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +99,13 @@ public class ExportOrchestrationService
|
|||||||
var records = await Task.Run(() => _hanaService.GetSalesRecords(
|
var records = await Task.Run(() => _hanaService.GetSalesRecords(
|
||||||
site.HanaServer, site.Schema, site.TSC, site.Land, settings.DateFilter));
|
site.HanaServer, site.Schema, site.TSC, site.Land, settings.DateFilter));
|
||||||
|
|
||||||
|
UpdateStatus(site.Id, "Transformationen anwenden...");
|
||||||
|
var rules = await db.FieldTransformationRules
|
||||||
|
.Where(r => r.IsActive && r.SourceSystem == (string.IsNullOrWhiteSpace(site.SourceSystem) ? "SAP" : site.SourceSystem))
|
||||||
|
.OrderBy(r => r.SortOrder)
|
||||||
|
.ToListAsync();
|
||||||
|
_transformationService.Apply(records, rules);
|
||||||
|
|
||||||
UpdateStatus(site.Id, "Excel erstellen...");
|
UpdateStatus(site.Id, "Excel erstellen...");
|
||||||
var outputDir = Path.Combine(AppContext.BaseDirectory, "output");
|
var outputDir = Path.Combine(AppContext.BaseDirectory, "output");
|
||||||
var filePath = _excelService.CreateExcelFile(outputDir, site.TSC, DateTime.UtcNow.Date, records);
|
var filePath = _excelService.CreateExcelFile(outputDir, site.TSC, DateTime.UtcNow.Date, records);
|
||||||
|
|||||||
@@ -32,6 +32,38 @@ public class HanaQueryService
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ConnectionTestResult TestConnectionDetailed(HanaServer server)
|
||||||
|
{
|
||||||
|
var testResult = new ConnectionTestResult
|
||||||
|
{
|
||||||
|
TestedAtUtc = DateTime.UtcNow,
|
||||||
|
ConnectionStringPreview = server.GetConnectionStringPreview(),
|
||||||
|
Stage = "Verbindungsaufbau"
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var connectionString = server.BuildConnectionString();
|
||||||
|
using var connection = new HanaConnection(connectionString);
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
testResult.Stage = "Ping-Query";
|
||||||
|
using var command = new HanaCommand("SELECT 1 FROM DUMMY", connection);
|
||||||
|
command.ExecuteScalar();
|
||||||
|
|
||||||
|
testResult.Success = true;
|
||||||
|
testResult.Stage = "OK";
|
||||||
|
return testResult;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
testResult.Success = false;
|
||||||
|
testResult.ErrorMessage = ex.Message;
|
||||||
|
testResult.ExceptionType = ex.GetType().Name;
|
||||||
|
return testResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void TestConnection(HanaServer server)
|
public void TestConnection(HanaServer server)
|
||||||
{
|
{
|
||||||
var connectionString = server.BuildConnectionString();
|
var connectionString = server.BuildConnectionString();
|
||||||
@@ -173,3 +205,13 @@ LEFT JOIN {schema}.""OSLP"" emp ON h.""SlpCode"" = emp.""SlpCode""
|
|||||||
WHERE h.""CANCELED"" = 'N' AND h.""DocDate"" >= '{dateFilter}'
|
WHERE h.""CANCELED"" = 'N' AND h.""DocDate"" >= '{dateFilter}'
|
||||||
ORDER BY h.""DocDate"" DESC, h.""DocNum"", p.""LineNum""";
|
ORDER BY h.""DocDate"" DESC, h.""DocNum"", p.""LineNum""";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ConnectionTestResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public DateTime TestedAtUtc { get; set; }
|
||||||
|
public string Stage { get; set; } = string.Empty;
|
||||||
|
public string ErrorMessage { get; set; } = string.Empty;
|
||||||
|
public string ExceptionType { get; set; } = string.Empty;
|
||||||
|
public string ConnectionStringPreview { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using TrafagSalesExporter.Models;
|
||||||
|
|
||||||
|
namespace TrafagSalesExporter.Services;
|
||||||
|
|
||||||
|
public class RecordTransformationService
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, PropertyInfo> PropertyMap = typeof(SalesRecord)
|
||||||
|
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||||
|
.ToDictionary(p => p.Name, p => p, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public void Apply(List<SalesRecord> records, IEnumerable<FieldTransformationRule> rules)
|
||||||
|
{
|
||||||
|
var orderedRules = rules.Where(r => r.IsActive).OrderBy(r => r.SortOrder).ToList();
|
||||||
|
if (orderedRules.Count == 0 || records.Count == 0) return;
|
||||||
|
|
||||||
|
foreach (var record in records)
|
||||||
|
{
|
||||||
|
foreach (var rule in orderedRules)
|
||||||
|
{
|
||||||
|
ApplyRule(record, rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyRule(SalesRecord record, FieldTransformationRule rule)
|
||||||
|
{
|
||||||
|
if (!PropertyMap.TryGetValue(rule.SourceField, out var sourceProp)) return;
|
||||||
|
if (!PropertyMap.TryGetValue(rule.TargetField, out var targetProp)) return;
|
||||||
|
|
||||||
|
var sourceValue = sourceProp.GetValue(record);
|
||||||
|
object? result = rule.TransformationType switch
|
||||||
|
{
|
||||||
|
"Copy" => sourceValue,
|
||||||
|
"Uppercase" => sourceValue?.ToString()?.ToUpperInvariant(),
|
||||||
|
"Lowercase" => sourceValue?.ToString()?.ToLowerInvariant(),
|
||||||
|
"Prefix" => $"{rule.Argument}{sourceValue}",
|
||||||
|
"Suffix" => $"{sourceValue}{rule.Argument}",
|
||||||
|
"Replace" => ApplyReplace(sourceValue?.ToString(), rule.Argument),
|
||||||
|
"Constant" => rule.Argument,
|
||||||
|
_ => sourceValue
|
||||||
|
};
|
||||||
|
|
||||||
|
SetPropertyValue(record, targetProp, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ApplyReplace(string? input, string? argument)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input)) return string.Empty;
|
||||||
|
if (string.IsNullOrWhiteSpace(argument)) return input;
|
||||||
|
|
||||||
|
var parts = argument.Split("=>", 2, StringSplitOptions.TrimEntries);
|
||||||
|
if (parts.Length != 2) return input;
|
||||||
|
return input.Replace(parts[0], parts[1], StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetPropertyValue(SalesRecord record, PropertyInfo property, object? value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (property.PropertyType == typeof(string))
|
||||||
|
{
|
||||||
|
property.SetValue(record, value?.ToString() ?? string.Empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property.PropertyType == typeof(int))
|
||||||
|
{
|
||||||
|
if (int.TryParse(value?.ToString(), out var parsedInt)) property.SetValue(record, parsedInt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property.PropertyType == typeof(decimal))
|
||||||
|
{
|
||||||
|
if (decimal.TryParse(value?.ToString(), out var parsedDecimal)) property.SetValue(record, parsedDecimal);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property.PropertyType == typeof(DateTime?) || property.PropertyType == typeof(DateTime))
|
||||||
|
{
|
||||||
|
if (DateTime.TryParse(value?.ToString(), out var parsedDate)) property.SetValue(record, parsedDate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
property.SetValue(record, value);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// skip invalid conversion to keep export running
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user