From 3b6f66d0fb119df7adbda63e7a007f0320e1a518 Mon Sep 17 00:00:00 2001 From: metacube Date: Mon, 13 Apr 2026 11:22:40 +0200 Subject: [PATCH 1/4] asdf --- .gitignore | 10 ++++++++ .../Properties/launchSettings.json | 12 +++++++++ .../TrafagSalesExporter.csproj | 7 +++++- TrafagSalesExporter/TrafagSalesExporter.sln | 25 +++++++++++++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 TrafagSalesExporter/Properties/launchSettings.json create mode 100644 TrafagSalesExporter/TrafagSalesExporter.sln diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e9e8982 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Ignore Visual Studio + build artifacts +.vs/ +TrafagSalesExporter/.vs/ +TrafagSalesExporter/bin/ +TrafagSalesExporter/obj/ +TrafagSalesExporter/*.user +TrafagSalesExporter/*.suo +TrafagSalesExporter/*.db +TrafagSalesExporter/*.db-shm +TrafagSalesExporter/*.db-wal diff --git a/TrafagSalesExporter/Properties/launchSettings.json b/TrafagSalesExporter/Properties/launchSettings.json new file mode 100644 index 0000000..0626a9f --- /dev/null +++ b/TrafagSalesExporter/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "TrafagSalesExporter": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:55415;http://localhost:55416" + } + } +} \ No newline at end of file diff --git a/TrafagSalesExporter/TrafagSalesExporter.csproj b/TrafagSalesExporter/TrafagSalesExporter.csproj index ddd1092..83a293a 100644 --- a/TrafagSalesExporter/TrafagSalesExporter.csproj +++ b/TrafagSalesExporter/TrafagSalesExporter.csproj @@ -15,6 +15,11 @@ - + + + + + ..\..\..\..\..\..\Program Files\sap\hdbclient\dotnetcore\v2.1\Sap.Data.Hana.Core.v2.1.dll + diff --git a/TrafagSalesExporter/TrafagSalesExporter.sln b/TrafagSalesExporter/TrafagSalesExporter.sln new file mode 100644 index 0000000..2e8e252 --- /dev/null +++ b/TrafagSalesExporter/TrafagSalesExporter.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.37012.4 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrafagSalesExporter", "TrafagSalesExporter.csproj", "{49B56D6D-731C-6482-4A5C-82EAEEBCE593}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {49B56D6D-731C-6482-4A5C-82EAEEBCE593}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {49B56D6D-731C-6482-4A5C-82EAEEBCE593}.Debug|Any CPU.Build.0 = Debug|Any CPU + {49B56D6D-731C-6482-4A5C-82EAEEBCE593}.Release|Any CPU.ActiveCfg = Release|Any CPU + {49B56D6D-731C-6482-4A5C-82EAEEBCE593}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DC174EA0-ECCB-4957-9D97-E7ABED992867} + EndGlobalSection +EndGlobal From c336c1c7f879318ed421a28df5bc9c4eaf1713c8 Mon Sep 17 00:00:00 2001 From: Metacube Date: Mon, 13 Apr 2026 11:24:04 +0200 Subject: [PATCH 2/4] Ignore Visual Studio workspace files in TrafagSalesExporter --- TrafagSalesExporter/.gitignore | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 TrafagSalesExporter/.gitignore diff --git a/TrafagSalesExporter/.gitignore b/TrafagSalesExporter/.gitignore new file mode 100644 index 0000000..4d6b02d --- /dev/null +++ b/TrafagSalesExporter/.gitignore @@ -0,0 +1,8 @@ +# Build artifacts +bin/ +obj/ + +# Visual Studio user/IDE files +.vs/ +*.user +*.suo From ec827a4ce8020d06255367610437fdf191a62973 Mon Sep 17 00:00:00 2001 From: Metacube Date: Mon, 13 Apr 2026 11:52:05 +0200 Subject: [PATCH 3/4] Add connection diagnostics and visual field transformation mapping --- TrafagSalesExporter/.gitignore | 8 + .../Components/Layout/NavMenu.razor | 3 + .../Components/Pages/Standorte.razor | 59 ++++++-- .../Components/Pages/Transformations.razor | 137 ++++++++++++++++++ TrafagSalesExporter/Data/AppDbContext.cs | 23 +++ .../Models/FieldTransformationRule.cs | 26 ++++ TrafagSalesExporter/Models/HanaServer.cs | 19 +++ TrafagSalesExporter/Models/Site.cs | 3 + TrafagSalesExporter/Program.cs | 1 + .../Services/ExportOrchestrationService.cs | 10 ++ .../Services/HanaQueryService.cs | 42 ++++++ .../Services/RecordTransformationService.cs | 92 ++++++++++++ 12 files changed, 412 insertions(+), 11 deletions(-) create mode 100644 TrafagSalesExporter/.gitignore create mode 100644 TrafagSalesExporter/Components/Pages/Transformations.razor create mode 100644 TrafagSalesExporter/Models/FieldTransformationRule.cs create mode 100644 TrafagSalesExporter/Services/RecordTransformationService.cs diff --git a/TrafagSalesExporter/.gitignore b/TrafagSalesExporter/.gitignore new file mode 100644 index 0000000..4d6b02d --- /dev/null +++ b/TrafagSalesExporter/.gitignore @@ -0,0 +1,8 @@ +# Build artifacts +bin/ +obj/ + +# Visual Studio user/IDE files +.vs/ +*.user +*.suo diff --git a/TrafagSalesExporter/Components/Layout/NavMenu.razor b/TrafagSalesExporter/Components/Layout/NavMenu.razor index 27b1476..ecc7ae1 100644 --- a/TrafagSalesExporter/Components/Layout/NavMenu.razor +++ b/TrafagSalesExporter/Components/Layout/NavMenu.razor @@ -5,6 +5,9 @@ Standorte + + Transformationen + Settings diff --git a/TrafagSalesExporter/Components/Pages/Standorte.razor b/TrafagSalesExporter/Components/Pages/Standorte.razor index 2495b8e..d9f1486 100644 --- a/TrafagSalesExporter/Components/Pages/Standorte.razor +++ b/TrafagSalesExporter/Components/Pages/Standorte.razor @@ -11,7 +11,6 @@ Standorte -@* HANA Server Section *@ HANA Server Host Port Username + Verbindungsstatus Aktionen @@ -32,6 +32,20 @@ @context.Host @context.Port @context.Username + + @if (_connectionStatus.TryGetValue(context.Id, out var status)) + { + + + @(status.Success ? "OK" : "Fehler") - @status.Stage + + + } + else + { + Nicht getestet + } + @@ -44,7 +58,6 @@ -@* Sites Section *@ Standorte (Sites) Land TSC Schema + Quellsystem Server Aktiv Aktionen @@ -65,6 +79,7 @@ @context.Land @context.TSC @context.Schema + @context.SourceSystem @(context.HanaServer?.Name ?? "-") @if (context.IsActive) @@ -86,7 +101,6 @@ -@* Server Dialog *@ @(_editingServer.Id == 0 ? "Server hinzufügen" : "Server bearbeiten") @@ -113,7 +127,6 @@ -@* Site Dialog *@ @(_editingSite.Id == 0 ? "Standort hinzufügen" : "Standort bearbeiten") @@ -128,6 +141,12 @@ + + @foreach (var system in _sourceSystems) + { + @system + } + @@ -137,6 +156,8 @@ @code { + private readonly string[] _sourceSystems = ["SAP", "BI1", "SAGE"]; + private readonly Dictionary _connectionStatus = new(); private List _servers = new(); private List _sites = new(); private HanaServer _editingServer = new(); @@ -157,7 +178,6 @@ _sites = await db.Sites.Include(s => s.HanaServer).OrderBy(s => s.Land).ToListAsync(); } - // Server CRUD private void AddServer() { _editingServer = new HanaServer { Port = 30015 }; @@ -205,6 +225,7 @@ existing.AdditionalParams = _editingServer.AdditionalParams; } } + await db.SaveChangesAsync(); _serverDialogVisible = false; await LoadDataAsync(); @@ -227,29 +248,41 @@ db.HanaServers.Remove(entity); await db.SaveChangesAsync(); } + await LoadDataAsync(); Snackbar.Add("Server gelöscht", Severity.Info); } 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() { _editingSite = new Site { IsActive = true, + SourceSystem = "SAP", HanaServerId = _servers.FirstOrDefault()?.Id ?? 0 }; _siteDialogVisible = true; @@ -264,6 +297,7 @@ Schema = site.Schema, TSC = site.TSC, Land = site.Land, + SourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) ? "SAP" : site.SourceSystem, IsActive = site.IsActive }; _siteDialogVisible = true; @@ -285,9 +319,11 @@ existing.Schema = _editingSite.Schema; existing.TSC = _editingSite.TSC; existing.Land = _editingSite.Land; + existing.SourceSystem = _editingSite.SourceSystem; existing.IsActive = _editingSite.IsActive; } } + await db.SaveChangesAsync(); _siteDialogVisible = false; await LoadDataAsync(); @@ -310,6 +346,7 @@ db.Sites.Remove(entity); await db.SaveChangesAsync(); } + await LoadDataAsync(); Snackbar.Add("Standort gelöscht", Severity.Info); } diff --git a/TrafagSalesExporter/Components/Pages/Transformations.razor b/TrafagSalesExporter/Components/Pages/Transformations.razor new file mode 100644 index 0000000..96086a3 --- /dev/null +++ b/TrafagSalesExporter/Components/Pages/Transformations.razor @@ -0,0 +1,137 @@ +@page "/transformations" +@using Microsoft.EntityFrameworkCore +@using System.Reflection +@using TrafagSalesExporter.Data +@using TrafagSalesExporter.Models +@inject IDbContextFactory DbFactory +@inject ISnackbar Snackbar + +Transformationen + +Transformer Ansicht +Definiere pro Quellsystem (SAP, BI1, SAGE) Feld-Remapping und Transformationen. + + + + + Regel hinzufügen + + + Alle speichern + + + + + + Aktiv + System + Source + Target + Typ + Argument + Sort + Aktionen + + + + + + @foreach (var system in _systems) + { + @system + } + + + + + @foreach (var field in _recordFields) + { + @field + } + + + + + @foreach (var field in _recordFields) + { + @field + } + + + + + @foreach (var type in _types) + { + @type + } + + + + + + + + + + + + + + + +@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 _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(); + } +} diff --git a/TrafagSalesExporter/Data/AppDbContext.cs b/TrafagSalesExporter/Data/AppDbContext.cs index 6f2decd..f40ed85 100644 --- a/TrafagSalesExporter/Data/AppDbContext.cs +++ b/TrafagSalesExporter/Data/AppDbContext.cs @@ -13,6 +13,7 @@ public class AppDbContext : DbContext public DbSet SharePointConfigs => Set(); public DbSet ExportSettings => Set(); public DbSet ExportLogs => Set(); + public DbSet FieldTransformationRules => Set(); /// /// 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", "ValidateCertificate", "INTEGER NOT NULL DEFAULT 0"); 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) @@ -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) { if (db.HanaServers.Any()) return; diff --git a/TrafagSalesExporter/Models/FieldTransformationRule.cs b/TrafagSalesExporter/Models/FieldTransformationRule.cs new file mode 100644 index 0000000..8a099ce --- /dev/null +++ b/TrafagSalesExporter/Models/FieldTransformationRule.cs @@ -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; +} diff --git a/TrafagSalesExporter/Models/HanaServer.cs b/TrafagSalesExporter/Models/HanaServer.cs index a77b146..36fd579 100644 --- a/TrafagSalesExporter/Models/HanaServer.cs +++ b/TrafagSalesExporter/Models/HanaServer.cs @@ -62,4 +62,23 @@ public class HanaServer 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(); + } } + diff --git a/TrafagSalesExporter/Models/Site.cs b/TrafagSalesExporter/Models/Site.cs index 42df2d3..cb97303 100644 --- a/TrafagSalesExporter/Models/Site.cs +++ b/TrafagSalesExporter/Models/Site.cs @@ -21,5 +21,8 @@ public class Site [Required] public string Land { get; set; } = string.Empty; + [Required] + public string SourceSystem { get; set; } = "SAP"; + public bool IsActive { get; set; } = true; } diff --git a/TrafagSalesExporter/Program.cs b/TrafagSalesExporter/Program.cs index 609affa..879d8e2 100644 --- a/TrafagSalesExporter/Program.cs +++ b/TrafagSalesExporter/Program.cs @@ -16,6 +16,7 @@ builder.Services.AddDbContextFactory(options => builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHostedService(sp => sp.GetRequiredService()); diff --git a/TrafagSalesExporter/Services/ExportOrchestrationService.cs b/TrafagSalesExporter/Services/ExportOrchestrationService.cs index ae13bd1..bd3d650 100644 --- a/TrafagSalesExporter/Services/ExportOrchestrationService.cs +++ b/TrafagSalesExporter/Services/ExportOrchestrationService.cs @@ -11,6 +11,7 @@ public class ExportOrchestrationService private readonly HanaQueryService _hanaService; private readonly ExcelExportService _excelService; private readonly SharePointUploadService _sharePointService; + private readonly RecordTransformationService _transformationService; private readonly ILogger _logger; public event Action? OnExportStatusChanged; @@ -23,12 +24,14 @@ public class ExportOrchestrationService HanaQueryService hanaService, ExcelExportService excelService, SharePointUploadService sharePointService, + RecordTransformationService transformationService, ILogger logger) { _dbFactory = dbFactory; _hanaService = hanaService; _excelService = excelService; _sharePointService = sharePointService; + _transformationService = transformationService; _logger = logger; } @@ -96,6 +99,13 @@ public class ExportOrchestrationService var records = await Task.Run(() => _hanaService.GetSalesRecords( 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..."); var outputDir = Path.Combine(AppContext.BaseDirectory, "output"); var filePath = _excelService.CreateExcelFile(outputDir, site.TSC, DateTime.UtcNow.Date, records); diff --git a/TrafagSalesExporter/Services/HanaQueryService.cs b/TrafagSalesExporter/Services/HanaQueryService.cs index 4c2a9e1..3038fe8 100644 --- a/TrafagSalesExporter/Services/HanaQueryService.cs +++ b/TrafagSalesExporter/Services/HanaQueryService.cs @@ -32,6 +32,38 @@ public class HanaQueryService 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) { 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}' 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; +} diff --git a/TrafagSalesExporter/Services/RecordTransformationService.cs b/TrafagSalesExporter/Services/RecordTransformationService.cs new file mode 100644 index 0000000..71ed4d1 --- /dev/null +++ b/TrafagSalesExporter/Services/RecordTransformationService.cs @@ -0,0 +1,92 @@ +using System.Reflection; +using TrafagSalesExporter.Models; + +namespace TrafagSalesExporter.Services; + +public class RecordTransformationService +{ + private static readonly Dictionary PropertyMap = typeof(SalesRecord) + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .ToDictionary(p => p.Name, p => p, StringComparer.OrdinalIgnoreCase); + + public void Apply(List records, IEnumerable 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 + } + } +} From 97e598fe3b660bc4a6b52b9e1f75656b456492af Mon Sep 17 00:00:00 2001 From: Metacube Date: Mon, 13 Apr 2026 12:19:42 +0200 Subject: [PATCH 4/4] Fix MudBlazor generic/value callback compile errors --- TrafagSalesExporter/.gitignore | 8 + .../Components/Layout/NavMenu.razor | 3 + .../Components/Pages/Standorte.razor | 61 ++++++-- .../Components/Pages/Transformations.razor | 137 ++++++++++++++++++ TrafagSalesExporter/Data/AppDbContext.cs | 23 +++ .../Models/FieldTransformationRule.cs | 26 ++++ TrafagSalesExporter/Models/HanaServer.cs | 19 +++ TrafagSalesExporter/Models/Site.cs | 3 + TrafagSalesExporter/Program.cs | 1 + .../Services/ExportOrchestrationService.cs | 10 ++ .../Services/HanaQueryService.cs | 42 ++++++ .../Services/RecordTransformationService.cs | 92 ++++++++++++ 12 files changed, 413 insertions(+), 12 deletions(-) create mode 100644 TrafagSalesExporter/.gitignore create mode 100644 TrafagSalesExporter/Components/Pages/Transformations.razor create mode 100644 TrafagSalesExporter/Models/FieldTransformationRule.cs create mode 100644 TrafagSalesExporter/Services/RecordTransformationService.cs diff --git a/TrafagSalesExporter/.gitignore b/TrafagSalesExporter/.gitignore new file mode 100644 index 0000000..4d6b02d --- /dev/null +++ b/TrafagSalesExporter/.gitignore @@ -0,0 +1,8 @@ +# Build artifacts +bin/ +obj/ + +# Visual Studio user/IDE files +.vs/ +*.user +*.suo diff --git a/TrafagSalesExporter/Components/Layout/NavMenu.razor b/TrafagSalesExporter/Components/Layout/NavMenu.razor index 27b1476..ecc7ae1 100644 --- a/TrafagSalesExporter/Components/Layout/NavMenu.razor +++ b/TrafagSalesExporter/Components/Layout/NavMenu.razor @@ -5,6 +5,9 @@ Standorte + + Transformationen + Settings diff --git a/TrafagSalesExporter/Components/Pages/Standorte.razor b/TrafagSalesExporter/Components/Pages/Standorte.razor index 2495b8e..52aff37 100644 --- a/TrafagSalesExporter/Components/Pages/Standorte.razor +++ b/TrafagSalesExporter/Components/Pages/Standorte.razor @@ -11,7 +11,6 @@ Standorte -@* HANA Server Section *@ HANA Server Host Port Username + Verbindungsstatus Aktionen @@ -32,6 +32,20 @@ @context.Host @context.Port @context.Username + + @if (_connectionStatus.TryGetValue(context.Id, out var status)) + { + + + @(status.Success ? "OK" : "Fehler") - @status.Stage + + + } + else + { + Nicht getestet + } + @@ -44,7 +58,6 @@ -@* Sites Section *@ Standorte (Sites) Land TSC Schema + Quellsystem Server Aktiv Aktionen @@ -65,6 +79,7 @@ @context.Land @context.TSC @context.Schema + @context.SourceSystem @(context.HanaServer?.Name ?? "-") @if (context.IsActive) @@ -86,7 +101,6 @@ -@* Server Dialog *@ @(_editingServer.Id == 0 ? "Server hinzufügen" : "Server bearbeiten") @@ -113,7 +127,6 @@ -@* Site Dialog *@ @(_editingSite.Id == 0 ? "Standort hinzufügen" : "Standort bearbeiten") @@ -122,12 +135,18 @@ @foreach (var s in _servers) { - @s.Name + @s.Name } + + @foreach (var system in _sourceSystems) + { + @system + } + @@ -137,6 +156,8 @@ @code { + private readonly string[] _sourceSystems = ["SAP", "BI1", "SAGE"]; + private readonly Dictionary _connectionStatus = new(); private List _servers = new(); private List _sites = new(); private HanaServer _editingServer = new(); @@ -157,7 +178,6 @@ _sites = await db.Sites.Include(s => s.HanaServer).OrderBy(s => s.Land).ToListAsync(); } - // Server CRUD private void AddServer() { _editingServer = new HanaServer { Port = 30015 }; @@ -205,6 +225,7 @@ existing.AdditionalParams = _editingServer.AdditionalParams; } } + await db.SaveChangesAsync(); _serverDialogVisible = false; await LoadDataAsync(); @@ -227,29 +248,41 @@ db.HanaServers.Remove(entity); await db.SaveChangesAsync(); } + await LoadDataAsync(); Snackbar.Add("Server gelöscht", Severity.Info); } 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() { _editingSite = new Site { IsActive = true, + SourceSystem = "SAP", HanaServerId = _servers.FirstOrDefault()?.Id ?? 0 }; _siteDialogVisible = true; @@ -264,6 +297,7 @@ Schema = site.Schema, TSC = site.TSC, Land = site.Land, + SourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) ? "SAP" : site.SourceSystem, IsActive = site.IsActive }; _siteDialogVisible = true; @@ -285,9 +319,11 @@ existing.Schema = _editingSite.Schema; existing.TSC = _editingSite.TSC; existing.Land = _editingSite.Land; + existing.SourceSystem = _editingSite.SourceSystem; existing.IsActive = _editingSite.IsActive; } } + await db.SaveChangesAsync(); _siteDialogVisible = false; await LoadDataAsync(); @@ -310,6 +346,7 @@ db.Sites.Remove(entity); await db.SaveChangesAsync(); } + await LoadDataAsync(); Snackbar.Add("Standort gelöscht", Severity.Info); } diff --git a/TrafagSalesExporter/Components/Pages/Transformations.razor b/TrafagSalesExporter/Components/Pages/Transformations.razor new file mode 100644 index 0000000..a7a4eb6 --- /dev/null +++ b/TrafagSalesExporter/Components/Pages/Transformations.razor @@ -0,0 +1,137 @@ +@page "/transformations" +@using Microsoft.EntityFrameworkCore +@using System.Reflection +@using TrafagSalesExporter.Data +@using TrafagSalesExporter.Models +@inject IDbContextFactory DbFactory +@inject ISnackbar Snackbar + +Transformationen + +Transformer Ansicht +Definiere pro Quellsystem (SAP, BI1, SAGE) Feld-Remapping und Transformationen. + + + + + Regel hinzufügen + + + Alle speichern + + + + + + Aktiv + System + Source + Target + Typ + Argument + Sort + Aktionen + + + + + + @foreach (var system in _systems) + { + @system + } + + + + + @foreach (var field in _recordFields) + { + @field + } + + + + + @foreach (var field in _recordFields) + { + @field + } + + + + + @foreach (var type in _types) + { + @type + } + + + + + + + + + + + + + + + +@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 _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(); + } +} diff --git a/TrafagSalesExporter/Data/AppDbContext.cs b/TrafagSalesExporter/Data/AppDbContext.cs index 6f2decd..f40ed85 100644 --- a/TrafagSalesExporter/Data/AppDbContext.cs +++ b/TrafagSalesExporter/Data/AppDbContext.cs @@ -13,6 +13,7 @@ public class AppDbContext : DbContext public DbSet SharePointConfigs => Set(); public DbSet ExportSettings => Set(); public DbSet ExportLogs => Set(); + public DbSet FieldTransformationRules => Set(); /// /// 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", "ValidateCertificate", "INTEGER NOT NULL DEFAULT 0"); 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) @@ -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) { if (db.HanaServers.Any()) return; diff --git a/TrafagSalesExporter/Models/FieldTransformationRule.cs b/TrafagSalesExporter/Models/FieldTransformationRule.cs new file mode 100644 index 0000000..8a099ce --- /dev/null +++ b/TrafagSalesExporter/Models/FieldTransformationRule.cs @@ -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; +} diff --git a/TrafagSalesExporter/Models/HanaServer.cs b/TrafagSalesExporter/Models/HanaServer.cs index a77b146..36fd579 100644 --- a/TrafagSalesExporter/Models/HanaServer.cs +++ b/TrafagSalesExporter/Models/HanaServer.cs @@ -62,4 +62,23 @@ public class HanaServer 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(); + } } + diff --git a/TrafagSalesExporter/Models/Site.cs b/TrafagSalesExporter/Models/Site.cs index 42df2d3..cb97303 100644 --- a/TrafagSalesExporter/Models/Site.cs +++ b/TrafagSalesExporter/Models/Site.cs @@ -21,5 +21,8 @@ public class Site [Required] public string Land { get; set; } = string.Empty; + [Required] + public string SourceSystem { get; set; } = "SAP"; + public bool IsActive { get; set; } = true; } diff --git a/TrafagSalesExporter/Program.cs b/TrafagSalesExporter/Program.cs index 609affa..879d8e2 100644 --- a/TrafagSalesExporter/Program.cs +++ b/TrafagSalesExporter/Program.cs @@ -16,6 +16,7 @@ builder.Services.AddDbContextFactory(options => builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHostedService(sp => sp.GetRequiredService()); diff --git a/TrafagSalesExporter/Services/ExportOrchestrationService.cs b/TrafagSalesExporter/Services/ExportOrchestrationService.cs index ae13bd1..bd3d650 100644 --- a/TrafagSalesExporter/Services/ExportOrchestrationService.cs +++ b/TrafagSalesExporter/Services/ExportOrchestrationService.cs @@ -11,6 +11,7 @@ public class ExportOrchestrationService private readonly HanaQueryService _hanaService; private readonly ExcelExportService _excelService; private readonly SharePointUploadService _sharePointService; + private readonly RecordTransformationService _transformationService; private readonly ILogger _logger; public event Action? OnExportStatusChanged; @@ -23,12 +24,14 @@ public class ExportOrchestrationService HanaQueryService hanaService, ExcelExportService excelService, SharePointUploadService sharePointService, + RecordTransformationService transformationService, ILogger logger) { _dbFactory = dbFactory; _hanaService = hanaService; _excelService = excelService; _sharePointService = sharePointService; + _transformationService = transformationService; _logger = logger; } @@ -96,6 +99,13 @@ public class ExportOrchestrationService var records = await Task.Run(() => _hanaService.GetSalesRecords( 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..."); var outputDir = Path.Combine(AppContext.BaseDirectory, "output"); var filePath = _excelService.CreateExcelFile(outputDir, site.TSC, DateTime.UtcNow.Date, records); diff --git a/TrafagSalesExporter/Services/HanaQueryService.cs b/TrafagSalesExporter/Services/HanaQueryService.cs index 4c2a9e1..3038fe8 100644 --- a/TrafagSalesExporter/Services/HanaQueryService.cs +++ b/TrafagSalesExporter/Services/HanaQueryService.cs @@ -32,6 +32,38 @@ public class HanaQueryService 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) { 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}' 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; +} diff --git a/TrafagSalesExporter/Services/RecordTransformationService.cs b/TrafagSalesExporter/Services/RecordTransformationService.cs new file mode 100644 index 0000000..71ed4d1 --- /dev/null +++ b/TrafagSalesExporter/Services/RecordTransformationService.cs @@ -0,0 +1,92 @@ +using System.Reflection; +using TrafagSalesExporter.Models; + +namespace TrafagSalesExporter.Services; + +public class RecordTransformationService +{ + private static readonly Dictionary PropertyMap = typeof(SalesRecord) + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .ToDictionary(p => p.Name, p => p, StringComparer.OrdinalIgnoreCase); + + public void Apply(List records, IEnumerable 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 + } + } +}