umfangreiches refactoring
This commit is contained in:
@@ -1,9 +1,7 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
@using Microsoft.EntityFrameworkCore
|
|
||||||
@using System.Diagnostics
|
@using System.Diagnostics
|
||||||
@using TrafagSalesExporter.Data
|
|
||||||
@using TrafagSalesExporter.Services
|
@using TrafagSalesExporter.Services
|
||||||
@inject IDbContextFactory<AppDbContext> DbFactory
|
@inject IDashboardPageService DashboardPageActions
|
||||||
@inject ExportOrchestrationService Orchestrator
|
@inject ExportOrchestrationService Orchestrator
|
||||||
@inject TimerBackgroundService TimerService
|
@inject TimerBackgroundService TimerService
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@@ -170,49 +168,9 @@
|
|||||||
private async Task LoadDataAsync()
|
private async Task LoadDataAsync()
|
||||||
{
|
{
|
||||||
_loading = true;
|
_loading = true;
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
var state = await DashboardPageActions.LoadAsync();
|
||||||
|
_dashboardRows = state.DashboardRows;
|
||||||
var sites = await db.Sites.Include(s => s.HanaServer).Where(s => s.IsActive).ToListAsync();
|
_consolidatedRows = state.ConsolidatedRows;
|
||||||
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());
|
|
||||||
|
|
||||||
_dashboardRows = 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 ?? "",
|
|
||||||
LastStatus = log?.Status ?? "",
|
|
||||||
RowCount = log?.RowCount ?? 0,
|
|
||||||
LastRun = log?.Timestamp,
|
|
||||||
DurationSeconds = log?.DurationSeconds ?? 0,
|
|
||||||
ErrorMessage = log?.ErrorMessage ?? "",
|
|
||||||
FilePath = log?.FilePath ?? "",
|
|
||||||
LiveMessage = appLog is null ? string.Empty : $"{appLog.Category}: {appLog.Message}",
|
|
||||||
LiveDetails = appLog?.Details ?? ""
|
|
||||||
};
|
|
||||||
}).ToList();
|
|
||||||
|
|
||||||
_consolidatedRows = BuildConsolidatedRows(settings: await db.ExportSettings.FirstOrDefaultAsync() ?? new());
|
|
||||||
|
|
||||||
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || Orchestrator.IsConsolidatedExporting();
|
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || Orchestrator.IsConsolidatedExporting();
|
||||||
_loading = false;
|
_loading = false;
|
||||||
@@ -321,15 +279,6 @@
|
|||||||
OpenFile(row.FilePath);
|
OpenFile(row.FilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 void OpenFile(string filePath)
|
private void OpenFile(string filePath)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
|
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
|
||||||
@@ -417,63 +366,6 @@
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DashboardRow
|
|
||||||
{
|
|
||||||
public int SiteId { get; set; }
|
|
||||||
public string Land { get; set; } = "";
|
|
||||||
public string TSC { get; set; } = "";
|
|
||||||
public string Schema { get; set; } = "";
|
|
||||||
public string ServerName { get; set; } = "";
|
|
||||||
public string LastStatus { get; set; } = "";
|
|
||||||
public int RowCount { get; set; }
|
|
||||||
public DateTime? LastRun { get; set; }
|
|
||||||
public double DurationSeconds { get; set; }
|
|
||||||
public string ErrorMessage { get; set; } = "";
|
|
||||||
public string FilePath { get; set; } = "";
|
|
||||||
public string LiveMessage { get; set; } = "";
|
|
||||||
public string LiveDetails { get; set; } = "";
|
|
||||||
public bool HasOpenableFile => !string.IsNullOrWhiteSpace(FilePath) && File.Exists(FilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ConsolidatedDashboardRow
|
|
||||||
{
|
|
||||||
public string Label { get; set; } = "";
|
|
||||||
public string FilePath { get; set; } = "";
|
|
||||||
public string DisplayPath { get; set; } = "";
|
|
||||||
public DateTime? LastModified { get; set; }
|
|
||||||
public bool HasOpenableFile => !string.IsNullOrWhiteSpace(FilePath) && File.Exists(FilePath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
@page "/logs"
|
@page "/logs"
|
||||||
@using Microsoft.EntityFrameworkCore
|
@using TrafagSalesExporter.Services
|
||||||
@using TrafagSalesExporter.Data
|
@inject ILogsPageService LogsPageActions
|
||||||
@inject IDbContextFactory<AppDbContext> DbFactory
|
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@inject IDialogService DialogService
|
@inject IDialogService DialogService
|
||||||
@inject TrafagSalesExporter.Services.IUiTextService UiText
|
@inject TrafagSalesExporter.Services.IUiTextService UiText
|
||||||
@@ -117,37 +116,16 @@
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
|
||||||
_availableLands = await db.ExportLogs.Select(l => l.Land).Distinct().OrderBy(l => l).ToListAsync();
|
|
||||||
await LoadLogsAsync();
|
await LoadLogsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadLogsAsync()
|
private async Task LoadLogsAsync()
|
||||||
{
|
{
|
||||||
_loading = true;
|
_loading = true;
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
var state = await LogsPageActions.LoadAsync(_filterLand, _filterStatus, _filterDate);
|
||||||
IQueryable<ExportLog> query = db.ExportLogs.OrderByDescending(l => l.Timestamp);
|
_availableLands = state.AvailableLands;
|
||||||
|
_logs = state.Logs;
|
||||||
if (!string.IsNullOrEmpty(_filterLand))
|
_appLogs = state.AppLogs;
|
||||||
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);
|
|
||||||
|
|
||||||
_logs = await query.Take(500).ToListAsync();
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
_appLogs = await appLogQuery.Take(500).ToListAsync();
|
|
||||||
_loading = false;
|
_loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,13 +143,9 @@
|
|||||||
|
|
||||||
if (result != true) return;
|
if (result != true) return;
|
||||||
|
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
var deletedCount = await LogsPageActions.DeleteOldLogsAsync(90);
|
||||||
var cutoff = DateTime.Now.AddDays(-90);
|
|
||||||
var oldLogs = await db.ExportLogs.Where(l => l.Timestamp < cutoff).ToListAsync();
|
|
||||||
db.ExportLogs.RemoveRange(oldLogs);
|
|
||||||
var count = await db.SaveChangesAsync();
|
|
||||||
await LoadLogsAsync();
|
await LoadLogsAsync();
|
||||||
Snackbar.Add(string.Format(T("{0} alte Logs geloescht", "{0} old logs deleted"), oldLogs.Count), Severity.Info);
|
Snackbar.Add(string.Format(T("{0} alte Logs geloescht", "{0} old logs deleted"), deletedCount), Severity.Info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@page "/management-cockpit"
|
@page "/management-cockpit"
|
||||||
@using TrafagSalesExporter.Models
|
@using TrafagSalesExporter.Models
|
||||||
@using TrafagSalesExporter.Services
|
@using TrafagSalesExporter.Services
|
||||||
@inject IManagementCockpitService CockpitService
|
@inject IManagementCockpitPageService CockpitPageService
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@inject IUiTextService UiText
|
@inject IUiTextService UiText
|
||||||
|
|
||||||
@@ -335,8 +335,11 @@
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
await ReloadFiles();
|
var state = await CockpitPageService.InitializeAsync(_selectedFilePath, _selectedCentralYear);
|
||||||
await ReloadCentralYears();
|
_files = state.Files;
|
||||||
|
_centralYears = state.CentralYears;
|
||||||
|
_selectedFilePath = state.SelectedFilePath;
|
||||||
|
_selectedCentralYear = state.SelectedCentralYear;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ReloadFiles()
|
private async Task ReloadFiles()
|
||||||
@@ -344,7 +347,7 @@
|
|||||||
_loadingFiles = true;
|
_loadingFiles = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_files = await CockpitService.GetAvailableFilesAsync();
|
_files = await CockpitPageService.LoadFilesAsync();
|
||||||
_selectedFilePath ??= _files.FirstOrDefault()?.Path;
|
_selectedFilePath ??= _files.FirstOrDefault()?.Path;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -355,7 +358,7 @@
|
|||||||
|
|
||||||
private async Task ReloadCentralYears()
|
private async Task ReloadCentralYears()
|
||||||
{
|
{
|
||||||
_centralYears = await CockpitService.GetAvailableCentralYearsAsync();
|
_centralYears = await CockpitPageService.LoadCentralYearsAsync();
|
||||||
if (_selectedCentralYear == 0)
|
if (_selectedCentralYear == 0)
|
||||||
_selectedCentralYear = _centralYears.LastOrDefault();
|
_selectedCentralYear = _centralYears.LastOrDefault();
|
||||||
}
|
}
|
||||||
@@ -368,7 +371,7 @@
|
|||||||
_analyzing = true;
|
_analyzing = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_result = await CockpitService.AnalyzeAsync(_selectedFilePath);
|
_result = await CockpitPageService.AnalyzeAsync(_selectedFilePath);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -388,7 +391,7 @@
|
|||||||
_analyzingCentral = true;
|
_analyzingCentral = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_centralResult = await CockpitService.AnalyzeCentralAsync(_selectedCentralYear, _selectedCentralMonth);
|
_centralResult = await CockpitPageService.AnalyzeCentralAsync(_selectedCentralYear, _selectedCentralMonth);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
@page "/settings"
|
@page "/settings"
|
||||||
@using Microsoft.EntityFrameworkCore
|
|
||||||
@using TrafagSalesExporter.Data
|
|
||||||
@using TrafagSalesExporter.Models
|
@using TrafagSalesExporter.Models
|
||||||
@using TrafagSalesExporter.Services
|
@using TrafagSalesExporter.Services
|
||||||
@inject IDbContextFactory<AppDbContext> DbFactory
|
@inject ISettingsPageService SettingsPageActions
|
||||||
@inject ISharePointUploadService SpService
|
|
||||||
@inject TimerBackgroundService TimerService
|
|
||||||
@inject IHanaQueryService HanaService
|
|
||||||
@inject ISapGatewayService SapGatewayService
|
|
||||||
@inject IConfigTransferService ConfigTransferService
|
|
||||||
@inject IExchangeRateImportService ExchangeRateImportService
|
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
@@ -328,35 +320,16 @@
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
var state = await SettingsPageActions.LoadAsync();
|
||||||
_spConfig = await db.SharePointConfigs.FirstOrDefaultAsync() ?? new SharePointConfig();
|
_spConfig = state.SharePointConfig;
|
||||||
_exportSettings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings();
|
_exportSettings = state.ExportSettings;
|
||||||
_sourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync();
|
_sourceSystems = state.SourceSystems;
|
||||||
_exchangeRates = await db.CurrencyExchangeRates
|
_exchangeRates = state.ExchangeRates;
|
||||||
.OrderBy(x => x.FromCurrency)
|
|
||||||
.ThenBy(x => x.ToCurrency)
|
|
||||||
.ThenByDescending(x => x.ValidFrom)
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveSharePoint()
|
private async Task SaveSharePoint()
|
||||||
{
|
{
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
await SettingsPageActions.SaveSharePointAsync(_spConfig);
|
||||||
var existing = await db.SharePointConfigs.FirstOrDefaultAsync();
|
|
||||||
if (existing is null)
|
|
||||||
{
|
|
||||||
db.SharePointConfigs.Add(_spConfig);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
existing.SiteUrl = _spConfig.SiteUrl;
|
|
||||||
existing.ExportFolder = _spConfig.ExportFolder;
|
|
||||||
existing.CentralExportFolder = _spConfig.CentralExportFolder;
|
|
||||||
existing.TenantId = _spConfig.TenantId;
|
|
||||||
existing.ClientId = _spConfig.ClientId;
|
|
||||||
existing.ClientSecret = _spConfig.ClientSecret;
|
|
||||||
}
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
Snackbar.Add("SharePoint Konfiguration gespeichert", Severity.Success);
|
Snackbar.Add("SharePoint Konfiguration gespeichert", Severity.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,15 +338,7 @@
|
|||||||
_testingSp = true;
|
_testingSp = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var tenantId = NormalizeConfigValue(_spConfig.TenantId);
|
_sharePointTestPreview = await SettingsPageActions.BuildSharePointTestPreviewAsync(_spConfig);
|
||||||
var clientId = NormalizeConfigValue(_spConfig.ClientId);
|
|
||||||
var clientSecret = NormalizeConfigValue(_spConfig.ClientSecret);
|
|
||||||
var siteUrl = NormalizeConfigValue(_spConfig.SiteUrl);
|
|
||||||
|
|
||||||
_sharePointTestPreview = BuildSharePointTestPreview(tenantId, clientId, clientSecret, siteUrl);
|
|
||||||
|
|
||||||
await SpService.TestConnectionAsync(
|
|
||||||
tenantId, clientId, clientSecret, siteUrl);
|
|
||||||
Snackbar.Add("SharePoint Verbindung erfolgreich!", Severity.Success);
|
Snackbar.Add("SharePoint Verbindung erfolgreich!", Severity.Success);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -388,24 +353,7 @@
|
|||||||
|
|
||||||
private async Task SaveExportSettings()
|
private async Task SaveExportSettings()
|
||||||
{
|
{
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
await SettingsPageActions.SaveExportSettingsAsync(_exportSettings);
|
||||||
var existing = await db.ExportSettings.FirstOrDefaultAsync();
|
|
||||||
if (existing is null)
|
|
||||||
{
|
|
||||||
db.ExportSettings.Add(_exportSettings);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
existing.DateFilter = _exportSettings.DateFilter;
|
|
||||||
existing.TimerHour = _exportSettings.TimerHour;
|
|
||||||
existing.TimerMinute = _exportSettings.TimerMinute;
|
|
||||||
existing.TimerEnabled = _exportSettings.TimerEnabled;
|
|
||||||
existing.DebugLoggingEnabled = _exportSettings.DebugLoggingEnabled;
|
|
||||||
existing.LocalSiteExportFolder = _exportSettings.LocalSiteExportFolder;
|
|
||||||
existing.LocalConsolidatedExportFolder = _exportSettings.LocalConsolidatedExportFolder;
|
|
||||||
}
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
TimerService.Recalculate();
|
|
||||||
Snackbar.Add("Export Einstellungen gespeichert", Severity.Success);
|
Snackbar.Add("Export Einstellungen gespeichert", Severity.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,47 +441,16 @@
|
|||||||
|
|
||||||
private async Task SaveSourceSystems()
|
private async Task SaveSourceSystems()
|
||||||
{
|
{
|
||||||
var normalized = _sourceSystems
|
try
|
||||||
.Select(x => new SourceSystemDefinition
|
|
||||||
{
|
{
|
||||||
Id = x.Id,
|
_sourceSystems = await SettingsPageActions.SaveSourceSystemsAsync(_sourceSystems);
|
||||||
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)))
|
|
||||||
{
|
|
||||||
Snackbar.Add("Jedes Quellsystem braucht einen Anzeigenamen.", Severity.Warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var duplicates = normalized
|
|
||||||
.GroupBy(x => x.Code)
|
|
||||||
.FirstOrDefault(g => g.Count() > 1);
|
|
||||||
if (duplicates is not null)
|
|
||||||
{
|
|
||||||
Snackbar.Add($"Quellsystem-Code doppelt vorhanden: {duplicates.Key}", Severity.Warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
_sourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync();
|
|
||||||
Snackbar.Add("Quellsysteme gespeichert", Severity.Success);
|
Snackbar.Add("Quellsysteme gespeichert", Severity.Success);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add(ex.Message, Severity.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void AddExchangeRate()
|
private void AddExchangeRate()
|
||||||
{
|
{
|
||||||
@@ -554,32 +471,7 @@
|
|||||||
|
|
||||||
private async Task SaveExchangeRates()
|
private async Task SaveExchangeRates()
|
||||||
{
|
{
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
_exchangeRates = await SettingsPageActions.SaveExchangeRatesAsync(_exchangeRates);
|
||||||
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();
|
|
||||||
|
|
||||||
_exchangeRates = await db.CurrencyExchangeRates
|
|
||||||
.OrderBy(x => x.FromCurrency)
|
|
||||||
.ThenBy(x => x.ToCurrency)
|
|
||||||
.ThenByDescending(x => x.ValidFrom)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
Snackbar.Add("Wechselkurse gespeichert", Severity.Success);
|
Snackbar.Add("Wechselkurse gespeichert", Severity.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,8 +483,8 @@
|
|||||||
_refreshingExchangeRates = true;
|
_refreshingExchangeRates = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await ExchangeRateImportService.RefreshEcbRatesAsync();
|
var result = await SettingsPageActions.RefreshEcbRatesAsync();
|
||||||
_exchangeRates = await LoadExchangeRatesAsync();
|
_exchangeRates = result.ExchangeRates;
|
||||||
Snackbar.Add($"ECB-Kurse aktualisiert: {result.ImportedCount} Kurse vom {result.RateDate:yyyy-MM-dd}.", Severity.Success);
|
Snackbar.Add($"ECB-Kurse aktualisiert: {result.ImportedCount} Kurse vom {result.RateDate:yyyy-MM-dd}.", Severity.Success);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -613,7 +505,7 @@
|
|||||||
_exportingConfig = true;
|
_exportingConfig = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var json = await ConfigTransferService.ExportJsonAsync(_includeSecretsInExport);
|
var json = await SettingsPageActions.ExportConfigurationAsync(_includeSecretsInExport);
|
||||||
var suffix = _includeSecretsInExport ? "with-secrets" : "without-secrets";
|
var suffix = _includeSecretsInExport ? "with-secrets" : "without-secrets";
|
||||||
var fileName = $"trafag-config-{DateTime.UtcNow:yyyyMMdd-HHmmss}-{suffix}.json";
|
var fileName = $"trafag-config-{DateTime.UtcNow:yyyyMMdd-HHmmss}-{suffix}.json";
|
||||||
await JS.InvokeVoidAsync("trafagDownload.saveTextFile", fileName, json, "application/json;charset=utf-8");
|
await JS.InvokeVoidAsync("trafagDownload.saveTextFile", fileName, json, "application/json;charset=utf-8");
|
||||||
@@ -641,18 +533,11 @@
|
|||||||
await using var stream = file.OpenReadStream(5 * 1024 * 1024);
|
await using var stream = file.OpenReadStream(5 * 1024 * 1024);
|
||||||
using var reader = new StreamReader(stream);
|
using var reader = new StreamReader(stream);
|
||||||
var json = await reader.ReadToEndAsync();
|
var json = await reader.ReadToEndAsync();
|
||||||
await ConfigTransferService.ImportJsonAsync(json);
|
var state = await SettingsPageActions.ImportConfigurationAsync(json);
|
||||||
|
_spConfig = state.SharePointConfig;
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
_exportSettings = state.ExportSettings;
|
||||||
_spConfig = await db.SharePointConfigs.FirstOrDefaultAsync() ?? new SharePointConfig();
|
_sourceSystems = state.SourceSystems;
|
||||||
_exportSettings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings();
|
_exchangeRates = state.ExchangeRates;
|
||||||
_sourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync();
|
|
||||||
_exchangeRates = await db.CurrencyExchangeRates
|
|
||||||
.OrderBy(x => x.FromCurrency)
|
|
||||||
.ThenBy(x => x.ToCurrency)
|
|
||||||
.ThenByDescending(x => x.ValidFrom)
|
|
||||||
.ToListAsync();
|
|
||||||
TimerService.Recalculate();
|
|
||||||
Snackbar.Add("Konfiguration importiert", Severity.Success);
|
Snackbar.Add("Konfiguration importiert", Severity.Success);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -674,70 +559,13 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
await TestCentralSapCredentials(definition);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(definition.ConnectionKind, SourceSystemConnectionKinds.Hana, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
await TestCentralHanaCredentials(definition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task TestCentralHanaCredentials(SourceSystemDefinition definition)
|
|
||||||
{
|
|
||||||
var sourceSystem = definition.Code;
|
|
||||||
if (!_testingSystems.Add(sourceSystem))
|
if (!_testingSystems.Add(sourceSystem))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var username = definition.CentralUsername;
|
var result = await SettingsPageActions.TestCentralCredentialsAsync(definition);
|
||||||
var password = definition.CentralPassword;
|
Snackbar.Add(result.Message, result.Success ? Severity.Success : result.Warning ? Severity.Warning : Severity.Error);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
|
||||||
{
|
|
||||||
Snackbar.Add($"Für {sourceSystem} sind keine zentralen Zugangsdaten gepflegt.", Severity.Warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
{
|
|
||||||
Snackbar.Add($"Keine zentrale HANA-Konfiguration fuer {sourceSystem} gefunden.", Severity.Warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
if (result.Success)
|
|
||||||
{
|
|
||||||
Snackbar.Add($"{sourceSystem}: Zentrale HANA-Verbindung erfolgreich.", Severity.Success);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Snackbar.Add($"{sourceSystem}: {result.ExceptionType} - {result.ErrorMessage}", Severity.Error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -745,48 +573,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task TestCentralSapCredentials(SourceSystemDefinition definition)
|
private static string NormalizeSourceSystemCode(string? code) => Services.SettingsPageService.NormalizeSourceSystemCode(code);
|
||||||
{
|
|
||||||
var sourceSystem = definition.Code;
|
|
||||||
if (!_testingSystems.Add(sourceSystem))
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
private static string NormalizeConnectionKind(string? connectionKind) => Services.SettingsPageService.NormalizeConnectionKind(connectionKind);
|
||||||
{
|
|
||||||
var username = definition.CentralUsername;
|
|
||||||
var password = definition.CentralPassword;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
|
||||||
{
|
|
||||||
Snackbar.Add("Für SAP sind keine zentralen Gateway-Zugangsdaten gepflegt.", Severity.Warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(definition.CentralServiceUrl))
|
|
||||||
{
|
|
||||||
Snackbar.Add($"Fuer {sourceSystem} ist keine zentrale SAP Service URL gepflegt.", Severity.Warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await SapGatewayService.TestConnectionAsync(definition.CentralServiceUrl, username.Trim(), password.Trim());
|
|
||||||
Snackbar.Add($"{sourceSystem}: Zentrale SAP Gateway-Verbindung erfolgreich.", Severity.Success);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Snackbar.Add($"{sourceSystem}: {ex.Message}", Severity.Error);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_testingSystems.Remove(sourceSystem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string NormalizeSourceSystemCode(string? code) => NormalizeConfigValue(code).ToUpperInvariant();
|
|
||||||
|
|
||||||
private static string NormalizeConnectionKind(string? connectionKind)
|
|
||||||
=> SourceSystemConnectionKinds.All.Contains(connectionKind ?? string.Empty, StringComparer.OrdinalIgnoreCase)
|
|
||||||
? (connectionKind ?? string.Empty).Trim().ToUpperInvariant()
|
|
||||||
: SourceSystemConnectionKinds.Hana;
|
|
||||||
|
|
||||||
private static string GetConnectionKindLabel(string connectionKind) => connectionKind switch
|
private static string GetConnectionKindLabel(string connectionKind) => connectionKind switch
|
||||||
{
|
{
|
||||||
@@ -808,31 +597,6 @@
|
|||||||
private static string GetUsernameSummary(SourceSystemDefinition definition)
|
private static string GetUsernameSummary(SourceSystemDefinition definition)
|
||||||
=> string.IsNullOrWhiteSpace(definition.CentralUsername) ? "-" : definition.CentralUsername;
|
=> string.IsNullOrWhiteSpace(definition.CentralUsername) ? "-" : definition.CentralUsername;
|
||||||
|
|
||||||
private static string NormalizeConfigValue(string? value) => value?.Trim() ?? string.Empty;
|
private static string NormalizeConfigValue(string? value) => Services.SettingsPageService.NormalizeConfigValue(value);
|
||||||
|
|
||||||
private 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}"
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<CurrencyExchangeRate>> LoadExchangeRatesAsync()
|
|
||||||
{
|
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
|
||||||
return await db.CurrencyExchangeRates
|
|
||||||
.OrderBy(x => x.FromCurrency)
|
|
||||||
.ThenBy(x => x.ToCurrency)
|
|
||||||
.ThenByDescending(x => x.ValidFrom)
|
|
||||||
.ToListAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
@page "/standorte"
|
@page "/standorte"
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
@using Microsoft.EntityFrameworkCore
|
|
||||||
@using System.Text.Json
|
@using System.Text.Json
|
||||||
@using System.Reflection
|
@using System.Reflection
|
||||||
@using TrafagSalesExporter.Data
|
|
||||||
@using TrafagSalesExporter.Models
|
@using TrafagSalesExporter.Models
|
||||||
@using TrafagSalesExporter.Services
|
@using TrafagSalesExporter.Services
|
||||||
@inject IDbContextFactory<AppDbContext> DbFactory
|
@inject IStandortePageService StandortePageService
|
||||||
@inject IHanaQueryService HanaService
|
@inject IStandorteSapEditorService SapEditorService
|
||||||
@inject ISapGatewayService SapGatewayService
|
|
||||||
@inject ISharePointUploadService SharePointService
|
|
||||||
@inject IAppEventLogService AppEventLogService
|
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@inject IDialogService DialogService
|
@inject IDialogService DialogService
|
||||||
|
|
||||||
@@ -466,16 +461,10 @@
|
|||||||
|
|
||||||
private async Task LoadDataAsync()
|
private async Task LoadDataAsync()
|
||||||
{
|
{
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
var state = await StandortePageService.LoadAsync();
|
||||||
_sourceSystemDefinitions = await db.SourceSystemDefinitions
|
_sourceSystemDefinitions = state.SourceSystems;
|
||||||
.OrderBy(x => x.Code)
|
_servers = state.Servers;
|
||||||
.ToListAsync();
|
_sites = state.Sites;
|
||||||
_servers = await db.HanaServers
|
|
||||||
.Where(s => GetHanaSourceSystemCodes().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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EditServer(HanaServer server)
|
private void EditServer(HanaServer server)
|
||||||
@@ -492,58 +481,7 @@
|
|||||||
_savingServer = true;
|
_savingServer = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_editingServer.SourceSystem = string.IsNullOrWhiteSpace(_editingServer.SourceSystem)
|
await StandortePageService.SaveServerAsync(_editingServer, GetHanaSourceSystemCodes());
|
||||||
? GetHanaSourceSystemCodes().FirstOrDefault() ?? string.Empty
|
|
||||||
: _editingServer.SourceSystem.Trim().ToUpperInvariant();
|
|
||||||
_editingServer.Name = string.IsNullOrWhiteSpace(_editingServer.Name) ? _editingServer.SourceSystem : _editingServer.Name.Trim();
|
|
||||||
_editingServer.Host = _editingServer.Host.Trim();
|
|
||||||
_editingServer.DatabaseName = _editingServer.DatabaseName.Trim();
|
|
||||||
_editingServer.AdditionalParams = _editingServer.AdditionalParams.Trim();
|
|
||||||
_editingServer.Username = string.Empty;
|
|
||||||
_editingServer.Password = string.Empty;
|
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
|
||||||
if (_editingServer.Id == 0)
|
|
||||||
{
|
|
||||||
var existingForSourceSystem = await db.HanaServers
|
|
||||||
.OrderBy(x => x.Id)
|
|
||||||
.FirstOrDefaultAsync(x => x.SourceSystem == _editingServer.SourceSystem);
|
|
||||||
|
|
||||||
if (existingForSourceSystem is null)
|
|
||||||
{
|
|
||||||
db.HanaServers.Add(_editingServer);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
existingForSourceSystem.Name = _editingServer.Name;
|
|
||||||
existingForSourceSystem.Host = _editingServer.Host;
|
|
||||||
existingForSourceSystem.Port = _editingServer.Port;
|
|
||||||
existingForSourceSystem.Username = string.Empty;
|
|
||||||
existingForSourceSystem.Password = string.Empty;
|
|
||||||
existingForSourceSystem.DatabaseName = _editingServer.DatabaseName;
|
|
||||||
existingForSourceSystem.UseSsl = _editingServer.UseSsl;
|
|
||||||
existingForSourceSystem.ValidateCertificate = _editingServer.ValidateCertificate;
|
|
||||||
existingForSourceSystem.AdditionalParams = _editingServer.AdditionalParams;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var existing = await db.HanaServers.FindAsync(_editingServer.Id);
|
|
||||||
if (existing is not null)
|
|
||||||
{
|
|
||||||
existing.SourceSystem = _editingServer.SourceSystem;
|
|
||||||
existing.Name = _editingServer.Name;
|
|
||||||
existing.Host = _editingServer.Host;
|
|
||||||
existing.Port = _editingServer.Port;
|
|
||||||
existing.Username = string.Empty;
|
|
||||||
existing.Password = string.Empty;
|
|
||||||
existing.DatabaseName = _editingServer.DatabaseName;
|
|
||||||
existing.UseSsl = _editingServer.UseSsl;
|
|
||||||
existing.ValidateCertificate = _editingServer.ValidateCertificate;
|
|
||||||
existing.AdditionalParams = _editingServer.AdditionalParams;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
_serverDialogVisible = false;
|
_serverDialogVisible = false;
|
||||||
await LoadDataAsync();
|
await LoadDataAsync();
|
||||||
Snackbar.Add("Server gespeichert", Severity.Success);
|
Snackbar.Add("Server gespeichert", Severity.Success);
|
||||||
@@ -571,27 +509,7 @@
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
await StandortePageService.DeleteServerAsync(server);
|
||||||
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)
|
|
||||||
{
|
|
||||||
Snackbar.Add(
|
|
||||||
$"Server kann nicht gelöscht werden. Noch verknüpfte Standorte: {string.Join(", ", linkedSites)}",
|
|
||||||
Severity.Warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var entity = await db.HanaServers.FindAsync(server.Id);
|
|
||||||
if (entity is not null)
|
|
||||||
{
|
|
||||||
db.HanaServers.Remove(entity);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -605,50 +523,19 @@
|
|||||||
|
|
||||||
private async Task TestServerConnection(HanaServer server)
|
private async Task TestServerConnection(HanaServer server)
|
||||||
{
|
{
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
try
|
||||||
var sourceDefinition = await db.SourceSystemDefinitions
|
|
||||||
.OrderBy(x => x.Id)
|
|
||||||
.FirstOrDefaultAsync(x => x.Code == server.SourceSystem);
|
|
||||||
|
|
||||||
if (sourceDefinition is null)
|
|
||||||
{
|
{
|
||||||
Snackbar.Add($"Quellsystem '{server.SourceSystem}' nicht gefunden.", Severity.Warning);
|
var result = await StandortePageService.TestServerConnectionAsync(server);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(sourceDefinition.CentralUsername) || string.IsNullOrWhiteSpace(sourceDefinition.CentralPassword))
|
|
||||||
{
|
|
||||||
Snackbar.Add($"Fuer {server.SourceSystem} sind keine zentralen Zugangsdaten im Quellsystem gepflegt.", Severity.Warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
|
||||||
var result = await Task.Run(() => HanaService.TestConnectionDetailed(testServer));
|
|
||||||
_connectionStatus[server.Id] = result;
|
_connectionStatus[server.Id] = result;
|
||||||
|
Snackbar.Add(
|
||||||
if (result.Success)
|
result.Success
|
||||||
{
|
? $"Verbindung zu '{server.Name}' erfolgreich."
|
||||||
Snackbar.Add($"Verbindung zu '{server.Name}' erfolgreich.", Severity.Success);
|
: $"{server.Name}: {result.ExceptionType} - {result.ErrorMessage}",
|
||||||
|
result.Success ? Severity.Success : Severity.Error);
|
||||||
}
|
}
|
||||||
else
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Snackbar.Add($"{server.Name}: {result.ExceptionType} - {result.ErrorMessage}", Severity.Error);
|
Snackbar.Add(ex.Message, Severity.Warning);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,33 +569,18 @@
|
|||||||
|
|
||||||
private void EditSite(Site site)
|
private void EditSite(Site site)
|
||||||
{
|
{
|
||||||
_editingSite = new Site
|
_ = EditSiteAsync(site);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EditSiteAsync(Site site)
|
||||||
{
|
{
|
||||||
Id = site.Id,
|
var editorState = await StandortePageService.LoadSiteEditorAsync(site, GetAvailableSourceSystems());
|
||||||
HanaServerId = site.HanaServerId,
|
_editingSite = editorState.Site;
|
||||||
Schema = site.Schema,
|
|
||||||
TSC = site.TSC,
|
|
||||||
Land = site.Land,
|
|
||||||
SourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem)
|
|
||||||
? GetAvailableSourceSystems().FirstOrDefault()?.Code ?? "SAP"
|
|
||||||
: site.SourceSystem,
|
|
||||||
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
|
|
||||||
};
|
|
||||||
_availableSchemas = [];
|
_availableSchemas = [];
|
||||||
_sapEntitySetsCache = ParseSapEntitySets(site.SapEntitySetsCache);
|
_sapEntitySetsCache = editorState.SapEntitySets;
|
||||||
using var db = DbFactory.CreateDbContext();
|
_sapSources = editorState.SapSources;
|
||||||
_sapSources = db.SapSourceDefinitions.Where(s => s.SiteId == site.Id).OrderBy(s => s.SortOrder).ThenBy(s => s.Id).ToList();
|
_sapJoins = editorState.SapJoins;
|
||||||
_sapJoins = db.SapJoinDefinitions.Where(j => j.SiteId == site.Id).OrderBy(j => j.SortOrder).ThenBy(j => j.Id).ToList();
|
_sapMappings = editorState.SapMappings;
|
||||||
_sapMappings = db.SapFieldMappings.Where(m => m.SiteId == site.Id).OrderBy(m => m.SortOrder).ThenBy(m => m.Id).ToList();
|
|
||||||
_sapAvailableSourceExpressions = BuildSourceExpressionsFromMappings();
|
_sapAvailableSourceExpressions = BuildSourceExpressionsFromMappings();
|
||||||
_sapSourceFieldMap = BuildSourceFieldMapFromJoins();
|
_sapSourceFieldMap = BuildSourceFieldMapFromJoins();
|
||||||
_siteDialogVisible = true;
|
_siteDialogVisible = true;
|
||||||
@@ -722,40 +594,7 @@
|
|||||||
_savingSite = true;
|
_savingSite = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
await StandortePageService.SaveSiteAsync(_editingSite, UsesHanaConnection(), IsSapSite(), _sapSources, _sapJoins, _sapMappings, _sapEntitySetsCache);
|
||||||
var serverId = UsesHanaConnection() ? await ResolveCentralHanaServerIdAsync(db, _editingSite.SourceSystem) : (int?)null;
|
|
||||||
_editingSite.HanaServerId = serverId;
|
|
||||||
_editingSite.SapEntitySetsCache = SerializeSapEntitySets(_sapEntitySetsCache);
|
|
||||||
|
|
||||||
if (_editingSite.Id == 0)
|
|
||||||
{
|
|
||||||
db.Sites.Add(_editingSite);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var existing = await db.Sites.FindAsync(_editingSite.Id);
|
|
||||||
if (existing is not null)
|
|
||||||
{
|
|
||||||
existing.HanaServerId = serverId;
|
|
||||||
existing.Schema = _editingSite.Schema;
|
|
||||||
existing.TSC = _editingSite.TSC;
|
|
||||||
existing.Land = _editingSite.Land;
|
|
||||||
existing.SourceSystem = _editingSite.SourceSystem;
|
|
||||||
existing.UsernameOverride = _editingSite.UsernameOverride;
|
|
||||||
existing.PasswordOverride = _editingSite.PasswordOverride;
|
|
||||||
existing.LocalExportFolderOverride = _editingSite.LocalExportFolderOverride;
|
|
||||||
existing.ManualImportFilePath = _editingSite.ManualImportFilePath;
|
|
||||||
existing.ManualImportLastUploadedAtUtc = _editingSite.ManualImportLastUploadedAtUtc;
|
|
||||||
existing.SapServiceUrl = _editingSite.SapServiceUrl;
|
|
||||||
existing.SapEntitySet = _editingSite.SapEntitySet;
|
|
||||||
existing.SapEntitySetsCache = _editingSite.SapEntitySetsCache;
|
|
||||||
existing.SapEntitySetsRefreshedAtUtc = _editingSite.SapEntitySetsRefreshedAtUtc;
|
|
||||||
existing.IsActive = _editingSite.IsActive;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
await SaveSapConfigurationAsync(db, _editingSite.Id);
|
|
||||||
_siteDialogVisible = false;
|
_siteDialogVisible = false;
|
||||||
await LoadDataAsync();
|
await LoadDataAsync();
|
||||||
Snackbar.Add("Standort gespeichert", Severity.Success);
|
Snackbar.Add("Standort gespeichert", Severity.Success);
|
||||||
@@ -779,22 +618,7 @@
|
|||||||
|
|
||||||
if (result != true) return;
|
if (result != true) return;
|
||||||
|
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
await StandortePageService.DeleteSiteAsync(site);
|
||||||
var entity = await db.Sites.FindAsync(site.Id);
|
|
||||||
if (entity is not null)
|
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
await LoadDataAsync();
|
await LoadDataAsync();
|
||||||
Snackbar.Add("Standort gelöscht", Severity.Info);
|
Snackbar.Add("Standort gelöscht", Severity.Info);
|
||||||
}
|
}
|
||||||
@@ -825,26 +649,6 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<int> ResolveCentralHanaServerIdAsync(AppDbContext db, string sourceSystem)
|
|
||||||
{
|
|
||||||
_editingSite.UsernameOverride = _editingSite.UsernameOverride.Trim();
|
|
||||||
_editingSite.PasswordOverride = _editingSite.PasswordOverride.Trim();
|
|
||||||
_editingSite.LocalExportFolderOverride = _editingSite.LocalExportFolderOverride.Trim();
|
|
||||||
_editingSite.ManualImportFilePath = _editingSite.ManualImportFilePath.Trim();
|
|
||||||
_editingSite.SapServiceUrl = _editingSite.SapServiceUrl.Trim();
|
|
||||||
_editingSite.SapEntitySet = _editingSite.SapEntitySet.Trim();
|
|
||||||
|
|
||||||
var normalizedSourceSystem = string.IsNullOrWhiteSpace(sourceSystem) ? string.Empty : 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task OnSchemaSelected(string schema)
|
private Task OnSchemaSelected(string schema)
|
||||||
{
|
{
|
||||||
_editingSite.Schema = schema;
|
_editingSite.Schema = schema;
|
||||||
@@ -939,52 +743,7 @@
|
|||||||
_loadingSchemas = true;
|
_loadingSchemas = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
_availableSchemas = await StandortePageService.LoadAvailableSchemasAsync(_editingSite);
|
||||||
var sourceDefinition = await db.SourceSystemDefinitions
|
|
||||||
.OrderBy(x => x.Id)
|
|
||||||
.FirstOrDefaultAsync(x => x.Code == _editingSite.SourceSystem);
|
|
||||||
|
|
||||||
if (sourceDefinition is null)
|
|
||||||
throw new InvalidOperationException($"Quellsystem '{_editingSite.SourceSystem}' nicht gefunden.");
|
|
||||||
|
|
||||||
var centralServer = await db.HanaServers
|
|
||||||
.OrderBy(x => x.Id)
|
|
||||||
.FirstOrDefaultAsync(x => x.SourceSystem == _editingSite.SourceSystem);
|
|
||||||
|
|
||||||
if (centralServer is null || string.IsNullOrWhiteSpace(centralServer.Host))
|
|
||||||
throw new InvalidOperationException($"Fuer {_editingSite.SourceSystem} ist keine gueltige zentrale HANA-Konfiguration vorhanden.");
|
|
||||||
|
|
||||||
var username = string.IsNullOrWhiteSpace(_editingSite.UsernameOverride)
|
|
||||||
? sourceDefinition.CentralUsername ?? string.Empty
|
|
||||||
: _editingSite.UsernameOverride;
|
|
||||||
var password = string.IsNullOrWhiteSpace(_editingSite.PasswordOverride)
|
|
||||||
? sourceDefinition.CentralPassword ?? string.Empty
|
|
||||||
: _editingSite.PasswordOverride;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
|
||||||
throw new InvalidOperationException($"Fuer {_editingSite.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
|
|
||||||
};
|
|
||||||
|
|
||||||
var schemas = await Task.Run(() => HanaService.GetAvailableSchemas(lookupServer));
|
|
||||||
_availableSchemas = schemas
|
|
||||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (_availableSchemas.Count == 0)
|
if (_availableSchemas.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -1018,31 +777,10 @@
|
|||||||
_refreshingSapEntitySets = true;
|
_refreshingSapEntitySets = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
var result = await StandortePageService.RefreshSapEntitySetsAsync(_editingSite);
|
||||||
var sourceDefinition = await db.SourceSystemDefinitions
|
_sapEntitySetsCache = result.EntitySets;
|
||||||
.OrderBy(x => x.Id)
|
_editingSite.SapEntitySetsCache = SerializeSapEntitySets(result.EntitySets);
|
||||||
.FirstOrDefaultAsync(x => x.Code == _editingSite.SourceSystem);
|
_editingSite.SapEntitySetsRefreshedAtUtc = result.RefreshedAtUtc;
|
||||||
var serviceUrl = string.IsNullOrWhiteSpace(_editingSite.SapServiceUrl)
|
|
||||||
? sourceDefinition?.CentralServiceUrl ?? string.Empty
|
|
||||||
: _editingSite.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(_editingSite.UsernameOverride)
|
|
||||||
? sourceDefinition?.CentralUsername ?? string.Empty
|
|
||||||
: _editingSite.UsernameOverride;
|
|
||||||
var password = string.IsNullOrWhiteSpace(_editingSite.PasswordOverride)
|
|
||||||
? sourceDefinition?.CentralPassword ?? string.Empty
|
|
||||||
: _editingSite.PasswordOverride;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
|
||||||
throw new InvalidOperationException("Für SAP sind weder zentrale Zugangsdaten noch Standort-Overrides gesetzt.");
|
|
||||||
|
|
||||||
await AppEventLogService.WriteAsync("SAP", "Refresh aus UI gestartet", siteId: _editingSite.Id, land: _editingSite.Land,
|
|
||||||
details: serviceUrl);
|
|
||||||
var entitySets = await SapGatewayService.GetEntitySetsAsync(serviceUrl, username.Trim(), password.Trim());
|
|
||||||
_sapEntitySetsCache = entitySets;
|
|
||||||
_editingSite.SapEntitySetsCache = SerializeSapEntitySets(entitySets);
|
|
||||||
_editingSite.SapEntitySetsRefreshedAtUtc = DateTime.UtcNow;
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(_editingSite.SapEntitySet) &&
|
if (!string.IsNullOrWhiteSpace(_editingSite.SapEntitySet) &&
|
||||||
!_sapEntitySetsCache.Contains(_editingSite.SapEntitySet, StringComparer.OrdinalIgnoreCase))
|
!_sapEntitySetsCache.Contains(_editingSite.SapEntitySet, StringComparer.OrdinalIgnoreCase))
|
||||||
@@ -1050,15 +788,11 @@
|
|||||||
_editingSite.SapEntitySet = string.Empty;
|
_editingSite.SapEntitySet = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
Snackbar.Add($"{entitySets.Count} SAP Entity Sets geladen.", Severity.Success);
|
Snackbar.Add($"{result.EntitySets.Count} SAP Entity Sets geladen.", Severity.Success);
|
||||||
await AppEventLogService.WriteAsync("SAP", "Refresh aus UI erfolgreich", siteId: _editingSite.Id, land: _editingSite.Land,
|
|
||||||
details: $"EntitySets={entitySets.Count}");
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Snackbar.Add(ex.Message, Severity.Error);
|
Snackbar.Add(ex.Message, Severity.Error);
|
||||||
await AppEventLogService.WriteAsync("SAP", "Refresh aus UI fehlgeschlagen", "Error", siteId: _editingSite.Id, land: _editingSite.Land,
|
|
||||||
details: ex.ToString());
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -1119,12 +853,10 @@
|
|||||||
_editingSite.ManualImportFilePath = targetPath;
|
_editingSite.ManualImportFilePath = targetPath;
|
||||||
_editingSite.ManualImportLastUploadedAtUtc = DateTime.UtcNow;
|
_editingSite.ManualImportLastUploadedAtUtc = DateTime.UtcNow;
|
||||||
Snackbar.Add("Excel-Datei hochgeladen.", Severity.Success);
|
Snackbar.Add("Excel-Datei hochgeladen.", Severity.Success);
|
||||||
await AppEventLogService.WriteAsync("ManualImport", "Excel-Datei hochgeladen", siteId: _editingSite.Id, land: _editingSite.Land, details: targetPath);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Snackbar.Add($"Upload fehlgeschlagen: {ex.Message}", Severity.Error);
|
Snackbar.Add($"Upload fehlgeschlagen: {ex.Message}", Severity.Error);
|
||||||
await AppEventLogService.WriteAsync("ManualImport", "Excel-Upload fehlgeschlagen", "Error", siteId: _editingSite.Id, land: _editingSite.Land, details: ex.ToString());
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -1136,55 +868,12 @@
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_editingSite.ManualImportFilePath = _editingSite.ManualImportFilePath.Trim();
|
_editingSite.ManualImportLastUploadedAtUtc = await StandortePageService.ValidateManualImportPathAsync(_editingSite.ManualImportFilePath);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(_editingSite.ManualImportFilePath))
|
|
||||||
throw new InvalidOperationException("Bitte zuerst einen Dateipfad eintragen.");
|
|
||||||
|
|
||||||
if (!string.Equals(Path.GetExtension(_editingSite.ManualImportFilePath), ".xlsx", StringComparison.OrdinalIgnoreCase))
|
|
||||||
throw new InvalidOperationException("Bitte eine Excel-Datei mit Endung .xlsx angeben.");
|
|
||||||
|
|
||||||
if (File.Exists(_editingSite.ManualImportFilePath))
|
|
||||||
{
|
|
||||||
_editingSite.ManualImportLastUploadedAtUtc = File.GetLastWriteTimeUtc(_editingSite.ManualImportFilePath);
|
|
||||||
}
|
|
||||||
else if (LooksLikeSharePointReference(_editingSite.ManualImportFilePath))
|
|
||||||
{
|
|
||||||
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, _editingSite.ManualImportFilePath);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_editingSite.ManualImportLastUploadedAtUtc = File.GetLastWriteTimeUtc(tempPath);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (File.Exists(tempPath))
|
|
||||||
File.Delete(tempPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Datei nicht gefunden oder nicht erreichbar: {_editingSite.ManualImportFilePath}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Snackbar.Add("Dateipfad ist gueltig und die Excel-Datei ist erreichbar.", Severity.Success);
|
Snackbar.Add("Dateipfad ist gueltig und die Excel-Datei ist erreichbar.", Severity.Success);
|
||||||
await AppEventLogService.WriteAsync("ManualImport", "Dateipfad erfolgreich geprueft", siteId: _editingSite.Id, land: _editingSite.Land, details: _editingSite.ManualImportFilePath);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Snackbar.Add($"Pfadpruefung fehlgeschlagen: {ex.Message}", Severity.Error);
|
Snackbar.Add($"Pfadpruefung fehlgeschlagen: {ex.Message}", Severity.Error);
|
||||||
await AppEventLogService.WriteAsync("ManualImport", "Dateipfadpruefung fehlgeschlagen", "Error", siteId: _editingSite.Id, land: _editingSite.Land, details: ex.ToString());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1206,182 +895,48 @@
|
|||||||
private static string SerializeSapEntitySets(List<string> entitySets)
|
private static string SerializeSapEntitySets(List<string> entitySets)
|
||||||
=> JsonSerializer.Serialize(entitySets);
|
=> JsonSerializer.Serialize(entitySets);
|
||||||
|
|
||||||
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 void AddSapSource()
|
private void AddSapSource()
|
||||||
{
|
{
|
||||||
_sapSources.Add(new SapSourceDefinition
|
SapEditorService.AddSapSource(_sapSources, _sapEntitySetsCache);
|
||||||
{
|
|
||||||
Alias = $"SRC{_sapSources.Count + 1}",
|
|
||||||
EntitySet = _sapEntitySetsCache.FirstOrDefault() ?? string.Empty,
|
|
||||||
IsActive = true,
|
|
||||||
IsPrimary = _sapSources.Count == 0,
|
|
||||||
SortOrder = _sapSources.Count
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveSapSource(SapSourceDefinition source)
|
private void RemoveSapSource(SapSourceDefinition source)
|
||||||
{
|
{
|
||||||
_sapSources.Remove(source);
|
SapEditorService.RemoveSapSource(_sapSources, source);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddSapJoin()
|
private void AddSapJoin()
|
||||||
{
|
{
|
||||||
_sapJoins.Add(new SapJoinDefinition
|
SapEditorService.AddSapJoin(_sapJoins);
|
||||||
{
|
|
||||||
JoinType = "Left",
|
|
||||||
IsActive = true,
|
|
||||||
SortOrder = _sapJoins.Count
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AutoMatchSapJoins()
|
private void AutoMatchSapJoins()
|
||||||
{
|
{
|
||||||
var activeSources = _sapSources
|
var result = SapEditorService.AutoMatchSapJoins(_sapSources, _sapJoins, _sapSourceFieldMap);
|
||||||
.Where(s => s.IsActive && !string.IsNullOrWhiteSpace(s.Alias))
|
SapEditorService.NormalizeSapConfigCollections(_sapSources, _sapJoins, _sapMappings);
|
||||||
.OrderBy(s => s.SortOrder)
|
Snackbar.Add(result.Message, result.Success ? Severity.Success : result.Warning ? Severity.Warning : Severity.Info);
|
||||||
.ThenBy(s => s.Id)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (activeSources.Count < 2)
|
|
||||||
{
|
|
||||||
Snackbar.Add("Für Auto-Match werden mindestens zwei aktive SAP-Quellen benötigt.", Severity.Warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_sapSourceFieldMap.Count == 0)
|
|
||||||
{
|
|
||||||
Snackbar.Add("Bitte zuerst 'Felder aus Quellen laden' ausführen.", Severity.Warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
Snackbar.Add("Kein passender Join-Vorschlag gefunden.", Severity.Info);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NormalizeSapConfigCollections();
|
|
||||||
Snackbar.Add($"{createdOrUpdated} Join-Vorschläge gesetzt.", Severity.Success);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveSapJoin(SapJoinDefinition join)
|
private void RemoveSapJoin(SapJoinDefinition join)
|
||||||
{
|
{
|
||||||
_sapJoins.Remove(join);
|
SapEditorService.RemoveSapJoin(_sapJoins, join);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddSapMapping()
|
private void AddSapMapping()
|
||||||
{
|
{
|
||||||
_sapMappings.Add(new SapFieldMapping
|
SapEditorService.AddSapMapping(_sapMappings, _salesRecordFields, _sapAvailableSourceExpressions);
|
||||||
{
|
|
||||||
TargetField = _salesRecordFields.First(),
|
|
||||||
SourceExpression = _sapAvailableSourceExpressions.FirstOrDefault() ?? "=SAP",
|
|
||||||
IsActive = true,
|
|
||||||
SortOrder = _sapMappings.Count
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveSapMapping(SapFieldMapping mapping)
|
private void RemoveSapMapping(SapFieldMapping mapping)
|
||||||
{
|
{
|
||||||
_sapMappings.Remove(mapping);
|
SapEditorService.RemoveSapMapping(_sapMappings, mapping);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<string> GetSapAliases()
|
private IEnumerable<string> GetSapAliases()
|
||||||
=> _sapSources.Where(s => !string.IsNullOrWhiteSpace(s.Alias)).Select(s => s.Alias).Distinct(StringComparer.OrdinalIgnoreCase);
|
=> SapEditorService.GetSapAliases(_sapSources);
|
||||||
|
|
||||||
private async Task SaveSapConfigurationAsync(AppDbContext db, int siteId)
|
|
||||||
{
|
|
||||||
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();
|
|
||||||
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 void NormalizeSapConfigCollections()
|
private void NormalizeSapConfigCollections()
|
||||||
{
|
=> SapEditorService.NormalizeSapConfigCollections(_sapSources, _sapJoins, _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 async Task RefreshSapSourceFields()
|
private async Task RefreshSapSourceFields()
|
||||||
{
|
{
|
||||||
@@ -1400,51 +955,9 @@
|
|||||||
if (activeSources.Count == 0)
|
if (activeSources.Count == 0)
|
||||||
throw new InvalidOperationException("Es gibt keine aktiven SAP-Quellen mit Alias und Entity Set.");
|
throw new InvalidOperationException("Es gibt keine aktiven SAP-Quellen mit Alias und Entity Set.");
|
||||||
|
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
var result = await StandortePageService.RefreshSapSourceFieldsAsync(_editingSite, activeSources, _sapMappings);
|
||||||
var sourceDefinition = await db.SourceSystemDefinitions
|
_sapAvailableSourceExpressions = result.SourceExpressions;
|
||||||
.OrderBy(x => x.Id)
|
_sapSourceFieldMap = result.SourceFieldMap;
|
||||||
.FirstOrDefaultAsync(x => x.Code == _editingSite.SourceSystem);
|
|
||||||
var serviceUrl = string.IsNullOrWhiteSpace(_editingSite.SapServiceUrl)
|
|
||||||
? sourceDefinition?.CentralServiceUrl ?? string.Empty
|
|
||||||
: _editingSite.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(_editingSite.UsernameOverride)
|
|
||||||
? sourceDefinition?.CentralUsername ?? string.Empty
|
|
||||||
: _editingSite.UsernameOverride;
|
|
||||||
var password = string.IsNullOrWhiteSpace(_editingSite.PasswordOverride)
|
|
||||||
? sourceDefinition?.CentralPassword ?? string.Empty
|
|
||||||
: _editingSite.PasswordOverride;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
|
||||||
throw new InvalidOperationException("Für 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}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
_sapAvailableSourceExpressions = expressions
|
|
||||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
_sapSourceFieldMap = sourceFieldMap;
|
|
||||||
|
|
||||||
foreach (var current in BuildSourceExpressionsFromMappings())
|
|
||||||
{
|
|
||||||
if (!_sapAvailableSourceExpressions.Contains(current, StringComparer.OrdinalIgnoreCase))
|
|
||||||
_sapAvailableSourceExpressions.Add(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
_sapAvailableSourceExpressions = _sapAvailableSourceExpressions
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
Snackbar.Add($"{_sapAvailableSourceExpressions.Count} Source Expressions geladen.", Severity.Success);
|
Snackbar.Add($"{_sapAvailableSourceExpressions.Count} Source Expressions geladen.", Severity.Success);
|
||||||
}
|
}
|
||||||
@@ -1459,73 +972,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<string> GetAvailableSourceExpressions(string? currentValue)
|
private IEnumerable<string> GetAvailableSourceExpressions(string? currentValue)
|
||||||
{
|
=> SapEditorService.GetAvailableSourceExpressions(_sapAvailableSourceExpressions, currentValue);
|
||||||
var expressions = new List<string>(_sapAvailableSourceExpressions);
|
|
||||||
if (!string.IsNullOrWhiteSpace(currentValue) && !expressions.Contains(currentValue, StringComparer.OrdinalIgnoreCase))
|
|
||||||
expressions.Insert(0, currentValue);
|
|
||||||
|
|
||||||
return expressions;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<string> BuildSourceExpressionsFromMappings()
|
private List<string> BuildSourceExpressionsFromMappings()
|
||||||
=> _sapMappings
|
=> SapEditorService.BuildSourceExpressionsFromMappings(_sapMappings);
|
||||||
.Select(m => m.SourceExpression)
|
|
||||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
private Dictionary<string, List<string>> BuildSourceFieldMapFromJoins()
|
private Dictionary<string, List<string>> BuildSourceFieldMapFromJoins()
|
||||||
{
|
=> SapEditorService.BuildSourceFieldMapFromJoins(_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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 IEnumerable<string> GetAvailableJoinFields(string? alias, string? currentKeys)
|
private IEnumerable<string> GetAvailableJoinFields(string? alias, string? currentKeys)
|
||||||
{
|
=> SapEditorService.GetAvailableJoinFields(_sapSourceFieldMap, alias, 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HashSet<string> GetSelectedJoinKeys(string? keys)
|
private static HashSet<string> GetSelectedJoinKeys(string? keys)
|
||||||
=> keys?
|
=> keys?
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
@page "/transformations"
|
@page "/transformations"
|
||||||
@using Microsoft.EntityFrameworkCore
|
|
||||||
@using System.Reflection
|
@using System.Reflection
|
||||||
@using TrafagSalesExporter.Data
|
|
||||||
@using TrafagSalesExporter.Models
|
@using TrafagSalesExporter.Models
|
||||||
@using TrafagSalesExporter.Services
|
@using TrafagSalesExporter.Services
|
||||||
@inject IDbContextFactory<AppDbContext> DbFactory
|
@inject ITransformationsPageService TransformationsPageActions
|
||||||
@inject ITransformationCatalog TransformationCatalog
|
@inject ITransformationCatalog TransformationCatalog
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@inject IUiTextService UiText
|
@inject IUiTextService UiText
|
||||||
@@ -199,9 +197,9 @@
|
|||||||
|
|
||||||
private async Task LoadAsync()
|
private async Task LoadAsync()
|
||||||
{
|
{
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
var state = await TransformationsPageActions.LoadAsync();
|
||||||
_sourceSystems = await db.SourceSystemDefinitions.OrderBy(x => x.Code).ToListAsync();
|
_sourceSystems = state.SourceSystems;
|
||||||
_rules = await db.FieldTransformationRules.OrderBy(r => r.SortOrder).ThenBy(r => r.Id).ToListAsync();
|
_rules = state.Rules;
|
||||||
|
|
||||||
foreach (var rule in _rules)
|
foreach (var rule in _rules)
|
||||||
{
|
{
|
||||||
@@ -235,12 +233,7 @@
|
|||||||
|
|
||||||
private async Task SaveAllAsync()
|
private async Task SaveAllAsync()
|
||||||
{
|
{
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
_rules = await TransformationsPageActions.SaveAllAsync(_rules);
|
||||||
db.FieldTransformationRules.RemoveRange(db.FieldTransformationRules);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
db.FieldTransformationRules.AddRange(_rules);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
Snackbar.Add(T("Transformationsregeln gespeichert.", "Transformation rules saved."), Severity.Success);
|
Snackbar.Add(T("Transformationsregeln gespeichert.", "Transformation rules saved."), Severity.Success);
|
||||||
await LoadAsync();
|
await LoadAsync();
|
||||||
|
|||||||
@@ -41,7 +41,16 @@ builder.Services.AddSingleton<IConsolidatedExportService, ConsolidatedExportServ
|
|||||||
builder.Services.AddSingleton<IExportLogService, ExportLogService>();
|
builder.Services.AddSingleton<IExportLogService, ExportLogService>();
|
||||||
builder.Services.AddSingleton<ICentralSalesRecordService, CentralSalesRecordService>();
|
builder.Services.AddSingleton<ICentralSalesRecordService, CentralSalesRecordService>();
|
||||||
builder.Services.AddSingleton<IConfigTransferService, ConfigTransferService>();
|
builder.Services.AddSingleton<IConfigTransferService, ConfigTransferService>();
|
||||||
|
builder.Services.AddSingleton<IDatabaseSchemaMaintenanceService, DatabaseSchemaMaintenanceService>();
|
||||||
|
builder.Services.AddSingleton<IDatabaseSeedService, DatabaseSeedService>();
|
||||||
builder.Services.AddSingleton<IDatabaseInitializationService, DatabaseInitializationService>();
|
builder.Services.AddSingleton<IDatabaseInitializationService, DatabaseInitializationService>();
|
||||||
|
builder.Services.AddSingleton<ISettingsPageService, SettingsPageService>();
|
||||||
|
builder.Services.AddSingleton<IStandortePageService, StandortePageService>();
|
||||||
|
builder.Services.AddSingleton<IStandorteSapEditorService, StandorteSapEditorService>();
|
||||||
|
builder.Services.AddSingleton<IManagementCockpitPageService, ManagementCockpitPageService>();
|
||||||
|
builder.Services.AddSingleton<IDashboardPageService, DashboardPageService>();
|
||||||
|
builder.Services.AddSingleton<ILogsPageService, LogsPageService>();
|
||||||
|
builder.Services.AddSingleton<ITransformationsPageService, TransformationsPageService>();
|
||||||
builder.Services.AddSingleton<IUiTextService, UiTextService>();
|
builder.Services.AddSingleton<IUiTextService, UiTextService>();
|
||||||
builder.Services.AddSingleton<ExportOrchestrationService>();
|
builder.Services.AddSingleton<ExportOrchestrationService>();
|
||||||
builder.Services.AddSingleton<TimerBackgroundService>();
|
builder.Services.AddSingleton<TimerBackgroundService>();
|
||||||
|
|||||||
@@ -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 System.Data;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using TrafagSalesExporter.Data;
|
using TrafagSalesExporter.Data;
|
||||||
using TrafagSalesExporter.Models;
|
|
||||||
|
|
||||||
namespace TrafagSalesExporter.Services;
|
namespace TrafagSalesExporter.Services;
|
||||||
|
|
||||||
public class DatabaseInitializationService : IDatabaseInitializationService
|
public partial class DatabaseInitializationService : IDatabaseInitializationService
|
||||||
{
|
{
|
||||||
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
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;
|
_dbFactory = dbFactory;
|
||||||
|
_schemaMaintenanceService = schemaMaintenanceService;
|
||||||
|
_seedService = seedService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task InitializeAsync()
|
public async Task InitializeAsync()
|
||||||
@@ -19,9 +25,8 @@ public class DatabaseInitializationService : IDatabaseInitializationService
|
|||||||
using var db = await _dbFactory.CreateDbContextAsync();
|
using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
await db.Database.EnsureCreatedAsync();
|
await db.Database.EnsureCreatedAsync();
|
||||||
ConfigureSqlite(db);
|
ConfigureSqlite(db);
|
||||||
EnsureSchema(db);
|
_schemaMaintenanceService.EnsureSchema(db);
|
||||||
SeedIfEmpty(db);
|
_seedService.SeedDefaults(db);
|
||||||
EnsureRecommendedTransformationRules(db);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ConfigureSqlite(AppDbContext db)
|
private static void ConfigureSqlite(AppDbContext db)
|
||||||
@@ -42,869 +47,4 @@ public class DatabaseInitializationService : IDatabaseInitializationService
|
|||||||
timeout.ExecuteNonQuery();
|
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; } = [];
|
||||||
|
}
|
||||||
+25
-2
@@ -1,6 +1,7 @@
|
|||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using TrafagSalesExporter.Data;
|
using TrafagSalesExporter.Data;
|
||||||
|
using TrafagSalesExporter.Models;
|
||||||
using TrafagSalesExporter.Services;
|
using TrafagSalesExporter.Services;
|
||||||
|
|
||||||
namespace TrafagSalesExporter.Tests;
|
namespace TrafagSalesExporter.Tests;
|
||||||
@@ -37,7 +38,7 @@ public class DatabaseInitializationServiceTests : IDisposable
|
|||||||
{
|
{
|
||||||
await PrepareLegacySitesTableAsync();
|
await PrepareLegacySitesTableAsync();
|
||||||
|
|
||||||
var service = new DatabaseInitializationService(_dbFactory);
|
var service = CreateService();
|
||||||
await service.InitializeAsync();
|
await service.InitializeAsync();
|
||||||
|
|
||||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
@@ -59,7 +60,7 @@ public class DatabaseInitializationServiceTests : IDisposable
|
|||||||
{
|
{
|
||||||
await PrepareBrokenHanaServerForeignKeyAsync();
|
await PrepareBrokenHanaServerForeignKeyAsync();
|
||||||
|
|
||||||
var service = new DatabaseInitializationService(_dbFactory);
|
var service = CreateService();
|
||||||
await service.InitializeAsync();
|
await service.InitializeAsync();
|
||||||
|
|
||||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
@@ -72,6 +73,25 @@ public class DatabaseInitializationServiceTests : IDisposable
|
|||||||
Assert.DoesNotContain("HanaServers_repair_old", tableSql, StringComparison.OrdinalIgnoreCase);
|
Assert.DoesNotContain("HanaServers_repair_old", tableSql, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InitializeAsync_Seeds_Default_SourceSystems_And_Central_HanaServers()
|
||||||
|
{
|
||||||
|
var service = CreateService();
|
||||||
|
|
||||||
|
await service.InitializeAsync();
|
||||||
|
|
||||||
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
Assert.Contains(db.SourceSystemDefinitions, x => x.Code == "SAP" && x.ConnectionKind == SourceSystemConnectionKinds.SapGateway);
|
||||||
|
Assert.Contains(db.SourceSystemDefinitions, x => x.Code == "BI1" && x.ConnectionKind == SourceSystemConnectionKinds.Hana);
|
||||||
|
Assert.Contains(db.SourceSystemDefinitions, x => x.Code == "SAGE" && x.ConnectionKind == SourceSystemConnectionKinds.Hana);
|
||||||
|
Assert.Contains(db.SourceSystemDefinitions, x => x.Code == "MANUAL_EXCEL" && x.ConnectionKind == SourceSystemConnectionKinds.ManualExcel);
|
||||||
|
|
||||||
|
Assert.Contains(db.HanaServers, x => x.SourceSystem == "BI1");
|
||||||
|
Assert.Contains(db.HanaServers, x => x.SourceSystem == "SAGE");
|
||||||
|
Assert.Equal(2, db.FieldTransformationRules.Count(x => x.SourceSystem == "MANUAL_EXCEL"));
|
||||||
|
}
|
||||||
|
|
||||||
private async Task PrepareLegacySitesTableAsync()
|
private async Task PrepareLegacySitesTableAsync()
|
||||||
{
|
{
|
||||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
@@ -179,6 +199,9 @@ VALUES (
|
|||||||
return (await command.ExecuteScalarAsync())?.ToString() ?? string.Empty;
|
return (await command.ExecuteScalarAsync())?.ToString() ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DatabaseInitializationService CreateService()
|
||||||
|
=> new(_dbFactory, new DatabaseSchemaMaintenanceService(), new DatabaseSeedService());
|
||||||
|
|
||||||
private sealed class TestDbContextFactory : IDbContextFactory<AppDbContext>
|
private sealed class TestDbContextFactory : IDbContextFactory<AppDbContext>
|
||||||
{
|
{
|
||||||
private readonly DbContextOptions<AppDbContext> _options;
|
private readonly DbContextOptions<AppDbContext> _options;
|
||||||
|
|||||||
Reference in New Issue
Block a user