Merge pull request #58 from metacube2/codex/fix-git-permission-denied-error

Add field transformation rules, UI, DB schema and integrate into export; improve HANA connection testing
This commit is contained in:
2026-04-13 11:52:17 +02:00
committed by GitHub
11 changed files with 404 additions and 11 deletions
@@ -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();
}
}
+23
View File
@@ -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;
}
+19
View File
@@ -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();
}
} }
+3
View File
@@ -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;
} }
+1
View File
@@ -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
}
}
}