Merge remote-tracking branch 'origin/main'

# Conflicts:
#	TrafagSalesExporter/TrafagSalesExporter.csproj
This commit is contained in:
2026-04-13 11:31:18 +02:00
8 changed files with 136 additions and 12 deletions
+8
View File
@@ -0,0 +1,8 @@
# Build artifacts
bin/
obj/
# Visual Studio user/IDE files
.vs/
*.user
*.suo
@@ -93,10 +93,19 @@
</TitleContent> </TitleContent>
<DialogContent> <DialogContent>
<MudTextField @bind-Value="_editingServer.Name" Label="Name" Required /> <MudTextField @bind-Value="_editingServer.Name" Label="Name" Required />
<MudTextField @bind-Value="_editingServer.Host" Label="Host" Required /> <MudTextField @bind-Value="_editingServer.Host" Label="Host" Required
<MudNumericField @bind-Value="_editingServer.Port" Label="Port" /> HelperText="IP oder Hostname (ohne Protokoll)" />
<MudNumericField @bind-Value="_editingServer.Port" Label="Port"
HelperText="Typisch 30015 (Tenant), 30013 (SystemDB), 3xx15 für Instanz xx" />
<MudTextField @bind-Value="_editingServer.Username" Label="Username" /> <MudTextField @bind-Value="_editingServer.Username" Label="Username" />
<MudTextField @bind-Value="_editingServer.Password" Label="Password" InputType="InputType.Password" /> <MudTextField @bind-Value="_editingServer.Password" Label="Password" InputType="InputType.Password" />
<MudTextField @bind-Value="_editingServer.DatabaseName" Label="Database Name (MDC)"
HelperText="Nur bei Multi-Tenant Setup angeben, sonst leer lassen" />
<MudSwitch @bind-Value="_editingServer.UseSsl" Label="SSL/TLS verwenden (encrypt=true)" Color="Color.Primary" />
<MudSwitch @bind-Value="_editingServer.ValidateCertificate" Label="SSL-Zertifikat validieren" Color="Color.Primary"
Disabled="!_editingServer.UseSsl" />
<MudTextField @bind-Value="_editingServer.AdditionalParams" Label="Zusätzliche Parameter"
HelperText="Optional, z.B. sslCryptoProvider=openssl;communicationTimeout=0" />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<MudButton OnClick="() => _serverDialogVisible = false">Abbrechen</MudButton> <MudButton OnClick="() => _serverDialogVisible = false">Abbrechen</MudButton>
@@ -164,7 +173,11 @@
Host = server.Host, Host = server.Host,
Port = server.Port, Port = server.Port,
Username = server.Username, Username = server.Username,
Password = server.Password Password = server.Password,
DatabaseName = server.DatabaseName,
UseSsl = server.UseSsl,
ValidateCertificate = server.ValidateCertificate,
AdditionalParams = server.AdditionalParams
}; };
_serverDialogVisible = true; _serverDialogVisible = true;
} }
@@ -186,6 +199,10 @@
existing.Port = _editingServer.Port; existing.Port = _editingServer.Port;
existing.Username = _editingServer.Username; existing.Username = _editingServer.Username;
existing.Password = _editingServer.Password; existing.Password = _editingServer.Password;
existing.DatabaseName = _editingServer.DatabaseName;
existing.UseSsl = _editingServer.UseSsl;
existing.ValidateCertificate = _editingServer.ValidateCertificate;
existing.AdditionalParams = _editingServer.AdditionalParams;
} }
} }
await db.SaveChangesAsync(); await db.SaveChangesAsync();
@@ -218,7 +235,7 @@
{ {
try try
{ {
await Task.Run(() => HanaService.TestConnection(server.Host, server.Port, server.Username, server.Password)); 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) catch (Exception ex)
+41
View File
@@ -1,3 +1,4 @@
using System.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using TrafagSalesExporter.Models; using TrafagSalesExporter.Models;
@@ -13,6 +14,46 @@ public class AppDbContext : DbContext
public DbSet<ExportSettings> ExportSettings => Set<ExportSettings>(); public DbSet<ExportSettings> ExportSettings => Set<ExportSettings>();
public DbSet<ExportLog> ExportLogs => Set<ExportLog>(); public DbSet<ExportLog> ExportLogs => Set<ExportLog>();
/// <summary>
/// Fügt Spalten zu existierenden Tabellen hinzu, die bei neueren Versionen
/// hinzugekommen sind. EnsureCreated aktualisiert das Schema nicht automatisch.
/// </summary>
public static void EnsureSchema(AppDbContext db)
{
AddColumnIfMissing(db, "HanaServers", "DatabaseName", "TEXT NOT NULL DEFAULT ''");
AddColumnIfMissing(db, "HanaServers", "UseSsl", "INTEGER NOT NULL DEFAULT 0");
AddColumnIfMissing(db, "HanaServers", "ValidateCertificate", "INTEGER NOT NULL DEFAULT 0");
AddColumnIfMissing(db, "HanaServers", "AdditionalParams", "TEXT NOT NULL DEFAULT ''");
}
private static void AddColumnIfMissing(AppDbContext db, string table, string column, string type)
{
var conn = db.Database.GetDbConnection();
if (conn.State != ConnectionState.Open) conn.Open();
bool exists = false;
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = $"PRAGMA table_info({table})";
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
if (string.Equals(reader["name"]?.ToString(), column, StringComparison.OrdinalIgnoreCase))
{
exists = true;
break;
}
}
}
if (!exists)
{
using var alter = conn.CreateCommand();
alter.CommandText = $"ALTER TABLE {table} ADD COLUMN {column} {type}";
alter.ExecuteNonQuery();
}
}
public static void SeedIfEmpty(AppDbContext db) public static void SeedIfEmpty(AppDbContext db)
{ {
if (db.HanaServers.Any()) return; if (db.HanaServers.Any()) return;
+45
View File
@@ -17,4 +17,49 @@ public class HanaServer
public string Username { get; set; } = string.Empty; public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty; public string Password { get; set; } = string.Empty;
/// <summary>
/// Name der Tenant-Datenbank bei Multi-Tenant Database Container (MDC) Setups.
/// Leer lassen, wenn direkt auf einen Tenant-Port verbunden wird.
/// </summary>
public string DatabaseName { get; set; } = string.Empty;
/// <summary>
/// SSL/TLS Verschlüsselung aktivieren (encrypt=true).
/// </summary>
public bool UseSsl { get; set; }
/// <summary>
/// SSL-Zertifikat validieren. Bei self-signed Zertifikaten auf false setzen.
/// </summary>
public bool ValidateCertificate { get; set; }
/// <summary>
/// Zusätzliche Verbindungsparameter (Semikolon-getrennt), z.B. "sslCryptoProvider=openssl".
/// </summary>
public string AdditionalParams { get; set; } = string.Empty;
public string BuildConnectionString()
{
var parts = new List<string>
{
$"ServerNode={Host}:{Port}",
$"UserName={Username}",
$"Password={Password}"
};
if (!string.IsNullOrWhiteSpace(DatabaseName))
parts.Add($"DatabaseName={DatabaseName}");
if (UseSsl)
{
parts.Add("encrypt=true");
parts.Add($"sslValidateCertificate={(ValidateCertificate ? "true" : "false")}");
}
if (!string.IsNullOrWhiteSpace(AdditionalParams))
parts.Add(AdditionalParams.Trim().Trim(';'));
return string.Join(";", parts);
}
} }
+1
View File
@@ -27,6 +27,7 @@ using (var scope = app.Services.CreateScope())
var dbFactory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<AppDbContext>>(); var dbFactory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<AppDbContext>>();
using var db = await dbFactory.CreateDbContextAsync(); using var db = await dbFactory.CreateDbContextAsync();
await db.Database.EnsureCreatedAsync(); await db.Database.EnsureCreatedAsync();
AppDbContext.EnsureSchema(db);
AppDbContext.SeedIfEmpty(db); AppDbContext.SeedIfEmpty(db);
} }
@@ -94,9 +94,7 @@ public class ExportOrchestrationService
UpdateStatus(site.Id, "HANA Abfrage..."); UpdateStatus(site.Id, "HANA Abfrage...");
var records = await Task.Run(() => _hanaService.GetSalesRecords( var records = await Task.Run(() => _hanaService.GetSalesRecords(
site.HanaServer.Host, site.HanaServer.Port, site.HanaServer, site.Schema, site.TSC, site.Land, settings.DateFilter));
site.HanaServer.Username, site.HanaServer.Password,
site.Schema, site.TSC, site.Land, settings.DateFilter));
UpdateStatus(site.Id, "Excel erstellen..."); UpdateStatus(site.Id, "Excel erstellen...");
var outputDir = Path.Combine(AppContext.BaseDirectory, "output"); var outputDir = Path.Combine(AppContext.BaseDirectory, "output");
@@ -5,10 +5,10 @@ namespace TrafagSalesExporter.Services;
public class HanaQueryService public class HanaQueryService
{ {
public List<SalesRecord> GetSalesRecords(string host, int port, string username, string password, public List<SalesRecord> GetSalesRecords(HanaServer server,
string schema, string tsc, string land, string dateFilter) string schema, string tsc, string land, string dateFilter)
{ {
var connectionString = $"ServerNode={host}:{port};UserName={username};Password={password}"; var connectionString = server.BuildConnectionString();
var result = new List<SalesRecord>(); var result = new List<SalesRecord>();
using var connection = new HanaConnection(connectionString); using var connection = new HanaConnection(connectionString);
@@ -32,9 +32,9 @@ public class HanaQueryService
return result; return result;
} }
public void TestConnection(string host, int port, string username, string password) public void TestConnection(HanaServer server)
{ {
var connectionString = $"ServerNode={host}:{port};UserName={username};Password={password}"; var connectionString = server.BuildConnectionString();
using var connection = new HanaConnection(connectionString); using var connection = new HanaConnection(connectionString);
connection.Open(); connection.Open();
} }
+15 -1
View File
@@ -3,6 +3,14 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<!--
Pfad zur SAP HANA Client DLL (wird mit dem SAP HANA Client installiert).
Standard-Pfad nach Installation: C:\Program Files\sap\hdbclient\dotnetcore\v2.1\
Kann bei Bedarf via MSBuild-Property überschrieben werden:
dotnet build /p:HanaClientDll="D:\pfad\zu\Sap.Data.Hana.Core.v2.1.dll"
-->
<HanaClientDll Condition="'$(HanaClientDll)' == ''">C:\Program Files\sap\hdbclient\dotnetcore\v2.1\Sap.Data.Hana.Core.v2.1.dll</HanaClientDll>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -19,7 +27,13 @@
<ItemGroup> <ItemGroup>
<Reference Include="Sap.Data.Hana.Core.v2.1"> <Reference Include="Sap.Data.Hana.Core.v2.1">
<HintPath>..\..\..\..\..\..\Program Files\sap\hdbclient\dotnetcore\v2.1\Sap.Data.Hana.Core.v2.1.dll</HintPath> <HintPath>$(HanaClientDll)</HintPath>
<Private>true</Private>
</Reference> </Reference>
</ItemGroup> </ItemGroup>
<Target Name="CheckHanaClient" BeforeTargets="ResolveAssemblyReferences">
<Warning Condition="!Exists('$(HanaClientDll)')"
Text="SAP HANA Client DLL nicht gefunden: $(HanaClientDll). Bitte SAP HANA Client installieren (https://tools.hana.ondemand.com) oder MSBuild-Property 'HanaClientDll' setzen." />
</Target>
</Project> </Project>