umfangreiches refactoring
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrafagSalesExporter.Data;
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface IDashboardPageService
|
||||
{
|
||||
Task<DashboardPageState> LoadAsync();
|
||||
}
|
||||
|
||||
public sealed class DashboardPageService : IDashboardPageService
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||
|
||||
public DashboardPageService(IDbContextFactory<AppDbContext> dbFactory)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
}
|
||||
|
||||
public async Task<DashboardPageState> LoadAsync()
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
|
||||
var sites = await db.Sites.Include(s => s.HanaServer).Where(s => s.IsActive).ToListAsync();
|
||||
var sourceSystems = await db.SourceSystemDefinitions.AsNoTracking().ToListAsync();
|
||||
var logs = await db.ExportLogs
|
||||
.GroupBy(l => l.SiteId)
|
||||
.Select(g => g.OrderByDescending(l => l.Timestamp).First())
|
||||
.ToListAsync();
|
||||
var appLogs = await db.AppEventLogs
|
||||
.Where(l => l.SiteId != null)
|
||||
.OrderByDescending(l => l.Timestamp)
|
||||
.Take(1000)
|
||||
.ToListAsync();
|
||||
var latestAppLogsBySite = appLogs
|
||||
.GroupBy(l => l.SiteId!.Value)
|
||||
.ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.Timestamp).First());
|
||||
|
||||
var rows = sites.Select(s =>
|
||||
{
|
||||
var log = logs.FirstOrDefault(l => l.SiteId == s.Id);
|
||||
latestAppLogsBySite.TryGetValue(s.Id, out var appLog);
|
||||
var sourceSystem = sourceSystems.FirstOrDefault(x => string.Equals(x.Code, s.SourceSystem, StringComparison.OrdinalIgnoreCase));
|
||||
return new DashboardRow
|
||||
{
|
||||
SiteId = s.Id,
|
||||
Land = s.Land,
|
||||
TSC = s.TSC,
|
||||
Schema = s.Schema,
|
||||
ServerName = string.Equals(sourceSystem?.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase)
|
||||
? ResolveDashboardSapServiceUrl(s, sourceSystems)
|
||||
: s.HanaServer?.Name ?? string.Empty,
|
||||
LastStatus = log?.Status ?? string.Empty,
|
||||
RowCount = log?.RowCount ?? 0,
|
||||
LastRun = log?.Timestamp,
|
||||
DurationSeconds = log?.DurationSeconds ?? 0,
|
||||
ErrorMessage = log?.ErrorMessage ?? string.Empty,
|
||||
FilePath = log?.FilePath ?? string.Empty,
|
||||
LiveMessage = appLog is null ? string.Empty : $"{appLog.Category}: {appLog.Message}",
|
||||
LiveDetails = appLog?.Details ?? string.Empty
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
return new DashboardPageState
|
||||
{
|
||||
DashboardRows = rows,
|
||||
ConsolidatedRows = BuildConsolidatedRows(await db.ExportSettings.FirstOrDefaultAsync() ?? new())
|
||||
};
|
||||
}
|
||||
|
||||
private static string ResolveDashboardSapServiceUrl(Site site, List<SourceSystemDefinition> sourceSystems)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(site.SapServiceUrl))
|
||||
return site.SapServiceUrl;
|
||||
|
||||
var sourceSystem = sourceSystems.FirstOrDefault(x => string.Equals(x.Code, site.SourceSystem, StringComparison.OrdinalIgnoreCase));
|
||||
return string.IsNullOrWhiteSpace(sourceSystem?.CentralServiceUrl) ? "SAP Gateway" : sourceSystem.CentralServiceUrl;
|
||||
}
|
||||
|
||||
private static List<ConsolidatedDashboardRow> BuildConsolidatedRows(ExportSettings settings)
|
||||
{
|
||||
var outputDirectory = ResolveConsolidatedOutputDirectory(settings);
|
||||
if (!Directory.Exists(outputDirectory))
|
||||
return [];
|
||||
|
||||
return Directory.GetFiles(outputDirectory, "Sales_All_*.xlsx")
|
||||
.Select(path => new FileInfo(path))
|
||||
.OrderByDescending(file => file.LastWriteTime)
|
||||
.Take(1)
|
||||
.Select(file => new ConsolidatedDashboardRow
|
||||
{
|
||||
Label = "Konsolidierter Export",
|
||||
FilePath = file.FullName,
|
||||
DisplayPath = file.FullName,
|
||||
LastModified = file.LastWriteTime
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string ResolveConsolidatedOutputDirectory(ExportSettings settings)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(settings.LocalConsolidatedExportFolder))
|
||||
return settings.LocalConsolidatedExportFolder.Trim();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(settings.LocalSiteExportFolder))
|
||||
return settings.LocalSiteExportFolder.Trim();
|
||||
|
||||
return Path.Combine(AppContext.BaseDirectory, "output");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DashboardPageState
|
||||
{
|
||||
public List<DashboardRow> DashboardRows { get; set; } = [];
|
||||
public List<ConsolidatedDashboardRow> ConsolidatedRows { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class DashboardRow
|
||||
{
|
||||
public int SiteId { get; set; }
|
||||
public string Land { get; set; } = string.Empty;
|
||||
public string TSC { get; set; } = string.Empty;
|
||||
public string Schema { get; set; } = string.Empty;
|
||||
public string ServerName { get; set; } = string.Empty;
|
||||
public string LastStatus { get; set; } = string.Empty;
|
||||
public int RowCount { get; set; }
|
||||
public DateTime? LastRun { get; set; }
|
||||
public double DurationSeconds { get; set; }
|
||||
public string ErrorMessage { get; set; } = string.Empty;
|
||||
public string FilePath { get; set; } = string.Empty;
|
||||
public string LiveMessage { get; set; } = string.Empty;
|
||||
public string LiveDetails { get; set; } = string.Empty;
|
||||
public bool HasOpenableFile => !string.IsNullOrWhiteSpace(FilePath) && File.Exists(FilePath);
|
||||
}
|
||||
|
||||
public sealed class ConsolidatedDashboardRow
|
||||
{
|
||||
public string Label { get; set; } = string.Empty;
|
||||
public string FilePath { get; set; } = string.Empty;
|
||||
public string DisplayPath { get; set; } = string.Empty;
|
||||
public DateTime? LastModified { get; set; }
|
||||
public bool HasOpenableFile => !string.IsNullOrWhiteSpace(FilePath) && File.Exists(FilePath);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
internal static class DatabaseSchemaSql
|
||||
{
|
||||
internal static string GetExportLogsCreateSql() => @"
|
||||
CREATE TABLE ExportLogs (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
Timestamp TEXT NOT NULL,
|
||||
SiteId INTEGER NOT NULL,
|
||||
Land TEXT NOT NULL,
|
||||
TSC TEXT NOT NULL,
|
||||
Status TEXT NOT NULL,
|
||||
RowCount INTEGER NOT NULL,
|
||||
ErrorMessage TEXT NULL,
|
||||
FileName TEXT NOT NULL DEFAULT '',
|
||||
FilePath TEXT NOT NULL DEFAULT '',
|
||||
DurationSeconds REAL NOT NULL,
|
||||
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||
);";
|
||||
|
||||
internal static string GetExportSettingsCreateSql() => @"
|
||||
CREATE TABLE ExportSettings (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
DateFilter TEXT NOT NULL,
|
||||
TimerHour INTEGER NOT NULL,
|
||||
TimerMinute INTEGER NOT NULL,
|
||||
TimerEnabled INTEGER NOT NULL,
|
||||
DebugLoggingEnabled INTEGER NOT NULL DEFAULT 0,
|
||||
LocalSiteExportFolder TEXT NOT NULL DEFAULT '',
|
||||
LocalConsolidatedExportFolder TEXT NOT NULL DEFAULT ''
|
||||
);";
|
||||
|
||||
internal static string GetHanaServersCreateSql() => @"
|
||||
CREATE TABLE HanaServers (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
SourceSystem TEXT NOT NULL,
|
||||
Name TEXT NOT NULL,
|
||||
Host TEXT NOT NULL,
|
||||
Port INTEGER NOT NULL,
|
||||
DatabaseName TEXT NOT NULL DEFAULT '',
|
||||
UseSsl INTEGER NOT NULL DEFAULT 0,
|
||||
ValidateCertificate INTEGER NOT NULL DEFAULT 0,
|
||||
AdditionalParams TEXT NOT NULL DEFAULT ''
|
||||
);";
|
||||
|
||||
internal static string GetSitesCreateSql() => @"
|
||||
CREATE TABLE Sites (
|
||||
Id INTEGER NOT NULL CONSTRAINT PK_Sites PRIMARY KEY AUTOINCREMENT,
|
||||
HanaServerId INTEGER NULL,
|
||||
Schema TEXT NOT NULL,
|
||||
TSC TEXT NOT NULL,
|
||||
Land TEXT NOT NULL,
|
||||
SourceSystem TEXT NOT NULL DEFAULT 'SAP',
|
||||
UsernameOverride TEXT NOT NULL DEFAULT '',
|
||||
PasswordOverride TEXT NOT NULL DEFAULT '',
|
||||
LocalExportFolderOverride TEXT NOT NULL DEFAULT '',
|
||||
ManualImportFilePath TEXT NOT NULL DEFAULT '',
|
||||
ManualImportLastUploadedAtUtc TEXT NULL,
|
||||
SapServiceUrl TEXT NOT NULL DEFAULT '',
|
||||
SapEntitySet TEXT NOT NULL DEFAULT '',
|
||||
SapEntitySetsCache TEXT NOT NULL DEFAULT '',
|
||||
SapEntitySetsRefreshedAtUtc TEXT NULL,
|
||||
IsActive INTEGER NOT NULL,
|
||||
CONSTRAINT FK_Sites_HanaServers_HanaServerId FOREIGN KEY (HanaServerId) REFERENCES HanaServers (Id)
|
||||
);";
|
||||
|
||||
internal static string GetAppEventLogsCreateSql() => @"
|
||||
CREATE TABLE AppEventLogs (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
Timestamp TEXT NOT NULL,
|
||||
Level TEXT NOT NULL,
|
||||
Category TEXT NOT NULL,
|
||||
SiteId INTEGER NULL,
|
||||
Land TEXT NOT NULL,
|
||||
Message TEXT NOT NULL,
|
||||
Details TEXT NOT NULL,
|
||||
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||
);";
|
||||
|
||||
internal static string GetCentralSalesRecordsCreateSql() => @"
|
||||
CREATE TABLE CentralSalesRecords (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
StoredAtUtc TEXT NOT NULL,
|
||||
SiteId INTEGER NOT NULL,
|
||||
SourceSystem TEXT NOT NULL,
|
||||
ExtractionDate TEXT NOT NULL,
|
||||
Tsc TEXT NOT NULL,
|
||||
InvoiceNumber TEXT NOT NULL,
|
||||
PositionOnInvoice INTEGER NOT NULL,
|
||||
Material TEXT NOT NULL,
|
||||
Name TEXT NOT NULL,
|
||||
ProductGroup TEXT NOT NULL,
|
||||
Quantity TEXT NOT NULL,
|
||||
SupplierNumber TEXT NOT NULL,
|
||||
SupplierName TEXT NOT NULL,
|
||||
SupplierCountry TEXT NOT NULL,
|
||||
CustomerNumber TEXT NOT NULL,
|
||||
CustomerName TEXT NOT NULL,
|
||||
CustomerCountry TEXT NOT NULL,
|
||||
CustomerIndustry TEXT NOT NULL,
|
||||
StandardCost TEXT NOT NULL,
|
||||
StandardCostCurrency TEXT NOT NULL,
|
||||
PurchaseOrderNumber TEXT NOT NULL,
|
||||
SalesPriceValue TEXT NOT NULL,
|
||||
SalesCurrency TEXT NOT NULL,
|
||||
Incoterms2020 TEXT NOT NULL,
|
||||
SalesResponsibleEmployee TEXT NOT NULL,
|
||||
InvoiceDate TEXT NULL,
|
||||
OrderDate TEXT NULL,
|
||||
Land TEXT NOT NULL,
|
||||
DocumentType TEXT NOT NULL,
|
||||
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||
);";
|
||||
|
||||
internal static string GetSapSourceDefinitionsCreateSql() => @"
|
||||
CREATE TABLE SapSourceDefinitions (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
SiteId INTEGER NOT NULL,
|
||||
Alias TEXT NOT NULL,
|
||||
EntitySet TEXT NOT NULL,
|
||||
IsPrimary INTEGER NOT NULL DEFAULT 0,
|
||||
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||
);";
|
||||
|
||||
internal static string GetSapJoinDefinitionsCreateSql() => @"
|
||||
CREATE TABLE SapJoinDefinitions (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
SiteId INTEGER NOT NULL,
|
||||
LeftAlias TEXT NOT NULL,
|
||||
RightAlias TEXT NOT NULL,
|
||||
LeftKeys TEXT NOT NULL,
|
||||
RightKeys TEXT NOT NULL,
|
||||
JoinType TEXT NOT NULL DEFAULT 'Left',
|
||||
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||
);";
|
||||
|
||||
internal static string GetSapFieldMappingsCreateSql() => @"
|
||||
CREATE TABLE SapFieldMappings (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
SiteId INTEGER NOT NULL,
|
||||
TargetField TEXT NOT NULL,
|
||||
SourceExpression TEXT NOT NULL,
|
||||
IsRequired INTEGER NOT NULL DEFAULT 0,
|
||||
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||
);";
|
||||
}
|
||||
@@ -1,17 +1,23 @@
|
||||
using System.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrafagSalesExporter.Data;
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class DatabaseInitializationService : IDatabaseInitializationService
|
||||
public partial class DatabaseInitializationService : IDatabaseInitializationService
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||
private readonly IDatabaseSchemaMaintenanceService _schemaMaintenanceService;
|
||||
private readonly IDatabaseSeedService _seedService;
|
||||
|
||||
public DatabaseInitializationService(IDbContextFactory<AppDbContext> dbFactory)
|
||||
public DatabaseInitializationService(
|
||||
IDbContextFactory<AppDbContext> dbFactory,
|
||||
IDatabaseSchemaMaintenanceService schemaMaintenanceService,
|
||||
IDatabaseSeedService seedService)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_schemaMaintenanceService = schemaMaintenanceService;
|
||||
_seedService = seedService;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
@@ -19,9 +25,8 @@ public class DatabaseInitializationService : IDatabaseInitializationService
|
||||
using var db = await _dbFactory.CreateDbContextAsync();
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
ConfigureSqlite(db);
|
||||
EnsureSchema(db);
|
||||
SeedIfEmpty(db);
|
||||
EnsureRecommendedTransformationRules(db);
|
||||
_schemaMaintenanceService.EnsureSchema(db);
|
||||
_seedService.SeedDefaults(db);
|
||||
}
|
||||
|
||||
private static void ConfigureSqlite(AppDbContext db)
|
||||
@@ -42,869 +47,4 @@ public class DatabaseInitializationService : IDatabaseInitializationService
|
||||
timeout.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureSchema(AppDbContext db)
|
||||
{
|
||||
EnsureSitesTableSupportsOptionalHanaServer(db);
|
||||
EnsureExportSettingsTableSupportsCurrentSchema(db);
|
||||
EnsureHanaServersTableSupportsCurrentSchema(db);
|
||||
RepairBrokenForeignKeys(db);
|
||||
AddColumnIfMissing(db, "HanaServers", "SourceSystem", "TEXT NOT NULL DEFAULT ''");
|
||||
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 ''");
|
||||
AddColumnIfMissing(db, "Sites", "SourceSystem", "TEXT NOT NULL DEFAULT 'SAP'");
|
||||
AddColumnIfMissing(db, "Sites", "UsernameOverride", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "PasswordOverride", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "LocalExportFolderOverride", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "ManualImportFilePath", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "ManualImportLastUploadedAtUtc", "TEXT NULL");
|
||||
AddColumnIfMissing(db, "Sites", "SapServiceUrl", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "SapEntitySet", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "SapEntitySetsCache", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "SapEntitySetsRefreshedAtUtc", "TEXT NULL");
|
||||
AddColumnIfMissing(db, "ExportSettings", "DebugLoggingEnabled", "INTEGER NOT NULL DEFAULT 0");
|
||||
AddColumnIfMissing(db, "ExportSettings", "LocalSiteExportFolder", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "ExportSettings", "LocalConsolidatedExportFolder", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "SharePointConfigs", "CentralExportFolder", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "ExportLogs", "FilePath", "TEXT NOT NULL DEFAULT ''");
|
||||
EnsureTransformationTable(db);
|
||||
AddColumnIfMissing(db, "FieldTransformationRules", "RuleScope", "TEXT NOT NULL DEFAULT 'Value'");
|
||||
EnsureCurrencyExchangeRateTable(db);
|
||||
EnsureSourceSystemDefinitionTable(db);
|
||||
AddColumnIfMissing(db, "SourceSystemDefinitions", "CentralServiceUrl", "TEXT NOT NULL DEFAULT ''");
|
||||
EnsureSapSourceTable(db);
|
||||
EnsureSapJoinTable(db);
|
||||
EnsureSapFieldMappingTable(db);
|
||||
EnsureCentralSalesRecordTable(db);
|
||||
EnsureAppEventLogTable(db);
|
||||
EnsureSourceSystemDefinitions(db);
|
||||
EnsureCentralHanaServerRecords(db);
|
||||
}
|
||||
|
||||
private static void EnsureExportSettingsTableSupportsCurrentSchema(AppDbContext db)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
var columns = GetTableColumns(conn, transaction: null, "ExportSettings");
|
||||
if (columns.Count == 0)
|
||||
return;
|
||||
|
||||
var legacyColumns = new[]
|
||||
{
|
||||
"SapUsername",
|
||||
"SapPassword",
|
||||
"Bi1Username",
|
||||
"Bi1Password",
|
||||
"SageUsername",
|
||||
"SagePassword"
|
||||
};
|
||||
|
||||
if (!legacyColumns.Any(columns.Contains))
|
||||
return;
|
||||
|
||||
RebuildTable(conn, "ExportSettings", GetExportSettingsCreateSql());
|
||||
}
|
||||
|
||||
private static void EnsureHanaServersTableSupportsCurrentSchema(AppDbContext db)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
var columns = GetTableColumns(conn, transaction: null, "HanaServers");
|
||||
if (columns.Count == 0)
|
||||
return;
|
||||
|
||||
if (!columns.Contains("Username") && !columns.Contains("Password"))
|
||||
return;
|
||||
|
||||
RebuildTable(conn, "HanaServers", GetHanaServersCreateSql());
|
||||
}
|
||||
|
||||
private static void EnsureSitesTableSupportsOptionalHanaServer(AppDbContext db)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
var hanaServerIdIsRequired = false;
|
||||
{
|
||||
using var pragma = conn.CreateCommand();
|
||||
pragma.CommandText = "PRAGMA table_info(Sites)";
|
||||
using var reader = pragma.ExecuteReader();
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
if (string.Equals(reader["name"]?.ToString(), "HanaServerId", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
hanaServerIdIsRequired = Convert.ToInt32(reader["notnull"]) == 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hanaServerIdIsRequired)
|
||||
return;
|
||||
|
||||
using var disableFk = conn.CreateCommand();
|
||||
disableFk.CommandText = "PRAGMA foreign_keys = OFF;";
|
||||
disableFk.ExecuteNonQuery();
|
||||
|
||||
using var transaction = conn.BeginTransaction();
|
||||
|
||||
using (var rename = conn.CreateCommand())
|
||||
{
|
||||
rename.Transaction = transaction;
|
||||
rename.CommandText = "ALTER TABLE Sites RENAME TO Sites_old;";
|
||||
rename.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
using (var create = conn.CreateCommand())
|
||||
{
|
||||
create.Transaction = transaction;
|
||||
create.CommandText = GetSitesCreateSql();
|
||||
create.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
using (var copy = conn.CreateCommand())
|
||||
{
|
||||
copy.Transaction = transaction;
|
||||
copy.CommandText = @"
|
||||
INSERT INTO Sites (
|
||||
Id, HanaServerId, Schema, TSC, Land, SourceSystem,
|
||||
UsernameOverride, PasswordOverride, LocalExportFolderOverride, ManualImportFilePath,
|
||||
ManualImportLastUploadedAtUtc, SapServiceUrl, SapEntitySet, SapEntitySetsCache,
|
||||
SapEntitySetsRefreshedAtUtc, IsActive
|
||||
)
|
||||
SELECT
|
||||
Id, HanaServerId, Schema, TSC, Land,
|
||||
COALESCE(SourceSystem, 'SAP'),
|
||||
COALESCE(UsernameOverride, ''),
|
||||
COALESCE(PasswordOverride, ''),
|
||||
COALESCE(LocalExportFolderOverride, ''),
|
||||
COALESCE(ManualImportFilePath, ''),
|
||||
ManualImportLastUploadedAtUtc,
|
||||
COALESCE(SapServiceUrl, ''),
|
||||
COALESCE(SapEntitySet, ''),
|
||||
COALESCE(SapEntitySetsCache, ''),
|
||||
SapEntitySetsRefreshedAtUtc,
|
||||
IsActive
|
||||
FROM Sites_old;";
|
||||
copy.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
using (var drop = conn.CreateCommand())
|
||||
{
|
||||
drop.Transaction = transaction;
|
||||
drop.CommandText = "DROP TABLE Sites_old;";
|
||||
drop.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
|
||||
using var enableFk = conn.CreateCommand();
|
||||
enableFk.CommandText = "PRAGMA foreign_keys = ON;";
|
||||
enableFk.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void RepairBrokenForeignKeys(AppDbContext db)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
var siteDependentTables = new[]
|
||||
{
|
||||
("ExportLogs", GetExportLogsCreateSql()),
|
||||
("AppEventLogs", GetAppEventLogsCreateSql()),
|
||||
("CentralSalesRecords", GetCentralSalesRecordsCreateSql()),
|
||||
("SapSourceDefinitions", GetSapSourceDefinitionsCreateSql()),
|
||||
("SapJoinDefinitions", GetSapJoinDefinitionsCreateSql()),
|
||||
("SapFieldMappings", GetSapFieldMappingsCreateSql())
|
||||
};
|
||||
|
||||
foreach (var (tableName, createSql) in siteDependentTables)
|
||||
{
|
||||
if (TableReferences(conn, tableName, "Sites_old"))
|
||||
RebuildTable(conn, tableName, createSql);
|
||||
}
|
||||
|
||||
if (TableReferences(conn, "Sites", "HanaServers_repair_old"))
|
||||
RebuildTable(conn, "Sites", GetSitesCreateSql());
|
||||
}
|
||||
|
||||
private static bool TableReferences(System.Data.Common.DbConnection connection, string tableName, string referencedTableName)
|
||||
{
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = $tableName;";
|
||||
|
||||
var parameter = command.CreateParameter();
|
||||
parameter.ParameterName = "$tableName";
|
||||
parameter.Value = tableName;
|
||||
command.Parameters.Add(parameter);
|
||||
|
||||
var sql = command.ExecuteScalar()?.ToString() ?? string.Empty;
|
||||
return sql.Contains(referencedTableName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static void RebuildTable(System.Data.Common.DbConnection connection, string tableName, string createSql)
|
||||
{
|
||||
using var disableFk = connection.CreateCommand();
|
||||
disableFk.CommandText = "PRAGMA foreign_keys = OFF;";
|
||||
disableFk.ExecuteNonQuery();
|
||||
|
||||
using var transaction = connection.BeginTransaction();
|
||||
|
||||
var tempTableName = $"{tableName}_repair_old";
|
||||
|
||||
using (var rename = connection.CreateCommand())
|
||||
{
|
||||
rename.Transaction = transaction;
|
||||
rename.CommandText = $"ALTER TABLE {tableName} RENAME TO {tempTableName};";
|
||||
rename.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
using (var create = connection.CreateCommand())
|
||||
{
|
||||
create.Transaction = transaction;
|
||||
create.CommandText = createSql;
|
||||
create.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
var columns = GetSharedColumns(connection, transaction, tableName, tempTableName);
|
||||
if (columns.Count > 0)
|
||||
{
|
||||
var columnList = string.Join(", ", columns);
|
||||
|
||||
using var copy = connection.CreateCommand();
|
||||
copy.Transaction = transaction;
|
||||
copy.CommandText = $"INSERT INTO {tableName} ({columnList}) SELECT {columnList} FROM {tempTableName};";
|
||||
copy.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
using (var drop = connection.CreateCommand())
|
||||
{
|
||||
drop.Transaction = transaction;
|
||||
drop.CommandText = $"DROP TABLE {tempTableName};";
|
||||
drop.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
|
||||
using var enableFk = connection.CreateCommand();
|
||||
enableFk.CommandText = "PRAGMA foreign_keys = ON;";
|
||||
enableFk.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static List<string> GetSharedColumns(System.Data.Common.DbConnection connection, System.Data.Common.DbTransaction? transaction, string newTableName, string oldTableName)
|
||||
{
|
||||
var newColumns = GetTableColumns(connection, transaction, newTableName);
|
||||
var oldColumns = GetTableColumns(connection, transaction, oldTableName);
|
||||
|
||||
return newColumns.Where(oldColumns.Contains).ToList();
|
||||
}
|
||||
|
||||
private static HashSet<string> GetTableColumns(System.Data.Common.DbConnection connection, System.Data.Common.DbTransaction? transaction, string tableName)
|
||||
{
|
||||
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
using var command = connection.CreateCommand();
|
||||
command.Transaction = transaction;
|
||||
command.CommandText = $"PRAGMA table_info({tableName})";
|
||||
|
||||
using var reader = command.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
var name = reader["name"]?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
columns.Add(name);
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
private static string GetExportLogsCreateSql() => @"
|
||||
CREATE TABLE ExportLogs (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
Timestamp TEXT NOT NULL,
|
||||
SiteId INTEGER NOT NULL,
|
||||
Land TEXT NOT NULL,
|
||||
TSC TEXT NOT NULL,
|
||||
Status TEXT NOT NULL,
|
||||
RowCount INTEGER NOT NULL,
|
||||
ErrorMessage TEXT NULL,
|
||||
FileName TEXT NOT NULL DEFAULT '',
|
||||
FilePath TEXT NOT NULL DEFAULT '',
|
||||
DurationSeconds REAL NOT NULL,
|
||||
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||
);";
|
||||
|
||||
private static string GetExportSettingsCreateSql() => @"
|
||||
CREATE TABLE ExportSettings (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
DateFilter TEXT NOT NULL,
|
||||
TimerHour INTEGER NOT NULL,
|
||||
TimerMinute INTEGER NOT NULL,
|
||||
TimerEnabled INTEGER NOT NULL,
|
||||
DebugLoggingEnabled INTEGER NOT NULL DEFAULT 0,
|
||||
LocalSiteExportFolder TEXT NOT NULL DEFAULT '',
|
||||
LocalConsolidatedExportFolder TEXT NOT NULL DEFAULT ''
|
||||
);";
|
||||
|
||||
private static string GetHanaServersCreateSql() => @"
|
||||
CREATE TABLE HanaServers (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
SourceSystem TEXT NOT NULL,
|
||||
Name TEXT NOT NULL,
|
||||
Host TEXT NOT NULL,
|
||||
Port INTEGER NOT NULL,
|
||||
DatabaseName TEXT NOT NULL DEFAULT '',
|
||||
UseSsl INTEGER NOT NULL DEFAULT 0,
|
||||
ValidateCertificate INTEGER NOT NULL DEFAULT 0,
|
||||
AdditionalParams TEXT NOT NULL DEFAULT ''
|
||||
);";
|
||||
|
||||
private static string GetSitesCreateSql() => @"
|
||||
CREATE TABLE Sites (
|
||||
Id INTEGER NOT NULL CONSTRAINT PK_Sites PRIMARY KEY AUTOINCREMENT,
|
||||
HanaServerId INTEGER NULL,
|
||||
Schema TEXT NOT NULL,
|
||||
TSC TEXT NOT NULL,
|
||||
Land TEXT NOT NULL,
|
||||
SourceSystem TEXT NOT NULL DEFAULT 'SAP',
|
||||
UsernameOverride TEXT NOT NULL DEFAULT '',
|
||||
PasswordOverride TEXT NOT NULL DEFAULT '',
|
||||
LocalExportFolderOverride TEXT NOT NULL DEFAULT '',
|
||||
ManualImportFilePath TEXT NOT NULL DEFAULT '',
|
||||
ManualImportLastUploadedAtUtc TEXT NULL,
|
||||
SapServiceUrl TEXT NOT NULL DEFAULT '',
|
||||
SapEntitySet TEXT NOT NULL DEFAULT '',
|
||||
SapEntitySetsCache TEXT NOT NULL DEFAULT '',
|
||||
SapEntitySetsRefreshedAtUtc TEXT NULL,
|
||||
IsActive INTEGER NOT NULL,
|
||||
CONSTRAINT FK_Sites_HanaServers_HanaServerId FOREIGN KEY (HanaServerId) REFERENCES HanaServers (Id)
|
||||
);";
|
||||
|
||||
private static string GetAppEventLogsCreateSql() => @"
|
||||
CREATE TABLE AppEventLogs (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
Timestamp TEXT NOT NULL,
|
||||
Level TEXT NOT NULL,
|
||||
Category TEXT NOT NULL,
|
||||
SiteId INTEGER NULL,
|
||||
Land TEXT NOT NULL,
|
||||
Message TEXT NOT NULL,
|
||||
Details TEXT NOT NULL,
|
||||
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||
);";
|
||||
|
||||
private static string GetCentralSalesRecordsCreateSql() => @"
|
||||
CREATE TABLE CentralSalesRecords (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
StoredAtUtc TEXT NOT NULL,
|
||||
SiteId INTEGER NOT NULL,
|
||||
SourceSystem TEXT NOT NULL,
|
||||
ExtractionDate TEXT NOT NULL,
|
||||
Tsc TEXT NOT NULL,
|
||||
InvoiceNumber TEXT NOT NULL,
|
||||
PositionOnInvoice INTEGER NOT NULL,
|
||||
Material TEXT NOT NULL,
|
||||
Name TEXT NOT NULL,
|
||||
ProductGroup TEXT NOT NULL,
|
||||
Quantity TEXT NOT NULL,
|
||||
SupplierNumber TEXT NOT NULL,
|
||||
SupplierName TEXT NOT NULL,
|
||||
SupplierCountry TEXT NOT NULL,
|
||||
CustomerNumber TEXT NOT NULL,
|
||||
CustomerName TEXT NOT NULL,
|
||||
CustomerCountry TEXT NOT NULL,
|
||||
CustomerIndustry TEXT NOT NULL,
|
||||
StandardCost TEXT NOT NULL,
|
||||
StandardCostCurrency TEXT NOT NULL,
|
||||
PurchaseOrderNumber TEXT NOT NULL,
|
||||
SalesPriceValue TEXT NOT NULL,
|
||||
SalesCurrency TEXT NOT NULL,
|
||||
Incoterms2020 TEXT NOT NULL,
|
||||
SalesResponsibleEmployee TEXT NOT NULL,
|
||||
InvoiceDate TEXT NULL,
|
||||
OrderDate TEXT NULL,
|
||||
Land TEXT NOT NULL,
|
||||
DocumentType TEXT NOT NULL,
|
||||
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||
);";
|
||||
|
||||
private static string GetSapSourceDefinitionsCreateSql() => @"
|
||||
CREATE TABLE SapSourceDefinitions (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
SiteId INTEGER NOT NULL,
|
||||
Alias TEXT NOT NULL,
|
||||
EntitySet TEXT NOT NULL,
|
||||
IsPrimary INTEGER NOT NULL DEFAULT 0,
|
||||
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||
);";
|
||||
|
||||
private static string GetSapJoinDefinitionsCreateSql() => @"
|
||||
CREATE TABLE SapJoinDefinitions (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
SiteId INTEGER NOT NULL,
|
||||
LeftAlias TEXT NOT NULL,
|
||||
RightAlias TEXT NOT NULL,
|
||||
LeftKeys TEXT NOT NULL,
|
||||
RightKeys TEXT NOT NULL,
|
||||
JoinType TEXT NOT NULL DEFAULT 'Left',
|
||||
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||
);";
|
||||
|
||||
private static string GetSapFieldMappingsCreateSql() => @"
|
||||
CREATE TABLE SapFieldMappings (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
SiteId INTEGER NOT NULL,
|
||||
TargetField TEXT NOT NULL,
|
||||
SourceExpression TEXT NOT NULL,
|
||||
IsRequired INTEGER NOT NULL DEFAULT 0,
|
||||
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||
);";
|
||||
|
||||
private static void AddColumnIfMissing(AppDbContext db, string table, string column, string type)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
var 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();
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
RuleScope TEXT NOT NULL DEFAULT 'Value',
|
||||
Argument TEXT NOT NULL DEFAULT '',
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||
IsActive INTEGER NOT NULL DEFAULT 1
|
||||
);";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void EnsureSapSourceTable(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 SapSourceDefinitions (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
SiteId INTEGER NOT NULL,
|
||||
Alias TEXT NOT NULL,
|
||||
EntitySet TEXT NOT NULL,
|
||||
IsPrimary INTEGER NOT NULL DEFAULT 0,
|
||||
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||
);";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void EnsureCurrencyExchangeRateTable(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 CurrencyExchangeRates (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
FromCurrency TEXT NOT NULL,
|
||||
ToCurrency TEXT NOT NULL,
|
||||
Rate REAL NOT NULL,
|
||||
ValidFrom TEXT NOT NULL,
|
||||
ValidTo TEXT NULL,
|
||||
Notes TEXT NOT NULL DEFAULT '',
|
||||
IsActive INTEGER NOT NULL DEFAULT 1
|
||||
);";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void EnsureSapJoinTable(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 SapJoinDefinitions (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
SiteId INTEGER NOT NULL,
|
||||
LeftAlias TEXT NOT NULL,
|
||||
RightAlias TEXT NOT NULL,
|
||||
LeftKeys TEXT NOT NULL,
|
||||
RightKeys TEXT NOT NULL,
|
||||
JoinType TEXT NOT NULL DEFAULT 'Left',
|
||||
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||
);";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void EnsureSapFieldMappingTable(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 SapFieldMappings (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
SiteId INTEGER NOT NULL,
|
||||
TargetField TEXT NOT NULL,
|
||||
SourceExpression TEXT NOT NULL,
|
||||
IsRequired INTEGER NOT NULL DEFAULT 0,
|
||||
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||
);";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void EnsureCentralSalesRecordTable(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 CentralSalesRecords (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
StoredAtUtc TEXT NOT NULL,
|
||||
SiteId INTEGER NOT NULL,
|
||||
SourceSystem TEXT NOT NULL,
|
||||
ExtractionDate TEXT NOT NULL,
|
||||
Tsc TEXT NOT NULL,
|
||||
InvoiceNumber TEXT NOT NULL,
|
||||
PositionOnInvoice INTEGER NOT NULL,
|
||||
Material TEXT NOT NULL,
|
||||
Name TEXT NOT NULL,
|
||||
ProductGroup TEXT NOT NULL,
|
||||
Quantity TEXT NOT NULL,
|
||||
SupplierNumber TEXT NOT NULL,
|
||||
SupplierName TEXT NOT NULL,
|
||||
SupplierCountry TEXT NOT NULL,
|
||||
CustomerNumber TEXT NOT NULL,
|
||||
CustomerName TEXT NOT NULL,
|
||||
CustomerCountry TEXT NOT NULL,
|
||||
CustomerIndustry TEXT NOT NULL,
|
||||
StandardCost TEXT NOT NULL,
|
||||
StandardCostCurrency TEXT NOT NULL,
|
||||
PurchaseOrderNumber TEXT NOT NULL,
|
||||
SalesPriceValue TEXT NOT NULL,
|
||||
SalesCurrency TEXT NOT NULL,
|
||||
Incoterms2020 TEXT NOT NULL,
|
||||
SalesResponsibleEmployee TEXT NOT NULL,
|
||||
InvoiceDate TEXT NULL,
|
||||
OrderDate TEXT NULL,
|
||||
Land TEXT NOT NULL,
|
||||
DocumentType TEXT NOT NULL,
|
||||
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||
);";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void EnsureAppEventLogTable(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 AppEventLogs (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
Timestamp TEXT NOT NULL,
|
||||
Level TEXT NOT NULL,
|
||||
Category TEXT NOT NULL,
|
||||
SiteId INTEGER NULL,
|
||||
Land TEXT NOT NULL,
|
||||
Message TEXT NOT NULL,
|
||||
Details TEXT NOT NULL,
|
||||
FOREIGN KEY (SiteId) REFERENCES Sites (Id)
|
||||
);";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void EnsureSourceSystemDefinitionTable(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 SourceSystemDefinitions (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
Code TEXT NOT NULL,
|
||||
DisplayName TEXT NOT NULL,
|
||||
ConnectionKind TEXT NOT NULL,
|
||||
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||
CentralServiceUrl TEXT NOT NULL DEFAULT '',
|
||||
CentralUsername TEXT NOT NULL DEFAULT '',
|
||||
CentralPassword TEXT NOT NULL DEFAULT ''
|
||||
);";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void SeedIfEmpty(AppDbContext db)
|
||||
{
|
||||
if (db.Sites.Any() || db.HanaServers.Any() || db.SharePointConfigs.Any() || db.ExportSettings.Any())
|
||||
return;
|
||||
|
||||
var serverBi1 = new HanaServer { SourceSystem = "BI1", Name = "BI1", Host = "travtrp0", Port = 30015, Username = "", Password = "" };
|
||||
var serverSage = new HanaServer { SourceSystem = "SAGE", Name = "SAGE", Host = "20.197.20.60", Port = 30015, Username = "", Password = "" };
|
||||
db.HanaServers.AddRange(serverBi1, serverSage);
|
||||
db.SaveChanges();
|
||||
|
||||
db.Sites.AddRange(
|
||||
new Site { HanaServerId = serverBi1.Id, Schema = "fr01_p", TSC = "TRFR", Land = "Frankreich", SourceSystem = "BI1", IsActive = true },
|
||||
new Site { HanaServerId = serverBi1.Id, Schema = "it01_p", TSC = "TRIT", Land = "Italien", SourceSystem = "BI1", IsActive = true },
|
||||
new Site { HanaServerId = serverBi1.Id, Schema = "us01_p", TSC = "TRUS", Land = "USA", SourceSystem = "BI1", IsActive = true },
|
||||
new Site { HanaServerId = serverSage.Id, Schema = "TRAFAG_LIVE", TSC = "TRIN", Land = "Indien", SourceSystem = "SAGE", IsActive = true }
|
||||
);
|
||||
|
||||
db.SharePointConfigs.Add(new SharePointConfig
|
||||
{
|
||||
SiteUrl = "https://trafagag.sharepoint.com/sites/WorldwideBIPlatform",
|
||||
ExportFolder = "/Shared Documents/Exports/",
|
||||
CentralExportFolder = "",
|
||||
TenantId = "",
|
||||
ClientId = "",
|
||||
ClientSecret = ""
|
||||
});
|
||||
|
||||
db.ExportSettings.Add(new ExportSettings
|
||||
{
|
||||
DateFilter = "2025-01-01",
|
||||
TimerHour = 3,
|
||||
TimerMinute = 0,
|
||||
TimerEnabled = true,
|
||||
DebugLoggingEnabled = false,
|
||||
LocalSiteExportFolder = "",
|
||||
LocalConsolidatedExportFolder = ""
|
||||
});
|
||||
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
private static void EnsureRecommendedTransformationRules(AppDbContext db)
|
||||
{
|
||||
var recommendedRules = new[]
|
||||
{
|
||||
new FieldTransformationRule
|
||||
{
|
||||
SourceSystem = "MANUAL_EXCEL",
|
||||
SourceField = nameof(SalesRecord.SalesCurrency),
|
||||
TargetField = nameof(SalesRecord.SalesCurrency),
|
||||
TransformationType = "Replace",
|
||||
RuleScope = "Value",
|
||||
Argument = "$=>USD",
|
||||
SortOrder = 100,
|
||||
IsActive = true
|
||||
},
|
||||
new FieldTransformationRule
|
||||
{
|
||||
SourceSystem = "MANUAL_EXCEL",
|
||||
SourceField = nameof(SalesRecord.StandardCostCurrency),
|
||||
TargetField = nameof(SalesRecord.StandardCostCurrency),
|
||||
TransformationType = "Replace",
|
||||
RuleScope = "Value",
|
||||
Argument = "$=>USD",
|
||||
SortOrder = 110,
|
||||
IsActive = true
|
||||
}
|
||||
};
|
||||
|
||||
var hasChanges = false;
|
||||
|
||||
foreach (var rule in recommendedRules)
|
||||
{
|
||||
var exists = db.FieldTransformationRules.Any(existing =>
|
||||
existing.SourceSystem == rule.SourceSystem &&
|
||||
existing.RuleScope == rule.RuleScope &&
|
||||
existing.SourceField == rule.SourceField &&
|
||||
existing.TargetField == rule.TargetField &&
|
||||
existing.TransformationType == rule.TransformationType &&
|
||||
existing.Argument == rule.Argument);
|
||||
|
||||
if (exists)
|
||||
continue;
|
||||
|
||||
db.FieldTransformationRules.Add(rule);
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (hasChanges)
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
private static void EnsureCentralHanaServerRecords(AppDbContext db)
|
||||
{
|
||||
var centralSystems = db.SourceSystemDefinitions
|
||||
.AsNoTracking()
|
||||
.Where(x => x.ConnectionKind == SourceSystemConnectionKinds.Hana)
|
||||
.OrderBy(x => x.Code)
|
||||
.Select(x => x.Code)
|
||||
.ToList();
|
||||
var changed = false;
|
||||
|
||||
foreach (var sourceSystem in centralSystems)
|
||||
{
|
||||
var existingCentral = db.HanaServers
|
||||
.OrderBy(x => x.Id)
|
||||
.FirstOrDefault(x => x.SourceSystem == sourceSystem);
|
||||
|
||||
if (existingCentral is not null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(existingCentral.Name))
|
||||
{
|
||||
existingCentral.Name = sourceSystem;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var linkedServer = db.Sites
|
||||
.Include(x => x.HanaServer)
|
||||
.Where(x => x.SourceSystem == sourceSystem && x.HanaServerId != null && x.HanaServer != null)
|
||||
.Select(x => x.HanaServer!)
|
||||
.OrderBy(x => x.Id)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (linkedServer is not null)
|
||||
{
|
||||
linkedServer.SourceSystem = sourceSystem;
|
||||
if (string.IsNullOrWhiteSpace(linkedServer.Name))
|
||||
linkedServer.Name = sourceSystem;
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
db.HanaServers.Add(new HanaServer
|
||||
{
|
||||
SourceSystem = sourceSystem,
|
||||
Name = sourceSystem,
|
||||
Host = string.Empty,
|
||||
Port = 30015,
|
||||
Username = string.Empty,
|
||||
Password = string.Empty,
|
||||
DatabaseName = string.Empty,
|
||||
AdditionalParams = string.Empty
|
||||
});
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
private static void EnsureSourceSystemDefinitions(AppDbContext db)
|
||||
{
|
||||
var defaults = new[]
|
||||
{
|
||||
new SourceSystemDefinition { Code = "SAP", DisplayName = "SAP", ConnectionKind = SourceSystemConnectionKinds.SapGateway, IsActive = true },
|
||||
new SourceSystemDefinition { Code = "BI1", DisplayName = "BI1", ConnectionKind = SourceSystemConnectionKinds.Hana, IsActive = true },
|
||||
new SourceSystemDefinition { Code = "SAGE", DisplayName = "SAGE", ConnectionKind = SourceSystemConnectionKinds.Hana, IsActive = true },
|
||||
new SourceSystemDefinition { Code = "MANUAL_EXCEL", DisplayName = "Manual Excel", ConnectionKind = SourceSystemConnectionKinds.ManualExcel, IsActive = true }
|
||||
};
|
||||
|
||||
var existing = db.SourceSystemDefinitions.ToList();
|
||||
var changed = false;
|
||||
|
||||
foreach (var item in defaults)
|
||||
{
|
||||
var current = existing.FirstOrDefault(x => x.Code == item.Code);
|
||||
if (current is null)
|
||||
{
|
||||
db.SourceSystemDefinitions.Add(item);
|
||||
existing.Add(item);
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(current.DisplayName))
|
||||
{
|
||||
current.DisplayName = item.DisplayName;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(current.ConnectionKind))
|
||||
{
|
||||
current.ConnectionKind = item.ConnectionKind;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(current.CentralServiceUrl) &&
|
||||
string.Equals(current.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var sapSite = db.Sites
|
||||
.Where(x => x.SourceSystem == current.Code && !string.IsNullOrWhiteSpace(x.SapServiceUrl))
|
||||
.OrderBy(x => x.Id)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (sapSite is not null)
|
||||
{
|
||||
current.CentralServiceUrl = sapSite.SapServiceUrl;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
db.SaveChanges();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrafagSalesExporter.Data;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class DatabaseSchemaMaintenanceService : IDatabaseSchemaMaintenanceService
|
||||
{
|
||||
public void EnsureSchema(AppDbContext db)
|
||||
{
|
||||
EnsureSitesTableSupportsOptionalHanaServer(db);
|
||||
EnsureExportSettingsTableSupportsCurrentSchema(db);
|
||||
EnsureHanaServersTableSupportsCurrentSchema(db);
|
||||
RepairBrokenForeignKeys(db);
|
||||
AddColumnIfMissing(db, "HanaServers", "SourceSystem", "TEXT NOT NULL DEFAULT ''");
|
||||
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 ''");
|
||||
AddColumnIfMissing(db, "Sites", "SourceSystem", "TEXT NOT NULL DEFAULT 'SAP'");
|
||||
AddColumnIfMissing(db, "Sites", "UsernameOverride", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "PasswordOverride", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "LocalExportFolderOverride", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "ManualImportFilePath", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "ManualImportLastUploadedAtUtc", "TEXT NULL");
|
||||
AddColumnIfMissing(db, "Sites", "SapServiceUrl", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "SapEntitySet", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "SapEntitySetsCache", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "Sites", "SapEntitySetsRefreshedAtUtc", "TEXT NULL");
|
||||
AddColumnIfMissing(db, "ExportSettings", "DebugLoggingEnabled", "INTEGER NOT NULL DEFAULT 0");
|
||||
AddColumnIfMissing(db, "ExportSettings", "LocalSiteExportFolder", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "ExportSettings", "LocalConsolidatedExportFolder", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "SharePointConfigs", "CentralExportFolder", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "ExportLogs", "FilePath", "TEXT NOT NULL DEFAULT ''");
|
||||
EnsureTransformationTable(db);
|
||||
AddColumnIfMissing(db, "FieldTransformationRules", "RuleScope", "TEXT NOT NULL DEFAULT 'Value'");
|
||||
EnsureCurrencyExchangeRateTable(db);
|
||||
EnsureSourceSystemDefinitionTable(db);
|
||||
AddColumnIfMissing(db, "SourceSystemDefinitions", "CentralServiceUrl", "TEXT NOT NULL DEFAULT ''");
|
||||
EnsureSapSourceTable(db);
|
||||
EnsureSapJoinTable(db);
|
||||
EnsureSapFieldMappingTable(db);
|
||||
EnsureCentralSalesRecordTable(db);
|
||||
EnsureAppEventLogTable(db);
|
||||
}
|
||||
|
||||
private static void EnsureExportSettingsTableSupportsCurrentSchema(AppDbContext db)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != System.Data.ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
var columns = DatabaseSchemaTools.GetTableColumns(conn, transaction: null, "ExportSettings");
|
||||
if (columns.Count == 0)
|
||||
return;
|
||||
|
||||
var legacyColumns = new[]
|
||||
{
|
||||
"SapUsername",
|
||||
"SapPassword",
|
||||
"Bi1Username",
|
||||
"Bi1Password",
|
||||
"SageUsername",
|
||||
"SagePassword"
|
||||
};
|
||||
|
||||
if (!legacyColumns.Any(columns.Contains))
|
||||
return;
|
||||
|
||||
DatabaseSchemaTools.RebuildTable(conn, "ExportSettings", DatabaseSchemaSql.GetExportSettingsCreateSql());
|
||||
}
|
||||
|
||||
private static void EnsureHanaServersTableSupportsCurrentSchema(AppDbContext db)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != System.Data.ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
var columns = DatabaseSchemaTools.GetTableColumns(conn, transaction: null, "HanaServers");
|
||||
if (columns.Count == 0)
|
||||
return;
|
||||
|
||||
if (!columns.Contains("Username") && !columns.Contains("Password"))
|
||||
return;
|
||||
|
||||
DatabaseSchemaTools.RebuildTable(conn, "HanaServers", DatabaseSchemaSql.GetHanaServersCreateSql());
|
||||
}
|
||||
|
||||
private static void EnsureSitesTableSupportsOptionalHanaServer(AppDbContext db)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != System.Data.ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
var hanaServerIdIsRequired = false;
|
||||
{
|
||||
using var pragma = conn.CreateCommand();
|
||||
pragma.CommandText = "PRAGMA table_info(Sites)";
|
||||
using var reader = pragma.ExecuteReader();
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
if (string.Equals(reader["name"]?.ToString(), "HanaServerId", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
hanaServerIdIsRequired = Convert.ToInt32(reader["notnull"]) == 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hanaServerIdIsRequired)
|
||||
return;
|
||||
|
||||
using var disableFk = conn.CreateCommand();
|
||||
disableFk.CommandText = "PRAGMA foreign_keys = OFF;";
|
||||
disableFk.ExecuteNonQuery();
|
||||
|
||||
using var transaction = conn.BeginTransaction();
|
||||
|
||||
using (var rename = conn.CreateCommand())
|
||||
{
|
||||
rename.Transaction = transaction;
|
||||
rename.CommandText = "ALTER TABLE Sites RENAME TO Sites_old;";
|
||||
rename.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
using (var create = conn.CreateCommand())
|
||||
{
|
||||
create.Transaction = transaction;
|
||||
create.CommandText = DatabaseSchemaSql.GetSitesCreateSql();
|
||||
create.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
using (var copy = conn.CreateCommand())
|
||||
{
|
||||
copy.Transaction = transaction;
|
||||
copy.CommandText = @"
|
||||
INSERT INTO Sites (
|
||||
Id, HanaServerId, Schema, TSC, Land, SourceSystem,
|
||||
UsernameOverride, PasswordOverride, LocalExportFolderOverride, ManualImportFilePath,
|
||||
ManualImportLastUploadedAtUtc, SapServiceUrl, SapEntitySet, SapEntitySetsCache,
|
||||
SapEntitySetsRefreshedAtUtc, IsActive
|
||||
)
|
||||
SELECT
|
||||
Id, HanaServerId, Schema, TSC, Land,
|
||||
COALESCE(SourceSystem, 'SAP'),
|
||||
COALESCE(UsernameOverride, ''),
|
||||
COALESCE(PasswordOverride, ''),
|
||||
COALESCE(LocalExportFolderOverride, ''),
|
||||
COALESCE(ManualImportFilePath, ''),
|
||||
ManualImportLastUploadedAtUtc,
|
||||
COALESCE(SapServiceUrl, ''),
|
||||
COALESCE(SapEntitySet, ''),
|
||||
COALESCE(SapEntitySetsCache, ''),
|
||||
SapEntitySetsRefreshedAtUtc,
|
||||
IsActive
|
||||
FROM Sites_old;";
|
||||
copy.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
using (var drop = conn.CreateCommand())
|
||||
{
|
||||
drop.Transaction = transaction;
|
||||
drop.CommandText = "DROP TABLE Sites_old;";
|
||||
drop.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
|
||||
using var enableFk = conn.CreateCommand();
|
||||
enableFk.CommandText = "PRAGMA foreign_keys = ON;";
|
||||
enableFk.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void RepairBrokenForeignKeys(AppDbContext db)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != System.Data.ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
var siteDependentTables = new[]
|
||||
{
|
||||
("ExportLogs", DatabaseSchemaSql.GetExportLogsCreateSql()),
|
||||
("AppEventLogs", DatabaseSchemaSql.GetAppEventLogsCreateSql()),
|
||||
("CentralSalesRecords", DatabaseSchemaSql.GetCentralSalesRecordsCreateSql()),
|
||||
("SapSourceDefinitions", DatabaseSchemaSql.GetSapSourceDefinitionsCreateSql()),
|
||||
("SapJoinDefinitions", DatabaseSchemaSql.GetSapJoinDefinitionsCreateSql()),
|
||||
("SapFieldMappings", DatabaseSchemaSql.GetSapFieldMappingsCreateSql())
|
||||
};
|
||||
|
||||
foreach (var (tableName, createSql) in siteDependentTables)
|
||||
{
|
||||
if (DatabaseSchemaTools.TableReferences(conn, tableName, "Sites_old"))
|
||||
DatabaseSchemaTools.RebuildTable(conn, tableName, createSql);
|
||||
}
|
||||
|
||||
if (DatabaseSchemaTools.TableReferences(conn, "Sites", "HanaServers_repair_old"))
|
||||
DatabaseSchemaTools.RebuildTable(conn, "Sites", DatabaseSchemaSql.GetSitesCreateSql());
|
||||
}
|
||||
|
||||
private static void AddColumnIfMissing(AppDbContext db, string table, string column, string type)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != System.Data.ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
var 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();
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureTransformationTable(AppDbContext db)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != System.Data.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,
|
||||
RuleScope TEXT NOT NULL DEFAULT 'Value',
|
||||
Argument TEXT NOT NULL DEFAULT '',
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||
IsActive INTEGER NOT NULL DEFAULT 1
|
||||
);";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void EnsureSapSourceTable(AppDbContext db)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != System.Data.ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = DatabaseSchemaSql.GetSapSourceDefinitionsCreateSql().Replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS");
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void EnsureCurrencyExchangeRateTable(AppDbContext db)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != System.Data.ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"
|
||||
CREATE TABLE IF NOT EXISTS CurrencyExchangeRates (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
FromCurrency TEXT NOT NULL,
|
||||
ToCurrency TEXT NOT NULL,
|
||||
Rate REAL NOT NULL,
|
||||
ValidFrom TEXT NOT NULL,
|
||||
ValidTo TEXT NULL,
|
||||
Notes TEXT NOT NULL DEFAULT '',
|
||||
IsActive INTEGER NOT NULL DEFAULT 1
|
||||
);";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void EnsureSapJoinTable(AppDbContext db)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != System.Data.ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = DatabaseSchemaSql.GetSapJoinDefinitionsCreateSql().Replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS");
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void EnsureSapFieldMappingTable(AppDbContext db)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != System.Data.ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = DatabaseSchemaSql.GetSapFieldMappingsCreateSql().Replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS");
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void EnsureCentralSalesRecordTable(AppDbContext db)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != System.Data.ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = DatabaseSchemaSql.GetCentralSalesRecordsCreateSql().Replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS");
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void EnsureAppEventLogTable(AppDbContext db)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != System.Data.ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = DatabaseSchemaSql.GetAppEventLogsCreateSql().Replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS");
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
private static void EnsureSourceSystemDefinitionTable(AppDbContext db)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != System.Data.ConnectionState.Open)
|
||||
conn.Open();
|
||||
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = @"
|
||||
CREATE TABLE IF NOT EXISTS SourceSystemDefinitions (
|
||||
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
Code TEXT NOT NULL,
|
||||
DisplayName TEXT NOT NULL,
|
||||
ConnectionKind TEXT NOT NULL,
|
||||
IsActive INTEGER NOT NULL DEFAULT 1,
|
||||
CentralServiceUrl TEXT NOT NULL DEFAULT '',
|
||||
CentralUsername TEXT NOT NULL DEFAULT '',
|
||||
CentralPassword TEXT NOT NULL DEFAULT ''
|
||||
);";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
internal static class DatabaseSchemaTools
|
||||
{
|
||||
internal static bool TableReferences(System.Data.Common.DbConnection connection, string tableName, string referencedTableName)
|
||||
{
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = $tableName;";
|
||||
|
||||
var parameter = command.CreateParameter();
|
||||
parameter.ParameterName = "$tableName";
|
||||
parameter.Value = tableName;
|
||||
command.Parameters.Add(parameter);
|
||||
|
||||
var sql = command.ExecuteScalar()?.ToString() ?? string.Empty;
|
||||
return sql.Contains(referencedTableName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
internal static void RebuildTable(System.Data.Common.DbConnection connection, string tableName, string createSql)
|
||||
{
|
||||
using var disableFk = connection.CreateCommand();
|
||||
disableFk.CommandText = "PRAGMA foreign_keys = OFF;";
|
||||
disableFk.ExecuteNonQuery();
|
||||
|
||||
using var transaction = connection.BeginTransaction();
|
||||
|
||||
var tempTableName = $"{tableName}_repair_old";
|
||||
|
||||
using (var rename = connection.CreateCommand())
|
||||
{
|
||||
rename.Transaction = transaction;
|
||||
rename.CommandText = $"ALTER TABLE {tableName} RENAME TO {tempTableName};";
|
||||
rename.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
using (var create = connection.CreateCommand())
|
||||
{
|
||||
create.Transaction = transaction;
|
||||
create.CommandText = createSql;
|
||||
create.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
var columns = GetSharedColumns(connection, transaction, tableName, tempTableName);
|
||||
if (columns.Count > 0)
|
||||
{
|
||||
var columnList = string.Join(", ", columns);
|
||||
|
||||
using var copy = connection.CreateCommand();
|
||||
copy.Transaction = transaction;
|
||||
copy.CommandText = $"INSERT INTO {tableName} ({columnList}) SELECT {columnList} FROM {tempTableName};";
|
||||
copy.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
using (var drop = connection.CreateCommand())
|
||||
{
|
||||
drop.Transaction = transaction;
|
||||
drop.CommandText = $"DROP TABLE {tempTableName};";
|
||||
drop.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
|
||||
using var enableFk = connection.CreateCommand();
|
||||
enableFk.CommandText = "PRAGMA foreign_keys = ON;";
|
||||
enableFk.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
internal static List<string> GetSharedColumns(System.Data.Common.DbConnection connection, System.Data.Common.DbTransaction? transaction, string newTableName, string oldTableName)
|
||||
{
|
||||
var newColumns = GetTableColumns(connection, transaction, newTableName);
|
||||
var oldColumns = GetTableColumns(connection, transaction, oldTableName);
|
||||
|
||||
return newColumns.Where(oldColumns.Contains).ToList();
|
||||
}
|
||||
|
||||
internal static HashSet<string> GetTableColumns(System.Data.Common.DbConnection connection, System.Data.Common.DbTransaction? transaction, string tableName)
|
||||
{
|
||||
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
using var command = connection.CreateCommand();
|
||||
command.Transaction = transaction;
|
||||
command.CommandText = $"PRAGMA table_info({tableName})";
|
||||
|
||||
using var reader = command.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
var name = reader["name"]?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
columns.Add(name);
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrafagSalesExporter.Data;
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class DatabaseSeedService : IDatabaseSeedService
|
||||
{
|
||||
public void SeedDefaults(AppDbContext db)
|
||||
{
|
||||
SeedIfEmpty(db);
|
||||
EnsureRecommendedTransformationRules(db);
|
||||
EnsureSourceSystemDefinitions(db);
|
||||
EnsureCentralHanaServerRecords(db);
|
||||
}
|
||||
|
||||
private static void SeedIfEmpty(AppDbContext db)
|
||||
{
|
||||
if (db.Sites.Any() || db.HanaServers.Any() || db.SharePointConfigs.Any() || db.ExportSettings.Any())
|
||||
return;
|
||||
|
||||
var serverBi1 = new HanaServer { SourceSystem = "BI1", Name = "BI1", Host = "travtrp0", Port = 30015, Username = "", Password = "" };
|
||||
var serverSage = new HanaServer { SourceSystem = "SAGE", Name = "SAGE", Host = "20.197.20.60", Port = 30015, Username = "", Password = "" };
|
||||
db.HanaServers.AddRange(serverBi1, serverSage);
|
||||
db.SaveChanges();
|
||||
|
||||
db.Sites.AddRange(
|
||||
new Site { HanaServerId = serverBi1.Id, Schema = "fr01_p", TSC = "TRFR", Land = "Frankreich", SourceSystem = "BI1", IsActive = true },
|
||||
new Site { HanaServerId = serverBi1.Id, Schema = "it01_p", TSC = "TRIT", Land = "Italien", SourceSystem = "BI1", IsActive = true },
|
||||
new Site { HanaServerId = serverBi1.Id, Schema = "us01_p", TSC = "TRUS", Land = "USA", SourceSystem = "BI1", IsActive = true },
|
||||
new Site { HanaServerId = serverSage.Id, Schema = "TRAFAG_LIVE", TSC = "TRIN", Land = "Indien", SourceSystem = "SAGE", IsActive = true }
|
||||
);
|
||||
|
||||
db.SharePointConfigs.Add(new SharePointConfig
|
||||
{
|
||||
SiteUrl = "https://trafagag.sharepoint.com/sites/WorldwideBIPlatform",
|
||||
ExportFolder = "/Shared Documents/Exports/",
|
||||
CentralExportFolder = "",
|
||||
TenantId = "",
|
||||
ClientId = "",
|
||||
ClientSecret = ""
|
||||
});
|
||||
|
||||
db.ExportSettings.Add(new ExportSettings
|
||||
{
|
||||
DateFilter = "2025-01-01",
|
||||
TimerHour = 3,
|
||||
TimerMinute = 0,
|
||||
TimerEnabled = true,
|
||||
DebugLoggingEnabled = false,
|
||||
LocalSiteExportFolder = "",
|
||||
LocalConsolidatedExportFolder = ""
|
||||
});
|
||||
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
private static void EnsureRecommendedTransformationRules(AppDbContext db)
|
||||
{
|
||||
var recommendedRules = new[]
|
||||
{
|
||||
new FieldTransformationRule
|
||||
{
|
||||
SourceSystem = "MANUAL_EXCEL",
|
||||
SourceField = nameof(SalesRecord.SalesCurrency),
|
||||
TargetField = nameof(SalesRecord.SalesCurrency),
|
||||
TransformationType = "Replace",
|
||||
RuleScope = "Value",
|
||||
Argument = "$=>USD",
|
||||
SortOrder = 100,
|
||||
IsActive = true
|
||||
},
|
||||
new FieldTransformationRule
|
||||
{
|
||||
SourceSystem = "MANUAL_EXCEL",
|
||||
SourceField = nameof(SalesRecord.StandardCostCurrency),
|
||||
TargetField = nameof(SalesRecord.StandardCostCurrency),
|
||||
TransformationType = "Replace",
|
||||
RuleScope = "Value",
|
||||
Argument = "$=>USD",
|
||||
SortOrder = 110,
|
||||
IsActive = true
|
||||
}
|
||||
};
|
||||
|
||||
var hasChanges = false;
|
||||
|
||||
foreach (var rule in recommendedRules)
|
||||
{
|
||||
var exists = db.FieldTransformationRules.Any(existing =>
|
||||
existing.SourceSystem == rule.SourceSystem &&
|
||||
existing.RuleScope == rule.RuleScope &&
|
||||
existing.SourceField == rule.SourceField &&
|
||||
existing.TargetField == rule.TargetField &&
|
||||
existing.TransformationType == rule.TransformationType &&
|
||||
existing.Argument == rule.Argument);
|
||||
|
||||
if (exists)
|
||||
continue;
|
||||
|
||||
db.FieldTransformationRules.Add(rule);
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (hasChanges)
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
private static void EnsureCentralHanaServerRecords(AppDbContext db)
|
||||
{
|
||||
var centralSystems = db.SourceSystemDefinitions
|
||||
.AsNoTracking()
|
||||
.Where(x => x.ConnectionKind == SourceSystemConnectionKinds.Hana)
|
||||
.OrderBy(x => x.Code)
|
||||
.Select(x => x.Code)
|
||||
.ToList();
|
||||
var changed = false;
|
||||
|
||||
foreach (var sourceSystem in centralSystems)
|
||||
{
|
||||
var existingCentral = db.HanaServers
|
||||
.OrderBy(x => x.Id)
|
||||
.FirstOrDefault(x => x.SourceSystem == sourceSystem);
|
||||
|
||||
if (existingCentral is not null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(existingCentral.Name))
|
||||
{
|
||||
existingCentral.Name = sourceSystem;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var linkedServer = db.Sites
|
||||
.Include(x => x.HanaServer)
|
||||
.Where(x => x.SourceSystem == sourceSystem && x.HanaServerId != null && x.HanaServer != null)
|
||||
.Select(x => x.HanaServer!)
|
||||
.OrderBy(x => x.Id)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (linkedServer is not null)
|
||||
{
|
||||
linkedServer.SourceSystem = sourceSystem;
|
||||
if (string.IsNullOrWhiteSpace(linkedServer.Name))
|
||||
linkedServer.Name = sourceSystem;
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
db.HanaServers.Add(new HanaServer
|
||||
{
|
||||
SourceSystem = sourceSystem,
|
||||
Name = sourceSystem,
|
||||
Host = string.Empty,
|
||||
Port = 30015,
|
||||
Username = string.Empty,
|
||||
Password = string.Empty,
|
||||
DatabaseName = string.Empty,
|
||||
AdditionalParams = string.Empty
|
||||
});
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
private static void EnsureSourceSystemDefinitions(AppDbContext db)
|
||||
{
|
||||
var defaults = new[]
|
||||
{
|
||||
new SourceSystemDefinition { Code = "SAP", DisplayName = "SAP", ConnectionKind = SourceSystemConnectionKinds.SapGateway, IsActive = true },
|
||||
new SourceSystemDefinition { Code = "BI1", DisplayName = "BI1", ConnectionKind = SourceSystemConnectionKinds.Hana, IsActive = true },
|
||||
new SourceSystemDefinition { Code = "SAGE", DisplayName = "SAGE", ConnectionKind = SourceSystemConnectionKinds.Hana, IsActive = true },
|
||||
new SourceSystemDefinition { Code = "MANUAL_EXCEL", DisplayName = "Manual Excel", ConnectionKind = SourceSystemConnectionKinds.ManualExcel, IsActive = true }
|
||||
};
|
||||
|
||||
var existing = db.SourceSystemDefinitions.ToList();
|
||||
var changed = false;
|
||||
|
||||
foreach (var item in defaults)
|
||||
{
|
||||
var current = existing.FirstOrDefault(x => x.Code == item.Code);
|
||||
if (current is null)
|
||||
{
|
||||
db.SourceSystemDefinitions.Add(item);
|
||||
existing.Add(item);
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(current.DisplayName))
|
||||
{
|
||||
current.DisplayName = item.DisplayName;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(current.ConnectionKind))
|
||||
{
|
||||
current.ConnectionKind = item.ConnectionKind;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(current.CentralServiceUrl) &&
|
||||
string.Equals(current.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var sapSite = db.Sites
|
||||
.Where(x => x.SourceSystem == current.Code && !string.IsNullOrWhiteSpace(x.SapServiceUrl))
|
||||
.OrderBy(x => x.Id)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (sapSite is not null)
|
||||
{
|
||||
current.CentralServiceUrl = sapSite.SapServiceUrl;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
db.SaveChanges();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using TrafagSalesExporter.Data;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface IDatabaseSchemaMaintenanceService
|
||||
{
|
||||
void EnsureSchema(AppDbContext db);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using TrafagSalesExporter.Data;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface IDatabaseSeedService
|
||||
{
|
||||
void SeedDefaults(AppDbContext db);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrafagSalesExporter.Data;
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface ILogsPageService
|
||||
{
|
||||
Task<LogsPageState> LoadAsync(string? filterLand, string? filterStatus, DateTime? filterDate);
|
||||
Task<int> DeleteOldLogsAsync(int olderThanDays);
|
||||
}
|
||||
|
||||
public sealed class LogsPageService : ILogsPageService
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||
|
||||
public LogsPageService(IDbContextFactory<AppDbContext> dbFactory)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
}
|
||||
|
||||
public async Task<LogsPageState> LoadAsync(string? filterLand, string? filterStatus, DateTime? filterDate)
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
|
||||
IQueryable<ExportLog> query = db.ExportLogs.OrderByDescending(l => l.Timestamp);
|
||||
|
||||
if (!string.IsNullOrEmpty(filterLand))
|
||||
query = query.Where(l => l.Land == filterLand);
|
||||
|
||||
if (!string.IsNullOrEmpty(filterStatus))
|
||||
query = query.Where(l => l.Status == filterStatus);
|
||||
|
||||
if (filterDate.HasValue)
|
||||
query = query.Where(l => l.Timestamp.Date == filterDate.Value.Date);
|
||||
|
||||
IQueryable<AppEventLog> appLogQuery = db.AppEventLogs.OrderByDescending(l => l.Timestamp);
|
||||
|
||||
if (!string.IsNullOrEmpty(filterLand))
|
||||
appLogQuery = appLogQuery.Where(l => l.Land == filterLand);
|
||||
|
||||
if (filterDate.HasValue)
|
||||
appLogQuery = appLogQuery.Where(l => l.Timestamp.Date == filterDate.Value.Date);
|
||||
|
||||
return new LogsPageState
|
||||
{
|
||||
AvailableLands = await db.ExportLogs.Select(l => l.Land).Distinct().OrderBy(l => l).ToListAsync(),
|
||||
Logs = await query.Take(500).ToListAsync(),
|
||||
AppLogs = await appLogQuery.Take(500).ToListAsync()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<int> DeleteOldLogsAsync(int olderThanDays)
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var cutoff = DateTime.Now.AddDays(-olderThanDays);
|
||||
var oldLogs = await db.ExportLogs.Where(l => l.Timestamp < cutoff).ToListAsync();
|
||||
db.ExportLogs.RemoveRange(oldLogs);
|
||||
await db.SaveChangesAsync();
|
||||
return oldLogs.Count;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class LogsPageState
|
||||
{
|
||||
public List<ExportLog> Logs { get; set; } = [];
|
||||
public List<AppEventLog> AppLogs { get; set; } = [];
|
||||
public List<string> AvailableLands { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface IManagementCockpitPageService
|
||||
{
|
||||
Task<ManagementCockpitPageState> InitializeAsync(string? selectedFilePath, int selectedCentralYear);
|
||||
Task<List<ManagementCockpitFileOption>> LoadFilesAsync();
|
||||
Task<List<int>> LoadCentralYearsAsync();
|
||||
Task<ManagementCockpitResult> AnalyzeAsync(string filePath);
|
||||
Task<ManagementCockpitCentralResult> AnalyzeCentralAsync(int year, int? month);
|
||||
}
|
||||
|
||||
public sealed class ManagementCockpitPageService : IManagementCockpitPageService
|
||||
{
|
||||
private readonly IManagementCockpitService _cockpitService;
|
||||
|
||||
public ManagementCockpitPageService(IManagementCockpitService cockpitService)
|
||||
{
|
||||
_cockpitService = cockpitService;
|
||||
}
|
||||
|
||||
public async Task<ManagementCockpitPageState> InitializeAsync(string? selectedFilePath, int selectedCentralYear)
|
||||
{
|
||||
var files = await _cockpitService.GetAvailableFilesAsync();
|
||||
var years = await _cockpitService.GetAvailableCentralYearsAsync();
|
||||
|
||||
return new ManagementCockpitPageState
|
||||
{
|
||||
Files = files,
|
||||
CentralYears = years,
|
||||
SelectedFilePath = selectedFilePath ?? files.FirstOrDefault()?.Path,
|
||||
SelectedCentralYear = selectedCentralYear == 0 ? years.LastOrDefault() : selectedCentralYear
|
||||
};
|
||||
}
|
||||
|
||||
public Task<List<ManagementCockpitFileOption>> LoadFilesAsync()
|
||||
=> _cockpitService.GetAvailableFilesAsync();
|
||||
|
||||
public Task<List<int>> LoadCentralYearsAsync()
|
||||
=> _cockpitService.GetAvailableCentralYearsAsync();
|
||||
|
||||
public Task<ManagementCockpitResult> AnalyzeAsync(string filePath)
|
||||
=> _cockpitService.AnalyzeAsync(filePath);
|
||||
|
||||
public Task<ManagementCockpitCentralResult> AnalyzeCentralAsync(int year, int? month)
|
||||
=> _cockpitService.AnalyzeCentralAsync(year, month);
|
||||
}
|
||||
|
||||
public sealed class ManagementCockpitPageState
|
||||
{
|
||||
public List<ManagementCockpitFileOption> Files { get; set; } = [];
|
||||
public List<int> CentralYears { get; set; } = [];
|
||||
public string? SelectedFilePath { get; set; }
|
||||
public int SelectedCentralYear { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrafagSalesExporter.Data;
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface ISettingsPageService
|
||||
{
|
||||
Task<SettingsPageState> LoadAsync();
|
||||
Task SaveSharePointAsync(SharePointConfig config);
|
||||
Task<string> BuildSharePointTestPreviewAsync(SharePointConfig config);
|
||||
Task SaveExportSettingsAsync(ExportSettings settings);
|
||||
Task<List<SourceSystemDefinition>> SaveSourceSystemsAsync(List<SourceSystemDefinition> sourceSystems);
|
||||
Task<List<CurrencyExchangeRate>> SaveExchangeRatesAsync(List<CurrencyExchangeRate> exchangeRates);
|
||||
Task<SettingsExchangeRateRefreshResult> RefreshEcbRatesAsync();
|
||||
Task<string> ExportConfigurationAsync(bool includeSecrets);
|
||||
Task<SettingsPageState> ImportConfigurationAsync(string json);
|
||||
Task<PageActionResult> TestCentralCredentialsAsync(SourceSystemDefinition definition);
|
||||
}
|
||||
|
||||
public sealed class SettingsPageService : ISettingsPageService
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||
private readonly ISharePointUploadService _sharePointService;
|
||||
private readonly TimerBackgroundService _timerService;
|
||||
private readonly IHanaQueryService _hanaService;
|
||||
private readonly ISapGatewayService _sapGatewayService;
|
||||
private readonly IConfigTransferService _configTransferService;
|
||||
private readonly IExchangeRateImportService _exchangeRateImportService;
|
||||
|
||||
public SettingsPageService(
|
||||
IDbContextFactory<AppDbContext> dbFactory,
|
||||
ISharePointUploadService sharePointService,
|
||||
TimerBackgroundService timerService,
|
||||
IHanaQueryService hanaService,
|
||||
ISapGatewayService sapGatewayService,
|
||||
IConfigTransferService configTransferService,
|
||||
IExchangeRateImportService exchangeRateImportService)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_sharePointService = sharePointService;
|
||||
_timerService = timerService;
|
||||
_hanaService = hanaService;
|
||||
_sapGatewayService = sapGatewayService;
|
||||
_configTransferService = configTransferService;
|
||||
_exchangeRateImportService = exchangeRateImportService;
|
||||
}
|
||||
|
||||
public async Task<SettingsPageState> LoadAsync()
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
return new SettingsPageState
|
||||
{
|
||||
SharePointConfig = await db.SharePointConfigs.FirstOrDefaultAsync() ?? new SharePointConfig(),
|
||||
ExportSettings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings(),
|
||||
SourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync(),
|
||||
ExchangeRates = await LoadExchangeRatesAsync(db)
|
||||
};
|
||||
}
|
||||
|
||||
public async Task SaveSharePointAsync(SharePointConfig config)
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var existing = await db.SharePointConfigs.FirstOrDefaultAsync();
|
||||
if (existing is null)
|
||||
{
|
||||
db.SharePointConfigs.Add(config);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.SiteUrl = config.SiteUrl;
|
||||
existing.ExportFolder = config.ExportFolder;
|
||||
existing.CentralExportFolder = config.CentralExportFolder;
|
||||
existing.TenantId = config.TenantId;
|
||||
existing.ClientId = config.ClientId;
|
||||
existing.ClientSecret = config.ClientSecret;
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<string> BuildSharePointTestPreviewAsync(SharePointConfig config)
|
||||
{
|
||||
var tenantId = NormalizeConfigValue(config.TenantId);
|
||||
var clientId = NormalizeConfigValue(config.ClientId);
|
||||
var clientSecret = NormalizeConfigValue(config.ClientSecret);
|
||||
var siteUrl = NormalizeConfigValue(config.SiteUrl);
|
||||
|
||||
await _sharePointService.TestConnectionAsync(tenantId, clientId, clientSecret, siteUrl);
|
||||
return BuildSharePointTestPreview(tenantId, clientId, clientSecret, siteUrl);
|
||||
}
|
||||
|
||||
public async Task SaveExportSettingsAsync(ExportSettings settings)
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var existing = await db.ExportSettings.FirstOrDefaultAsync();
|
||||
if (existing is null)
|
||||
{
|
||||
db.ExportSettings.Add(settings);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.DateFilter = settings.DateFilter;
|
||||
existing.TimerHour = settings.TimerHour;
|
||||
existing.TimerMinute = settings.TimerMinute;
|
||||
existing.TimerEnabled = settings.TimerEnabled;
|
||||
existing.DebugLoggingEnabled = settings.DebugLoggingEnabled;
|
||||
existing.LocalSiteExportFolder = settings.LocalSiteExportFolder;
|
||||
existing.LocalConsolidatedExportFolder = settings.LocalConsolidatedExportFolder;
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
_timerService.Recalculate();
|
||||
}
|
||||
|
||||
public async Task<List<SourceSystemDefinition>> SaveSourceSystemsAsync(List<SourceSystemDefinition> sourceSystems)
|
||||
{
|
||||
var normalized = sourceSystems
|
||||
.Select(x => new SourceSystemDefinition
|
||||
{
|
||||
Id = x.Id,
|
||||
Code = NormalizeSourceSystemCode(x.Code),
|
||||
DisplayName = NormalizeConfigValue(x.DisplayName),
|
||||
ConnectionKind = NormalizeConnectionKind(x.ConnectionKind),
|
||||
IsActive = x.IsActive,
|
||||
CentralServiceUrl = NormalizeConfigValue(x.CentralServiceUrl),
|
||||
CentralUsername = NormalizeConfigValue(x.CentralUsername),
|
||||
CentralPassword = x.CentralPassword ?? string.Empty
|
||||
})
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.Code))
|
||||
.ToList();
|
||||
|
||||
if (normalized.Any(x => string.IsNullOrWhiteSpace(x.DisplayName)))
|
||||
throw new InvalidOperationException("Jedes Quellsystem braucht einen Anzeigenamen.");
|
||||
|
||||
var duplicates = normalized.GroupBy(x => x.Code).FirstOrDefault(g => g.Count() > 1);
|
||||
if (duplicates is not null)
|
||||
throw new InvalidOperationException($"Quellsystem-Code doppelt vorhanden: {duplicates.Key}");
|
||||
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var existing = await db.SourceSystemDefinitions.ToListAsync();
|
||||
if (existing.Count > 0)
|
||||
db.SourceSystemDefinitions.RemoveRange(existing);
|
||||
|
||||
db.SourceSystemDefinitions.AddRange(normalized);
|
||||
await db.SaveChangesAsync();
|
||||
return await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<CurrencyExchangeRate>> SaveExchangeRatesAsync(List<CurrencyExchangeRate> exchangeRates)
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var existingRates = await db.CurrencyExchangeRates.ToListAsync();
|
||||
if (existingRates.Count > 0)
|
||||
db.CurrencyExchangeRates.RemoveRange(existingRates);
|
||||
|
||||
db.CurrencyExchangeRates.AddRange(exchangeRates.Select(rate => new CurrencyExchangeRate
|
||||
{
|
||||
FromCurrency = NormalizeConfigValue(rate.FromCurrency).ToUpperInvariant(),
|
||||
ToCurrency = NormalizeConfigValue(rate.ToCurrency).ToUpperInvariant(),
|
||||
Rate = rate.Rate,
|
||||
ValidFrom = rate.ValidFrom.Date,
|
||||
ValidTo = rate.ValidTo?.Date,
|
||||
Notes = NormalizeConfigValue(rate.Notes),
|
||||
IsActive = rate.IsActive
|
||||
}).Where(rate => !string.IsNullOrWhiteSpace(rate.FromCurrency)
|
||||
&& !string.IsNullOrWhiteSpace(rate.ToCurrency)
|
||||
&& rate.Rate > 0m));
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return await LoadExchangeRatesAsync(db);
|
||||
}
|
||||
|
||||
public async Task<SettingsExchangeRateRefreshResult> RefreshEcbRatesAsync()
|
||||
{
|
||||
var result = await _exchangeRateImportService.RefreshEcbRatesAsync();
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
return new SettingsExchangeRateRefreshResult
|
||||
{
|
||||
ImportedCount = result.ImportedCount,
|
||||
RateDate = result.RateDate,
|
||||
ExchangeRates = await LoadExchangeRatesAsync(db)
|
||||
};
|
||||
}
|
||||
|
||||
public Task<string> ExportConfigurationAsync(bool includeSecrets)
|
||||
=> _configTransferService.ExportJsonAsync(includeSecrets);
|
||||
|
||||
public async Task<SettingsPageState> ImportConfigurationAsync(string json)
|
||||
{
|
||||
await _configTransferService.ImportJsonAsync(json);
|
||||
_timerService.Recalculate();
|
||||
return await LoadAsync();
|
||||
}
|
||||
|
||||
public async Task<PageActionResult> TestCentralCredentialsAsync(SourceSystemDefinition definition)
|
||||
{
|
||||
if (string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase))
|
||||
return await TestCentralSapCredentialsAsync(definition);
|
||||
|
||||
if (string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.Hana, StringComparison.OrdinalIgnoreCase))
|
||||
return await TestCentralHanaCredentialsAsync(definition);
|
||||
|
||||
return PageActionResult.WarningResult($"Quellsystem '{definition.Code}' hat keinen testbaren Verbindungstyp.");
|
||||
}
|
||||
|
||||
private async Task<PageActionResult> TestCentralHanaCredentialsAsync(SourceSystemDefinition definition)
|
||||
{
|
||||
var sourceSystem = definition.Code;
|
||||
var username = definition.CentralUsername;
|
||||
var password = definition.CentralPassword;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
return PageActionResult.WarningResult($"Fuer {sourceSystem} sind keine zentralen Zugangsdaten gepflegt.");
|
||||
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var centralServer = await db.HanaServers
|
||||
.Where(s => s.SourceSystem == sourceSystem)
|
||||
.OrderBy(s => s.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (centralServer is null || string.IsNullOrWhiteSpace(centralServer.Host))
|
||||
return PageActionResult.WarningResult($"Keine zentrale HANA-Konfiguration fuer {sourceSystem} gefunden.");
|
||||
|
||||
var testServer = new HanaServer
|
||||
{
|
||||
SourceSystem = sourceSystem,
|
||||
Name = $"{sourceSystem} Central Test",
|
||||
Host = centralServer.Host,
|
||||
Port = centralServer.Port,
|
||||
Username = username.Trim(),
|
||||
Password = password.Trim(),
|
||||
DatabaseName = centralServer.DatabaseName,
|
||||
UseSsl = centralServer.UseSsl,
|
||||
ValidateCertificate = centralServer.ValidateCertificate,
|
||||
AdditionalParams = centralServer.AdditionalParams
|
||||
};
|
||||
|
||||
var result = await Task.Run(() => _hanaService.TestConnectionDetailed(testServer));
|
||||
return result.Success
|
||||
? PageActionResult.SuccessResult($"{sourceSystem}: Zentrale HANA-Verbindung erfolgreich.")
|
||||
: PageActionResult.ErrorResult($"{sourceSystem}: {result.ExceptionType} - {result.ErrorMessage}");
|
||||
}
|
||||
|
||||
private async Task<PageActionResult> TestCentralSapCredentialsAsync(SourceSystemDefinition definition)
|
||||
{
|
||||
var sourceSystem = definition.Code;
|
||||
var username = definition.CentralUsername;
|
||||
var password = definition.CentralPassword;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
return PageActionResult.WarningResult("Fuer SAP sind keine zentralen Gateway-Zugangsdaten gepflegt.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(definition.CentralServiceUrl))
|
||||
return PageActionResult.WarningResult($"Fuer {sourceSystem} ist keine zentrale SAP Service URL gepflegt.");
|
||||
|
||||
try
|
||||
{
|
||||
await _sapGatewayService.TestConnectionAsync(definition.CentralServiceUrl, username.Trim(), password.Trim());
|
||||
return PageActionResult.SuccessResult($"{sourceSystem}: Zentrale SAP Gateway-Verbindung erfolgreich.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return PageActionResult.ErrorResult($"{sourceSystem}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<List<CurrencyExchangeRate>> LoadExchangeRatesAsync(AppDbContext db)
|
||||
=> await db.CurrencyExchangeRates
|
||||
.OrderBy(x => x.FromCurrency)
|
||||
.ThenBy(x => x.ToCurrency)
|
||||
.ThenByDescending(x => x.ValidFrom)
|
||||
.ToListAsync();
|
||||
|
||||
public static string NormalizeSourceSystemCode(string? code) => NormalizeConfigValue(code).ToUpperInvariant();
|
||||
|
||||
public static string NormalizeConnectionKind(string? connectionKind)
|
||||
=> SourceSystemConnectionKinds.All.Contains(connectionKind ?? string.Empty, StringComparer.OrdinalIgnoreCase)
|
||||
? (connectionKind ?? string.Empty).Trim().ToUpperInvariant()
|
||||
: SourceSystemConnectionKinds.Hana;
|
||||
|
||||
public static string NormalizeConfigValue(string? value) => value?.Trim() ?? string.Empty;
|
||||
|
||||
public static string BuildSharePointTestPreview(string tenantId, string clientId, string clientSecret, string siteUrl)
|
||||
{
|
||||
var maskedSecret = string.IsNullOrEmpty(clientSecret)
|
||||
? "<leer>"
|
||||
: $"{new string('*', Math.Min(clientSecret.Length, 8))} (len={clientSecret.Length})";
|
||||
|
||||
return string.Join(Environment.NewLine,
|
||||
[
|
||||
$"Tenant ID: {tenantId}",
|
||||
$"Client ID: {clientId}",
|
||||
$"Client Secret: {maskedSecret}",
|
||||
$"Site URL: {siteUrl}"
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SettingsPageState
|
||||
{
|
||||
public SharePointConfig SharePointConfig { get; set; } = new();
|
||||
public ExportSettings ExportSettings { get; set; } = new();
|
||||
public List<SourceSystemDefinition> SourceSystems { get; set; } = [];
|
||||
public List<CurrencyExchangeRate> ExchangeRates { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class SettingsExchangeRateRefreshResult
|
||||
{
|
||||
public int ImportedCount { get; set; }
|
||||
public DateTime RateDate { get; set; }
|
||||
public List<CurrencyExchangeRate> ExchangeRates { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class PageActionResult
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
public bool Warning { get; init; }
|
||||
public string Message { get; init; } = string.Empty;
|
||||
|
||||
public static PageActionResult SuccessResult(string message) => new() { Success = true, Message = message };
|
||||
public static PageActionResult WarningResult(string message) => new() { Warning = true, Message = message };
|
||||
public static PageActionResult ErrorResult(string message) => new() { Message = message };
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrafagSalesExporter.Data;
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface IStandortePageService
|
||||
{
|
||||
Task<StandortePageState> LoadAsync();
|
||||
Task SaveServerAsync(HanaServer server, IEnumerable<string> hanaSourceSystemCodes);
|
||||
Task DeleteServerAsync(HanaServer server);
|
||||
Task<ConnectionTestResult> TestServerConnectionAsync(HanaServer server);
|
||||
Task<StandortEditorState> LoadSiteEditorAsync(Site site, IEnumerable<SourceSystemDefinition> sourceSystems);
|
||||
Task SaveSiteAsync(Site site, bool usesHanaConnection, bool isSapSite, List<SapSourceDefinition> sapSources, List<SapJoinDefinition> sapJoins, List<SapFieldMapping> sapMappings, List<string> sapEntitySetsCache);
|
||||
Task DeleteSiteAsync(Site site);
|
||||
Task<List<string>> LoadAvailableSchemasAsync(Site site);
|
||||
Task<SapEntitySetRefreshResult> RefreshSapEntitySetsAsync(Site site);
|
||||
Task<SapSourceFieldRefreshResult> RefreshSapSourceFieldsAsync(Site site, List<SapSourceDefinition> sapSources, List<SapFieldMapping> sapMappings);
|
||||
Task<DateTime> ValidateManualImportPathAsync(string manualImportFilePath);
|
||||
}
|
||||
|
||||
public sealed class StandortePageService : IStandortePageService
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||
private readonly IHanaQueryService _hanaService;
|
||||
private readonly ISapGatewayService _sapGatewayService;
|
||||
private readonly ISharePointUploadService _sharePointService;
|
||||
private readonly IAppEventLogService _appEventLogService;
|
||||
|
||||
public StandortePageService(
|
||||
IDbContextFactory<AppDbContext> dbFactory,
|
||||
IHanaQueryService hanaService,
|
||||
ISapGatewayService sapGatewayService,
|
||||
ISharePointUploadService sharePointService,
|
||||
IAppEventLogService appEventLogService)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_hanaService = hanaService;
|
||||
_sapGatewayService = sapGatewayService;
|
||||
_sharePointService = sharePointService;
|
||||
_appEventLogService = appEventLogService;
|
||||
}
|
||||
|
||||
public async Task<StandortePageState> LoadAsync()
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var sourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync();
|
||||
var hanaSourceSystemCodes = sourceSystems
|
||||
.Where(x => string.Equals(x.ConnectionKind, SourceSystemConnectionKinds.Hana, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(x => x.Code)
|
||||
.ToList();
|
||||
|
||||
return new StandortePageState
|
||||
{
|
||||
SourceSystems = sourceSystems,
|
||||
Servers = await db.HanaServers
|
||||
.Where(s => hanaSourceSystemCodes.Contains(s.SourceSystem))
|
||||
.OrderBy(s => s.SourceSystem)
|
||||
.ThenBy(s => s.Name)
|
||||
.ToListAsync(),
|
||||
Sites = await db.Sites.Include(s => s.HanaServer).OrderBy(s => s.Land).ToListAsync()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task SaveServerAsync(HanaServer server, IEnumerable<string> hanaSourceSystemCodes)
|
||||
{
|
||||
server.SourceSystem = string.IsNullOrWhiteSpace(server.SourceSystem)
|
||||
? hanaSourceSystemCodes.FirstOrDefault() ?? string.Empty
|
||||
: server.SourceSystem.Trim().ToUpperInvariant();
|
||||
server.Name = string.IsNullOrWhiteSpace(server.Name) ? server.SourceSystem : server.Name.Trim();
|
||||
server.Host = server.Host.Trim();
|
||||
server.DatabaseName = server.DatabaseName.Trim();
|
||||
server.AdditionalParams = server.AdditionalParams.Trim();
|
||||
server.Username = string.Empty;
|
||||
server.Password = string.Empty;
|
||||
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
if (server.Id == 0)
|
||||
{
|
||||
var existingForSourceSystem = await db.HanaServers
|
||||
.OrderBy(x => x.Id)
|
||||
.FirstOrDefaultAsync(x => x.SourceSystem == server.SourceSystem);
|
||||
|
||||
if (existingForSourceSystem is null)
|
||||
{
|
||||
db.HanaServers.Add(server);
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyServer(existingForSourceSystem, server);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var existing = await db.HanaServers.FindAsync(server.Id);
|
||||
if (existing is not null)
|
||||
ApplyServer(existing, server);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteServerAsync(HanaServer server)
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var linkedSites = await db.Sites
|
||||
.Where(s => s.HanaServerId == server.Id)
|
||||
.OrderBy(s => s.Land)
|
||||
.Select(s => $"{s.Land} ({s.TSC})")
|
||||
.ToListAsync();
|
||||
|
||||
if (linkedSites.Count > 0)
|
||||
throw new InvalidOperationException($"Server kann nicht geloescht werden. Noch verknuepfte Standorte: {string.Join(", ", linkedSites)}");
|
||||
|
||||
var entity = await db.HanaServers.FindAsync(server.Id);
|
||||
if (entity is not null)
|
||||
{
|
||||
db.HanaServers.Remove(entity);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ConnectionTestResult> TestServerConnectionAsync(HanaServer server)
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var sourceDefinition = await db.SourceSystemDefinitions
|
||||
.OrderBy(x => x.Id)
|
||||
.FirstOrDefaultAsync(x => x.Code == server.SourceSystem);
|
||||
|
||||
if (sourceDefinition is null)
|
||||
throw new InvalidOperationException($"Quellsystem '{server.SourceSystem}' nicht gefunden.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(sourceDefinition.CentralUsername) || string.IsNullOrWhiteSpace(sourceDefinition.CentralPassword))
|
||||
throw new InvalidOperationException($"Fuer {server.SourceSystem} sind keine zentralen Zugangsdaten im Quellsystem gepflegt.");
|
||||
|
||||
var testServer = new HanaServer
|
||||
{
|
||||
Id = server.Id,
|
||||
SourceSystem = server.SourceSystem,
|
||||
Name = server.Name,
|
||||
Host = server.Host,
|
||||
Port = server.Port,
|
||||
Username = sourceDefinition.CentralUsername.Trim(),
|
||||
Password = sourceDefinition.CentralPassword,
|
||||
DatabaseName = server.DatabaseName,
|
||||
UseSsl = server.UseSsl,
|
||||
ValidateCertificate = server.ValidateCertificate,
|
||||
AdditionalParams = server.AdditionalParams
|
||||
};
|
||||
|
||||
await _appEventLogService.WriteAsync("HANA", "Server-Test aus UI gestartet", details: testServer.GetConnectionStringPreview());
|
||||
return await Task.Run(() => _hanaService.TestConnectionDetailed(testServer));
|
||||
}
|
||||
|
||||
public async Task<StandortEditorState> LoadSiteEditorAsync(Site site, IEnumerable<SourceSystemDefinition> sourceSystems)
|
||||
{
|
||||
var effectiveSourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem)
|
||||
? sourceSystems.FirstOrDefault()?.Code ?? "SAP"
|
||||
: site.SourceSystem;
|
||||
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var sapSources = await db.SapSourceDefinitions.Where(s => s.SiteId == site.Id).OrderBy(s => s.SortOrder).ThenBy(s => s.Id).ToListAsync();
|
||||
var sapJoins = await db.SapJoinDefinitions.Where(j => j.SiteId == site.Id).OrderBy(j => j.SortOrder).ThenBy(j => j.Id).ToListAsync();
|
||||
var sapMappings = await db.SapFieldMappings.Where(m => m.SiteId == site.Id).OrderBy(m => m.SortOrder).ThenBy(m => m.Id).ToListAsync();
|
||||
|
||||
return new StandortEditorState
|
||||
{
|
||||
Site = new Site
|
||||
{
|
||||
Id = site.Id,
|
||||
HanaServerId = site.HanaServerId,
|
||||
Schema = site.Schema,
|
||||
TSC = site.TSC,
|
||||
Land = site.Land,
|
||||
SourceSystem = effectiveSourceSystem,
|
||||
UsernameOverride = site.UsernameOverride,
|
||||
PasswordOverride = site.PasswordOverride,
|
||||
LocalExportFolderOverride = site.LocalExportFolderOverride,
|
||||
ManualImportFilePath = site.ManualImportFilePath,
|
||||
ManualImportLastUploadedAtUtc = site.ManualImportLastUploadedAtUtc,
|
||||
SapServiceUrl = site.SapServiceUrl,
|
||||
SapEntitySet = site.SapEntitySet,
|
||||
SapEntitySetsCache = site.SapEntitySetsCache,
|
||||
SapEntitySetsRefreshedAtUtc = site.SapEntitySetsRefreshedAtUtc,
|
||||
IsActive = site.IsActive
|
||||
},
|
||||
SapEntitySets = ParseSapEntitySets(site.SapEntitySetsCache),
|
||||
SapSources = sapSources,
|
||||
SapJoins = sapJoins,
|
||||
SapMappings = sapMappings
|
||||
};
|
||||
}
|
||||
|
||||
public async Task SaveSiteAsync(Site site, bool usesHanaConnection, bool isSapSite, List<SapSourceDefinition> sapSources, List<SapJoinDefinition> sapJoins, List<SapFieldMapping> sapMappings, List<string> sapEntitySetsCache)
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var serverId = usesHanaConnection ? await ResolveCentralHanaServerIdAsync(db, site) : (int?)null;
|
||||
site.HanaServerId = serverId;
|
||||
site.SapEntitySetsCache = JsonSerializer.Serialize(sapEntitySetsCache);
|
||||
|
||||
if (site.Id == 0)
|
||||
{
|
||||
db.Sites.Add(site);
|
||||
}
|
||||
else
|
||||
{
|
||||
var existing = await db.Sites.FindAsync(site.Id);
|
||||
if (existing is not null)
|
||||
ApplySite(existing, site);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
await SaveSapConfigurationAsync(db, site.Id, isSapSite, sapSources, sapJoins, sapMappings);
|
||||
}
|
||||
|
||||
public async Task DeleteSiteAsync(Site site)
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var entity = await db.Sites.FindAsync(site.Id);
|
||||
if (entity is null)
|
||||
return;
|
||||
|
||||
var sources = await db.SapSourceDefinitions.Where(s => s.SiteId == site.Id).ToListAsync();
|
||||
var joins = await db.SapJoinDefinitions.Where(j => j.SiteId == site.Id).ToListAsync();
|
||||
var mappings = await db.SapFieldMappings.Where(m => m.SiteId == site.Id).ToListAsync();
|
||||
var centralRows = await db.CentralSalesRecords.Where(r => r.SiteId == site.Id).ToListAsync();
|
||||
if (sources.Count > 0) db.SapSourceDefinitions.RemoveRange(sources);
|
||||
if (joins.Count > 0) db.SapJoinDefinitions.RemoveRange(joins);
|
||||
if (mappings.Count > 0) db.SapFieldMappings.RemoveRange(mappings);
|
||||
if (centralRows.Count > 0) db.CentralSalesRecords.RemoveRange(centralRows);
|
||||
db.Sites.Remove(entity);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<List<string>> LoadAvailableSchemasAsync(Site site)
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var sourceDefinition = await db.SourceSystemDefinitions.OrderBy(x => x.Id).FirstOrDefaultAsync(x => x.Code == site.SourceSystem)
|
||||
?? throw new InvalidOperationException($"Quellsystem '{site.SourceSystem}' nicht gefunden.");
|
||||
|
||||
var centralServer = await db.HanaServers.OrderBy(x => x.Id).FirstOrDefaultAsync(x => x.SourceSystem == site.SourceSystem);
|
||||
if (centralServer is null || string.IsNullOrWhiteSpace(centralServer.Host))
|
||||
throw new InvalidOperationException($"Fuer {site.SourceSystem} ist keine gueltige zentrale HANA-Konfiguration vorhanden.");
|
||||
|
||||
var username = string.IsNullOrWhiteSpace(site.UsernameOverride) ? sourceDefinition.CentralUsername ?? string.Empty : site.UsernameOverride;
|
||||
var password = string.IsNullOrWhiteSpace(site.PasswordOverride) ? sourceDefinition.CentralPassword ?? string.Empty : site.PasswordOverride;
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
throw new InvalidOperationException($"Fuer {site.SourceSystem} sind weder zentrale Zugangsdaten noch Standort-Overrides gesetzt.");
|
||||
|
||||
var lookupServer = new HanaServer
|
||||
{
|
||||
Id = centralServer.Id,
|
||||
SourceSystem = centralServer.SourceSystem,
|
||||
Name = centralServer.Name,
|
||||
Host = centralServer.Host,
|
||||
Port = centralServer.Port,
|
||||
Username = username.Trim(),
|
||||
Password = password,
|
||||
DatabaseName = centralServer.DatabaseName,
|
||||
UseSsl = centralServer.UseSsl,
|
||||
ValidateCertificate = centralServer.ValidateCertificate,
|
||||
AdditionalParams = centralServer.AdditionalParams
|
||||
};
|
||||
|
||||
return await Task.Run(() => _hanaService.GetAvailableSchemas(lookupServer))
|
||||
.ContinueWith(task => task.Result
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList());
|
||||
}
|
||||
|
||||
public async Task<SapEntitySetRefreshResult> RefreshSapEntitySetsAsync(Site site)
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var sourceDefinition = await db.SourceSystemDefinitions.OrderBy(x => x.Id).FirstOrDefaultAsync(x => x.Code == site.SourceSystem);
|
||||
var serviceUrl = string.IsNullOrWhiteSpace(site.SapServiceUrl) ? sourceDefinition?.CentralServiceUrl ?? string.Empty : site.SapServiceUrl;
|
||||
if (string.IsNullOrWhiteSpace(serviceUrl))
|
||||
throw new InvalidOperationException("Es ist weder eine zentrale SAP Service URL noch ein Standort-Override gesetzt.");
|
||||
|
||||
var username = string.IsNullOrWhiteSpace(site.UsernameOverride) ? sourceDefinition?.CentralUsername ?? string.Empty : site.UsernameOverride;
|
||||
var password = string.IsNullOrWhiteSpace(site.PasswordOverride) ? sourceDefinition?.CentralPassword ?? string.Empty : site.PasswordOverride;
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
throw new InvalidOperationException("Fuer SAP sind weder zentrale Zugangsdaten noch Standort-Overrides gesetzt.");
|
||||
|
||||
await _appEventLogService.WriteAsync("SAP", "Refresh aus UI gestartet", siteId: site.Id, land: site.Land, details: serviceUrl);
|
||||
var entitySets = await _sapGatewayService.GetEntitySetsAsync(serviceUrl, username.Trim(), password.Trim());
|
||||
await _appEventLogService.WriteAsync("SAP", "Refresh aus UI erfolgreich", siteId: site.Id, land: site.Land, details: $"EntitySets={entitySets.Count}");
|
||||
|
||||
return new SapEntitySetRefreshResult
|
||||
{
|
||||
EntitySets = entitySets,
|
||||
RefreshedAtUtc = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<SapSourceFieldRefreshResult> RefreshSapSourceFieldsAsync(Site site, List<SapSourceDefinition> sapSources, List<SapFieldMapping> sapMappings)
|
||||
{
|
||||
var activeSources = sapSources
|
||||
.Where(s => s.IsActive && !string.IsNullOrWhiteSpace(s.Alias) && !string.IsNullOrWhiteSpace(s.EntitySet))
|
||||
.OrderBy(s => s.SortOrder)
|
||||
.ThenBy(s => s.Id)
|
||||
.ToList();
|
||||
|
||||
if (activeSources.Count == 0)
|
||||
throw new InvalidOperationException("Es gibt keine aktiven SAP-Quellen mit Alias und Entity Set.");
|
||||
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var sourceDefinition = await db.SourceSystemDefinitions.OrderBy(x => x.Id).FirstOrDefaultAsync(x => x.Code == site.SourceSystem);
|
||||
var serviceUrl = string.IsNullOrWhiteSpace(site.SapServiceUrl) ? sourceDefinition?.CentralServiceUrl ?? string.Empty : site.SapServiceUrl;
|
||||
if (string.IsNullOrWhiteSpace(serviceUrl))
|
||||
throw new InvalidOperationException("Es ist weder eine zentrale SAP Service URL noch ein Standort-Override gesetzt.");
|
||||
|
||||
var username = string.IsNullOrWhiteSpace(site.UsernameOverride) ? sourceDefinition?.CentralUsername ?? string.Empty : site.UsernameOverride;
|
||||
var password = string.IsNullOrWhiteSpace(site.PasswordOverride) ? sourceDefinition?.CentralPassword ?? string.Empty : site.PasswordOverride;
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
throw new InvalidOperationException("Fuer SAP sind weder zentrale Zugangsdaten noch Standort-Overrides gesetzt.");
|
||||
|
||||
var expressions = new List<string> { "=SAP" };
|
||||
var sourceFieldMap = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var source in activeSources)
|
||||
{
|
||||
var fieldNames = await _sapGatewayService.GetEntityFieldNamesAsync(serviceUrl, source.EntitySet, username.Trim(), password.Trim());
|
||||
sourceFieldMap[source.Alias] = fieldNames;
|
||||
expressions.AddRange(fieldNames.Select(field => $"{source.Alias}.{field}"));
|
||||
}
|
||||
|
||||
foreach (var current in sapMappings.Select(m => m.SourceExpression).Where(x => !string.IsNullOrWhiteSpace(x)))
|
||||
{
|
||||
if (!expressions.Contains(current, StringComparer.OrdinalIgnoreCase))
|
||||
expressions.Add(current);
|
||||
}
|
||||
|
||||
return new SapSourceFieldRefreshResult
|
||||
{
|
||||
SourceFieldMap = sourceFieldMap,
|
||||
SourceExpressions = expressions
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<DateTime> ValidateManualImportPathAsync(string manualImportFilePath)
|
||||
{
|
||||
var trimmedPath = manualImportFilePath.Trim();
|
||||
if (string.IsNullOrWhiteSpace(trimmedPath))
|
||||
throw new InvalidOperationException("Bitte zuerst einen Dateipfad eintragen.");
|
||||
if (!string.Equals(Path.GetExtension(trimmedPath), ".xlsx", StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidOperationException("Bitte eine Excel-Datei mit Endung .xlsx angeben.");
|
||||
|
||||
if (File.Exists(trimmedPath))
|
||||
return File.GetLastWriteTimeUtc(trimmedPath);
|
||||
|
||||
if (!LooksLikeSharePointReference(trimmedPath))
|
||||
throw new InvalidOperationException($"Datei nicht gefunden oder nicht erreichbar: {trimmedPath}");
|
||||
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var spConfig = await db.SharePointConfigs.FirstOrDefaultAsync();
|
||||
if (spConfig is null ||
|
||||
string.IsNullOrWhiteSpace(spConfig.TenantId) ||
|
||||
string.IsNullOrWhiteSpace(spConfig.ClientId) ||
|
||||
string.IsNullOrWhiteSpace(spConfig.ClientSecret) ||
|
||||
string.IsNullOrWhiteSpace(spConfig.SiteUrl))
|
||||
{
|
||||
throw new InvalidOperationException("Fuer SharePoint-Pruefung fehlt eine vollstaendige SharePoint-Konfiguration in Settings.");
|
||||
}
|
||||
|
||||
var tempPath = await _sharePointService.DownloadToTempFileAsync(
|
||||
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret, spConfig.SiteUrl, trimmedPath);
|
||||
try
|
||||
{
|
||||
return File.GetLastWriteTimeUtc(tempPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempPath))
|
||||
File.Delete(tempPath);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyServer(HanaServer target, HanaServer source)
|
||||
{
|
||||
target.SourceSystem = source.SourceSystem;
|
||||
target.Name = source.Name;
|
||||
target.Host = source.Host;
|
||||
target.Port = source.Port;
|
||||
target.Username = string.Empty;
|
||||
target.Password = string.Empty;
|
||||
target.DatabaseName = source.DatabaseName;
|
||||
target.UseSsl = source.UseSsl;
|
||||
target.ValidateCertificate = source.ValidateCertificate;
|
||||
target.AdditionalParams = source.AdditionalParams;
|
||||
}
|
||||
|
||||
private static void ApplySite(Site target, Site source)
|
||||
{
|
||||
target.HanaServerId = source.HanaServerId;
|
||||
target.Schema = source.Schema;
|
||||
target.TSC = source.TSC;
|
||||
target.Land = source.Land;
|
||||
target.SourceSystem = source.SourceSystem;
|
||||
target.UsernameOverride = source.UsernameOverride;
|
||||
target.PasswordOverride = source.PasswordOverride;
|
||||
target.LocalExportFolderOverride = source.LocalExportFolderOverride;
|
||||
target.ManualImportFilePath = source.ManualImportFilePath;
|
||||
target.ManualImportLastUploadedAtUtc = source.ManualImportLastUploadedAtUtc;
|
||||
target.SapServiceUrl = source.SapServiceUrl;
|
||||
target.SapEntitySet = source.SapEntitySet;
|
||||
target.SapEntitySetsCache = source.SapEntitySetsCache;
|
||||
target.SapEntitySetsRefreshedAtUtc = source.SapEntitySetsRefreshedAtUtc;
|
||||
target.IsActive = source.IsActive;
|
||||
}
|
||||
|
||||
private static List<string> ParseSapEntitySets(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return [];
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<List<string>>(json) ?? [];
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static bool LooksLikeSharePointReference(string path)
|
||||
=> path.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
|
||||
path.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ||
|
||||
path.StartsWith("/Shared Documents/", StringComparison.OrdinalIgnoreCase) ||
|
||||
path.StartsWith("Shared Documents/", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static void NormalizeSapConfigCollections(List<SapSourceDefinition> sapSources, List<SapJoinDefinition> sapJoins, List<SapFieldMapping> sapMappings)
|
||||
{
|
||||
for (var i = 0; i < sapSources.Count; i++)
|
||||
sapSources[i].SortOrder = i;
|
||||
for (var i = 0; i < sapJoins.Count; i++)
|
||||
sapJoins[i].SortOrder = i;
|
||||
for (var i = 0; i < sapMappings.Count; i++)
|
||||
sapMappings[i].SortOrder = i;
|
||||
|
||||
var selectedPrimaryIndex = sapSources.FindIndex(s => s.IsPrimary);
|
||||
var primarySource = selectedPrimaryIndex >= 0 ? sapSources[selectedPrimaryIndex] : sapSources.FirstOrDefault();
|
||||
foreach (var source in sapSources)
|
||||
source.IsPrimary = primarySource is not null && ReferenceEquals(source, primarySource);
|
||||
if (sapSources.Count > 0 && sapSources.All(s => !s.IsPrimary))
|
||||
sapSources[0].IsPrimary = true;
|
||||
}
|
||||
|
||||
private static async Task SaveSapConfigurationAsync(AppDbContext db, int siteId, bool isSapSite, List<SapSourceDefinition> sapSources, List<SapJoinDefinition> sapJoins, List<SapFieldMapping> sapMappings)
|
||||
{
|
||||
var oldSources = await db.SapSourceDefinitions.Where(s => s.SiteId == siteId).ToListAsync();
|
||||
var oldJoins = await db.SapJoinDefinitions.Where(j => j.SiteId == siteId).ToListAsync();
|
||||
var oldMappings = await db.SapFieldMappings.Where(m => m.SiteId == siteId).ToListAsync();
|
||||
if (oldSources.Count > 0) db.SapSourceDefinitions.RemoveRange(oldSources);
|
||||
if (oldJoins.Count > 0) db.SapJoinDefinitions.RemoveRange(oldJoins);
|
||||
if (oldMappings.Count > 0) db.SapFieldMappings.RemoveRange(oldMappings);
|
||||
|
||||
if (isSapSite)
|
||||
{
|
||||
NormalizeSapConfigCollections(sapSources, sapJoins, sapMappings);
|
||||
foreach (var source in sapSources) source.SiteId = siteId;
|
||||
foreach (var join in sapJoins) join.SiteId = siteId;
|
||||
foreach (var mapping in sapMappings) mapping.SiteId = siteId;
|
||||
db.SapSourceDefinitions.AddRange(sapSources);
|
||||
db.SapJoinDefinitions.AddRange(sapJoins);
|
||||
db.SapFieldMappings.AddRange(sapMappings);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static async Task<int> ResolveCentralHanaServerIdAsync(AppDbContext db, Site site)
|
||||
{
|
||||
site.UsernameOverride = site.UsernameOverride.Trim();
|
||||
site.PasswordOverride = site.PasswordOverride.Trim();
|
||||
site.LocalExportFolderOverride = site.LocalExportFolderOverride.Trim();
|
||||
site.ManualImportFilePath = site.ManualImportFilePath.Trim();
|
||||
site.SapServiceUrl = site.SapServiceUrl.Trim();
|
||||
site.SapEntitySet = site.SapEntitySet.Trim();
|
||||
|
||||
var normalizedSourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) ? string.Empty : site.SourceSystem.Trim().ToUpperInvariant();
|
||||
var centralServer = await db.HanaServers.OrderBy(x => x.Id).FirstOrDefaultAsync(x => x.SourceSystem == normalizedSourceSystem);
|
||||
if (centralServer is null || string.IsNullOrWhiteSpace(centralServer.Host))
|
||||
throw new InvalidOperationException($"Fuer Quellsystem '{normalizedSourceSystem}' ist keine gueltige zentrale HANA-Konfiguration vorhanden.");
|
||||
|
||||
return centralServer.Id;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class StandortePageState
|
||||
{
|
||||
public List<SourceSystemDefinition> SourceSystems { get; set; } = [];
|
||||
public List<HanaServer> Servers { get; set; } = [];
|
||||
public List<Site> Sites { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class StandortEditorState
|
||||
{
|
||||
public Site Site { get; set; } = new();
|
||||
public List<string> SapEntitySets { get; set; } = [];
|
||||
public List<SapSourceDefinition> SapSources { get; set; } = [];
|
||||
public List<SapJoinDefinition> SapJoins { get; set; } = [];
|
||||
public List<SapFieldMapping> SapMappings { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class SapEntitySetRefreshResult
|
||||
{
|
||||
public List<string> EntitySets { get; set; } = [];
|
||||
public DateTime RefreshedAtUtc { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SapSourceFieldRefreshResult
|
||||
{
|
||||
public List<string> SourceExpressions { get; set; } = [];
|
||||
public Dictionary<string, List<string>> SourceFieldMap { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface IStandorteSapEditorService
|
||||
{
|
||||
void AddSapSource(List<SapSourceDefinition> sapSources, List<string> sapEntitySetsCache);
|
||||
void RemoveSapSource(List<SapSourceDefinition> sapSources, SapSourceDefinition source);
|
||||
void AddSapJoin(List<SapJoinDefinition> sapJoins);
|
||||
SapAutoMatchResult AutoMatchSapJoins(List<SapSourceDefinition> sapSources, List<SapJoinDefinition> sapJoins, Dictionary<string, List<string>> sapSourceFieldMap);
|
||||
void RemoveSapJoin(List<SapJoinDefinition> sapJoins, SapJoinDefinition join);
|
||||
void AddSapMapping(List<SapFieldMapping> sapMappings, IReadOnlyList<string> salesRecordFields, List<string> sapAvailableSourceExpressions);
|
||||
void RemoveSapMapping(List<SapFieldMapping> sapMappings, SapFieldMapping mapping);
|
||||
List<string> BuildSourceExpressionsFromMappings(List<SapFieldMapping> sapMappings);
|
||||
Dictionary<string, List<string>> BuildSourceFieldMapFromJoins(List<SapJoinDefinition> sapJoins);
|
||||
IEnumerable<string> GetSapAliases(List<SapSourceDefinition> sapSources);
|
||||
IEnumerable<string> GetAvailableSourceExpressions(List<string> sapAvailableSourceExpressions, string? currentValue);
|
||||
IEnumerable<string> GetAvailableJoinFields(Dictionary<string, List<string>> sapSourceFieldMap, string? alias, string? currentKeys);
|
||||
void NormalizeSapConfigCollections(List<SapSourceDefinition> sapSources, List<SapJoinDefinition> sapJoins, List<SapFieldMapping> sapMappings);
|
||||
}
|
||||
|
||||
public sealed class StandorteSapEditorService : IStandorteSapEditorService
|
||||
{
|
||||
public void AddSapSource(List<SapSourceDefinition> sapSources, List<string> sapEntitySetsCache)
|
||||
{
|
||||
sapSources.Add(new SapSourceDefinition
|
||||
{
|
||||
Alias = $"SRC{sapSources.Count + 1}",
|
||||
EntitySet = sapEntitySetsCache.FirstOrDefault() ?? string.Empty,
|
||||
IsActive = true,
|
||||
IsPrimary = sapSources.Count == 0,
|
||||
SortOrder = sapSources.Count
|
||||
});
|
||||
}
|
||||
|
||||
public void RemoveSapSource(List<SapSourceDefinition> sapSources, SapSourceDefinition source)
|
||||
=> sapSources.Remove(source);
|
||||
|
||||
public void AddSapJoin(List<SapJoinDefinition> sapJoins)
|
||||
{
|
||||
sapJoins.Add(new SapJoinDefinition
|
||||
{
|
||||
JoinType = "Left",
|
||||
IsActive = true,
|
||||
SortOrder = sapJoins.Count
|
||||
});
|
||||
}
|
||||
|
||||
public SapAutoMatchResult AutoMatchSapJoins(List<SapSourceDefinition> sapSources, List<SapJoinDefinition> sapJoins, Dictionary<string, List<string>> sapSourceFieldMap)
|
||||
{
|
||||
var activeSources = sapSources
|
||||
.Where(s => s.IsActive && !string.IsNullOrWhiteSpace(s.Alias))
|
||||
.OrderBy(s => s.SortOrder)
|
||||
.ThenBy(s => s.Id)
|
||||
.ToList();
|
||||
|
||||
if (activeSources.Count < 2)
|
||||
return SapAutoMatchResult.WarningResult("Fuer Auto-Match werden mindestens zwei aktive SAP-Quellen benoetigt.");
|
||||
|
||||
if (sapSourceFieldMap.Count == 0)
|
||||
return SapAutoMatchResult.WarningResult("Bitte zuerst 'Felder aus Quellen laden' ausfuehren.");
|
||||
|
||||
var primary = activeSources.FirstOrDefault(s => s.IsPrimary) ?? activeSources.First();
|
||||
var createdOrUpdated = 0;
|
||||
|
||||
foreach (var source in activeSources.Where(s => !string.Equals(s.Alias, primary.Alias, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
if (!sapSourceFieldMap.TryGetValue(primary.Alias, out var leftFields) || leftFields.Count == 0)
|
||||
continue;
|
||||
if (!sapSourceFieldMap.TryGetValue(source.Alias, out var rightFields) || rightFields.Count == 0)
|
||||
continue;
|
||||
|
||||
var matchingFields = leftFields
|
||||
.Intersect(rightFields, StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (matchingFields.Count == 0)
|
||||
continue;
|
||||
|
||||
var existingJoin = sapJoins.FirstOrDefault(j =>
|
||||
string.Equals(j.LeftAlias, primary.Alias, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(j.RightAlias, source.Alias, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var keyList = string.Join(',', matchingFields);
|
||||
if (existingJoin is null)
|
||||
{
|
||||
sapJoins.Add(new SapJoinDefinition
|
||||
{
|
||||
LeftAlias = primary.Alias,
|
||||
RightAlias = source.Alias,
|
||||
LeftKeys = keyList,
|
||||
RightKeys = keyList,
|
||||
JoinType = "Left",
|
||||
IsActive = true,
|
||||
SortOrder = sapJoins.Count
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
existingJoin.LeftKeys = keyList;
|
||||
existingJoin.RightKeys = keyList;
|
||||
existingJoin.JoinType = "Left";
|
||||
existingJoin.IsActive = true;
|
||||
}
|
||||
|
||||
createdOrUpdated++;
|
||||
}
|
||||
|
||||
if (createdOrUpdated == 0)
|
||||
return SapAutoMatchResult.InfoResult("Kein passender Join-Vorschlag gefunden.");
|
||||
|
||||
NormalizeSapConfigCollections(sapSources, sapJoins, []);
|
||||
return SapAutoMatchResult.SuccessResult($"{createdOrUpdated} Join-Vorschlaege gesetzt.");
|
||||
}
|
||||
|
||||
public void RemoveSapJoin(List<SapJoinDefinition> sapJoins, SapJoinDefinition join)
|
||||
=> sapJoins.Remove(join);
|
||||
|
||||
public void AddSapMapping(List<SapFieldMapping> sapMappings, IReadOnlyList<string> salesRecordFields, List<string> sapAvailableSourceExpressions)
|
||||
{
|
||||
sapMappings.Add(new SapFieldMapping
|
||||
{
|
||||
TargetField = salesRecordFields.First(),
|
||||
SourceExpression = sapAvailableSourceExpressions.FirstOrDefault() ?? "=SAP",
|
||||
IsActive = true,
|
||||
SortOrder = sapMappings.Count
|
||||
});
|
||||
}
|
||||
|
||||
public void RemoveSapMapping(List<SapFieldMapping> sapMappings, SapFieldMapping mapping)
|
||||
=> sapMappings.Remove(mapping);
|
||||
|
||||
public List<string> BuildSourceExpressionsFromMappings(List<SapFieldMapping> sapMappings)
|
||||
=> sapMappings
|
||||
.Select(m => m.SourceExpression)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
public Dictionary<string, List<string>> BuildSourceFieldMapFromJoins(List<SapJoinDefinition> sapJoins)
|
||||
{
|
||||
var result = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var join in sapJoins)
|
||||
{
|
||||
AddJoinKeysToFieldMap(result, join.LeftAlias, join.LeftKeys);
|
||||
AddJoinKeysToFieldMap(result, join.RightAlias, join.RightKeys);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetSapAliases(List<SapSourceDefinition> sapSources)
|
||||
=> sapSources.Where(s => !string.IsNullOrWhiteSpace(s.Alias)).Select(s => s.Alias).Distinct(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public IEnumerable<string> GetAvailableSourceExpressions(List<string> sapAvailableSourceExpressions, string? currentValue)
|
||||
{
|
||||
var expressions = new List<string>(sapAvailableSourceExpressions);
|
||||
if (!string.IsNullOrWhiteSpace(currentValue) && !expressions.Contains(currentValue, StringComparer.OrdinalIgnoreCase))
|
||||
expressions.Insert(0, currentValue);
|
||||
|
||||
return expressions;
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetAvailableJoinFields(Dictionary<string, List<string>> sapSourceFieldMap, string? alias, string? currentKeys)
|
||||
{
|
||||
var values = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(alias) && sapSourceFieldMap.TryGetValue(alias, out var fields))
|
||||
values.AddRange(fields);
|
||||
|
||||
foreach (var key in GetSelectedJoinKeys(currentKeys))
|
||||
{
|
||||
if (!values.Contains(key, StringComparer.OrdinalIgnoreCase))
|
||||
values.Add(key);
|
||||
}
|
||||
|
||||
return values
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public void NormalizeSapConfigCollections(List<SapSourceDefinition> sapSources, List<SapJoinDefinition> sapJoins, List<SapFieldMapping> sapMappings)
|
||||
{
|
||||
for (var i = 0; i < sapSources.Count; i++)
|
||||
sapSources[i].SortOrder = i;
|
||||
for (var i = 0; i < sapJoins.Count; i++)
|
||||
sapJoins[i].SortOrder = i;
|
||||
for (var i = 0; i < sapMappings.Count; i++)
|
||||
sapMappings[i].SortOrder = i;
|
||||
|
||||
var selectedPrimaryIndex = sapSources.FindIndex(s => s.IsPrimary);
|
||||
var primarySource = selectedPrimaryIndex >= 0 ? sapSources[selectedPrimaryIndex] : sapSources.FirstOrDefault();
|
||||
foreach (var source in sapSources)
|
||||
source.IsPrimary = primarySource is not null && ReferenceEquals(source, primarySource);
|
||||
if (sapSources.Count > 0 && sapSources.All(s => !s.IsPrimary))
|
||||
sapSources[0].IsPrimary = true;
|
||||
}
|
||||
|
||||
private static void AddJoinKeysToFieldMap(Dictionary<string, List<string>> target, string alias, string keys)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(alias))
|
||||
return;
|
||||
|
||||
if (!target.TryGetValue(alias, out var fields))
|
||||
{
|
||||
fields = [];
|
||||
target[alias] = fields;
|
||||
}
|
||||
|
||||
foreach (var key in GetSelectedJoinKeys(keys))
|
||||
{
|
||||
if (!fields.Contains(key, StringComparer.OrdinalIgnoreCase))
|
||||
fields.Add(key);
|
||||
}
|
||||
|
||||
fields.Sort(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static HashSet<string> GetSelectedJoinKeys(string? keys)
|
||||
=> keys?
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase)
|
||||
?? [];
|
||||
}
|
||||
|
||||
public sealed class SapAutoMatchResult
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
public bool Warning { get; init; }
|
||||
public bool Info { get; init; }
|
||||
public string Message { get; init; } = string.Empty;
|
||||
|
||||
public static SapAutoMatchResult WarningResult(string message) => new() { Warning = true, Message = message };
|
||||
public static SapAutoMatchResult InfoResult(string message) => new() { Info = true, Message = message };
|
||||
public static SapAutoMatchResult SuccessResult(string message) => new() { Success = true, Message = message };
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrafagSalesExporter.Data;
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface ITransformationsPageService
|
||||
{
|
||||
Task<TransformationsPageState> LoadAsync();
|
||||
Task<List<FieldTransformationRule>> SaveAllAsync(List<FieldTransformationRule> rules);
|
||||
}
|
||||
|
||||
public sealed class TransformationsPageService : ITransformationsPageService
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||
|
||||
public TransformationsPageService(IDbContextFactory<AppDbContext> dbFactory)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
}
|
||||
|
||||
public async Task<TransformationsPageState> LoadAsync()
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var rules = await db.FieldTransformationRules.OrderBy(r => r.SortOrder).ThenBy(r => r.Id).ToListAsync();
|
||||
|
||||
foreach (var rule in rules)
|
||||
rule.RuleScope = string.IsNullOrWhiteSpace(rule.RuleScope) ? "Value" : rule.RuleScope;
|
||||
|
||||
return new TransformationsPageState
|
||||
{
|
||||
SourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync(),
|
||||
Rules = rules
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<List<FieldTransformationRule>> SaveAllAsync(List<FieldTransformationRule> rules)
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
db.FieldTransformationRules.RemoveRange(db.FieldTransformationRules);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
db.FieldTransformationRules.AddRange(rules);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return await db.FieldTransformationRules.OrderBy(r => r.SortOrder).ThenBy(r => r.Id).ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TransformationsPageState
|
||||
{
|
||||
public List<FieldTransformationRule> Rules { get; set; } = [];
|
||||
public List<SourceSystemDefinition> SourceSystems { get; set; } = [];
|
||||
}
|
||||
Reference in New Issue
Block a user