diverse Aenderungen
This commit is contained in:
@@ -8,6 +8,9 @@
|
||||
<MudNavLink Href="/transformations" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Transform">
|
||||
Transformationen
|
||||
</MudNavLink>
|
||||
<MudNavLink Href="/management-cockpit" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Analytics">
|
||||
Management Cockpit
|
||||
</MudNavLink>
|
||||
<MudNavLink Href="/settings" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Settings">
|
||||
Settings
|
||||
</MudNavLink>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@page "/"
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using System.Diagnostics
|
||||
@using TrafagSalesExporter.Data
|
||||
@using TrafagSalesExporter.Services
|
||||
@inject IDbContextFactory<AppDbContext> DbFactory
|
||||
@@ -40,6 +41,7 @@
|
||||
<MudTh>Schema</MudTh>
|
||||
<MudTh>Server</MudTh>
|
||||
<MudTh>Status</MudTh>
|
||||
<MudTh>Live-Status</MudTh>
|
||||
<MudTh>Zeilen</MudTh>
|
||||
<MudTh>Letzter Lauf</MudTh>
|
||||
<MudTh>Dauer</MudTh>
|
||||
@@ -71,16 +73,38 @@
|
||||
<MudText Typo="Typo.caption" Color="Color.Default">-</MudText>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
@if (!string.IsNullOrWhiteSpace(context.LiveMessage))
|
||||
{
|
||||
<MudTooltip Text="@context.LiveDetails">
|
||||
<MudText Typo="Typo.caption" Style="max-width:360px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; display:block;">
|
||||
@context.LiveMessage
|
||||
</MudText>
|
||||
</MudTooltip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Default">-</MudText>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd>@(context.RowCount > 0 ? context.RowCount.ToString("N0") : "-")</MudTd>
|
||||
<MudTd>@(context.LastRun.HasValue ? context.LastRun.Value.ToString("dd.MM.yyyy HH:mm:ss") : "-")</MudTd>
|
||||
<MudTd>@(context.DurationSeconds > 0 ? $"{context.DurationSeconds:F1}s" : "-")</MudTd>
|
||||
<MudTd>
|
||||
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.FileDownload"
|
||||
OnClick="() => ExportSingle(context.SiteId)"
|
||||
Disabled="Orchestrator.IsExporting(context.SiteId)">
|
||||
Export
|
||||
</MudButton>
|
||||
<MudStack Row Spacing="1">
|
||||
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.FileDownload"
|
||||
OnClick="() => ExportSingle(context.SiteId)"
|
||||
Disabled="Orchestrator.IsExporting(context.SiteId)">
|
||||
Export
|
||||
</MudButton>
|
||||
<MudButton Size="Size.Small" Variant="Variant.Text" Color="Color.Info"
|
||||
StartIcon="@Icons.Material.Filled.OpenInNew"
|
||||
OnClick="() => OpenExportFile(context)"
|
||||
Disabled="@(!context.HasOpenableFile || Orchestrator.IsExporting(context.SiteId))">
|
||||
Excel öffnen
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
@@ -89,6 +113,7 @@
|
||||
private List<DashboardRow> _dashboardRows = new();
|
||||
private bool _loading = true;
|
||||
private bool _anyRunning;
|
||||
private CancellationTokenSource? _pollingCts;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
@@ -106,10 +131,19 @@
|
||||
.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);
|
||||
return new DashboardRow
|
||||
{
|
||||
SiteId = s.Id,
|
||||
@@ -123,7 +157,10 @@
|
||||
RowCount = log?.RowCount ?? 0,
|
||||
LastRun = log?.Timestamp,
|
||||
DurationSeconds = log?.DurationSeconds ?? 0,
|
||||
ErrorMessage = log?.ErrorMessage ?? ""
|
||||
ErrorMessage = log?.ErrorMessage ?? "",
|
||||
FilePath = log?.FilePath ?? "",
|
||||
LiveMessage = appLog is null ? string.Empty : $"{appLog.Category}: {appLog.Message}",
|
||||
LiveDetails = appLog?.Details ?? ""
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
@@ -134,6 +171,8 @@
|
||||
private async Task ExportAll()
|
||||
{
|
||||
_anyRunning = true;
|
||||
await LoadDataAsync();
|
||||
StartPolling();
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Orchestrator.ExportAllAsync();
|
||||
@@ -148,14 +187,28 @@
|
||||
|
||||
private void ExportSingle(int siteId)
|
||||
{
|
||||
_anyRunning = true;
|
||||
_ = InvokeAsync(async () => await LoadDataAsync());
|
||||
StartPolling();
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Orchestrator.ExportSiteByIdAsync(siteId);
|
||||
var result = await Orchestrator.ExportSiteByIdAsync(siteId);
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
await LoadDataAsync();
|
||||
StateHasChanged();
|
||||
});
|
||||
|
||||
if (result?.Log.Status == "OK" && !string.IsNullOrWhiteSpace(result.FilePath))
|
||||
{
|
||||
await InvokeAsync(() =>
|
||||
Snackbar.Add($"Export gespeichert: {result.FilePath}", Severity.Success));
|
||||
}
|
||||
else if (result?.Log.Status == "Error" && !string.IsNullOrWhiteSpace(result.Log.ErrorMessage))
|
||||
{
|
||||
await InvokeAsync(() =>
|
||||
Snackbar.Add($"Export fehlgeschlagen: {result.Log.ErrorMessage}", Severity.Error));
|
||||
}
|
||||
});
|
||||
Snackbar.Add("Export gestartet", Severity.Info);
|
||||
}
|
||||
@@ -164,21 +217,136 @@
|
||||
{
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId));
|
||||
StateHasChanged();
|
||||
if (!_anyRunning)
|
||||
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || _dashboardRows.Count == 0;
|
||||
if (_anyRunning)
|
||||
{
|
||||
await LoadDataAsync();
|
||||
StartPolling();
|
||||
await RefreshLiveDataAsync();
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
StopPolling();
|
||||
await LoadDataAsync();
|
||||
StateHasChanged();
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
StopPolling();
|
||||
Orchestrator.OnExportStatusChanged -= HandleStatusChanged;
|
||||
}
|
||||
|
||||
private void OpenExportFile(DashboardRow row)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(row.FilePath) || !File.Exists(row.FilePath))
|
||||
{
|
||||
Snackbar.Add("Exportdatei nicht gefunden.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = row.FilePath,
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Datei konnte nicht geöffnet werden: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void StartPolling()
|
||||
{
|
||||
if (_pollingCts is not null && !_pollingCts.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
_pollingCts = new CancellationTokenSource();
|
||||
_ = PollDashboardAsync(_pollingCts.Token);
|
||||
}
|
||||
|
||||
private void StopPolling()
|
||||
{
|
||||
_pollingCts?.Cancel();
|
||||
_pollingCts?.Dispose();
|
||||
_pollingCts = null;
|
||||
}
|
||||
|
||||
private async Task PollDashboardAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(3));
|
||||
|
||||
try
|
||||
{
|
||||
while (await timer.WaitForNextTickAsync(cancellationToken))
|
||||
{
|
||||
var anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId));
|
||||
if (!anyRunning)
|
||||
{
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
_anyRunning = false;
|
||||
await LoadDataAsync();
|
||||
StateHasChanged();
|
||||
});
|
||||
StopPolling();
|
||||
break;
|
||||
}
|
||||
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
_anyRunning = true;
|
||||
await RefreshLiveDataAsync();
|
||||
StateHasChanged();
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshLiveDataAsync()
|
||||
{
|
||||
var runningSiteIds = _dashboardRows
|
||||
.Where(r => Orchestrator.IsExporting(r.SiteId))
|
||||
.Select(r => r.SiteId)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (runningSiteIds.Count == 0)
|
||||
{
|
||||
_anyRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
var appLogs = await db.AppEventLogs
|
||||
.Where(l => l.SiteId != null && runningSiteIds.Contains(l.SiteId.Value))
|
||||
.OrderByDescending(l => l.Timestamp)
|
||||
.Take(200)
|
||||
.ToListAsync();
|
||||
|
||||
var latestAppLogsBySite = appLogs
|
||||
.GroupBy(l => l.SiteId!.Value)
|
||||
.ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.Timestamp).First());
|
||||
|
||||
foreach (var row in _dashboardRows)
|
||||
{
|
||||
if (!latestAppLogsBySite.TryGetValue(row.SiteId, out var appLog))
|
||||
continue;
|
||||
|
||||
row.LiveMessage = $"{appLog.Category}: {appLog.Message}";
|
||||
row.LiveDetails = appLog.Details ?? string.Empty;
|
||||
}
|
||||
|
||||
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId));
|
||||
}
|
||||
|
||||
private class DashboardRow
|
||||
{
|
||||
public int SiteId { get; set; }
|
||||
@@ -191,5 +359,9 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,8 +75,39 @@
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
|
||||
<MudText Typo="Typo.h5" Class="mt-6 mb-2">Technische Logs</MudText>
|
||||
|
||||
<MudTable Items="_appLogs" Dense Hover Striped Loading="_loading">
|
||||
<HeaderContent>
|
||||
<MudTh>Zeitpunkt</MudTh>
|
||||
<MudTh>Level</MudTh>
|
||||
<MudTh>Kategorie</MudTh>
|
||||
<MudTh>Land</MudTh>
|
||||
<MudTh>Meldung</MudTh>
|
||||
<MudTh>Details</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Timestamp.ToString("dd.MM.yyyy HH:mm:ss")</MudTd>
|
||||
<MudTd>@context.Level</MudTd>
|
||||
<MudTd>@context.Category</MudTd>
|
||||
<MudTd>@(string.IsNullOrWhiteSpace(context.Land) ? "-" : context.Land)</MudTd>
|
||||
<MudTd>@context.Message</MudTd>
|
||||
<MudTd>
|
||||
@if (!string.IsNullOrWhiteSpace(context.Details))
|
||||
{
|
||||
<MudTooltip Text="@context.Details">
|
||||
<MudText Typo="Typo.caption" Style="max-width:420px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; display:block;">
|
||||
@context.Details
|
||||
</MudText>
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
|
||||
@code {
|
||||
private List<ExportLog> _logs = new();
|
||||
private List<AppEventLog> _appLogs = new();
|
||||
private List<string> _availableLands = new();
|
||||
private string? _filterLand;
|
||||
private string? _filterStatus;
|
||||
@@ -106,6 +137,16 @@
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
@page "/management-cockpit"
|
||||
@using TrafagSalesExporter.Models
|
||||
@using TrafagSalesExporter.Services
|
||||
@inject IManagementCockpitService CockpitService
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<PageTitle>Management Cockpit</PageTitle>
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Management Cockpit</MudText>
|
||||
|
||||
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
||||
<MudGrid>
|
||||
<MudItem xs="12" md="8">
|
||||
<MudSelect T="string" @bind-Value="_selectedFilePath" Label="Vorhandene Excel-Datei" Dense>
|
||||
@foreach (var file in _files)
|
||||
{
|
||||
<MudSelectItem Value="@file.Path">@file.DisplayName</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudStack Row Spacing="2">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" OnClick="ReloadFiles"
|
||||
StartIcon="@Icons.Material.Filled.Refresh" Disabled="_loadingFiles">
|
||||
Dateien laden
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="Analyze"
|
||||
StartIcon="@Icons.Material.Filled.Analytics" Disabled="_analyzing || string.IsNullOrWhiteSpace(_selectedFilePath)">
|
||||
@(_analyzing ? "Analysiere..." : "Cockpit erzeugen")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
@if (_result is not null)
|
||||
{
|
||||
<MudGrid Class="mb-4">
|
||||
<MudItem xs="12" md="3"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">Land</MudText><MudText Typo="Typo.h6">@_result.Summary.Land</MudText></MudPaper></MudItem>
|
||||
<MudItem xs="12" md="3"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">TSC</MudText><MudText Typo="Typo.h6">@_result.Summary.Tsc</MudText></MudPaper></MudItem>
|
||||
<MudItem xs="12" md="3"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">Umsatz</MudText><MudText Typo="Typo.h6">@_result.Summary.SalesValueTotal.ToString("N2")</MudText></MudPaper></MudItem>
|
||||
<MudItem xs="12" md="3"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">Geschätzte Marge</MudText><MudText Typo="Typo.h6">@($"{_result.Summary.EstimatedMarginPercent:F1}%")</MudText></MudPaper></MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" Class="mb-2">Management Aussagen</MudText>
|
||||
@foreach (var finding in _result.Findings)
|
||||
{
|
||||
<MudAlert Severity="@MapSeverity(finding.Severity)" Dense Variant="Variant.Outlined" Class="mb-2">
|
||||
<b>@finding.Title:</b> @finding.Detail
|
||||
</MudAlert>
|
||||
}
|
||||
</MudPaper>
|
||||
|
||||
<MudGrid Class="mb-4">
|
||||
<MudItem xs="12" md="4">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" Class="mb-2">Top Kunden</MudText>
|
||||
@foreach (var item in _result.TopCustomers)
|
||||
{
|
||||
<MudText Typo="Typo.body2">@($"{item.Label}: {item.Value:N2} ({item.SharePercent:F1}%)")</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" Class="mb-2">Top Produktgruppen</MudText>
|
||||
@foreach (var item in _result.TopProductGroups)
|
||||
{
|
||||
<MudText Typo="Typo.body2">@($"{item.Label}: {item.Value:N2} ({item.SharePercent:F1}%)")</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" Class="mb-2">Top Sales Owner</MudText>
|
||||
@foreach (var item in _result.TopSalesEmployees)
|
||||
{
|
||||
<MudText Typo="Typo.body2">@($"{item.Label}: {item.Value:N2} ({item.SharePercent:F1}%)")</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudText Typo="Typo.h6" Class="mb-2">Datenqualität</MudText>
|
||||
@foreach (var entry in _result.DataQualityCounts.OrderByDescending(x => x.Value))
|
||||
{
|
||||
<MudText Typo="Typo.body2">@($"{entry.Key}: {entry.Value}")</MudText>
|
||||
}
|
||||
</MudPaper>
|
||||
}
|
||||
|
||||
@code {
|
||||
private List<ManagementCockpitFileOption> _files = [];
|
||||
private string? _selectedFilePath;
|
||||
private ManagementCockpitResult? _result;
|
||||
private bool _loadingFiles;
|
||||
private bool _analyzing;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await ReloadFiles();
|
||||
}
|
||||
|
||||
private async Task ReloadFiles()
|
||||
{
|
||||
_loadingFiles = true;
|
||||
try
|
||||
{
|
||||
_files = await CockpitService.GetAvailableFilesAsync();
|
||||
_selectedFilePath ??= _files.FirstOrDefault()?.Path;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loadingFiles = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Analyze()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_selectedFilePath))
|
||||
return;
|
||||
|
||||
_analyzing = true;
|
||||
try
|
||||
{
|
||||
_result = await CockpitService.AnalyzeAsync(_selectedFilePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Cockpit konnte nicht erzeugt werden: {ex.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_analyzing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Severity MapSeverity(string severity) => severity switch
|
||||
{
|
||||
"Warning" => Severity.Warning,
|
||||
"Error" => Severity.Error,
|
||||
_ => Severity.Info
|
||||
};
|
||||
}
|
||||
@@ -168,6 +168,20 @@
|
||||
<MudItem xs="12" md="4">
|
||||
<MudSwitch @bind-Value="_exportSettings.TimerEnabled" Label="Timer aktiviert" Color="Color.Primary" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="4">
|
||||
<MudSwitch @bind-Value="_exportSettings.DebugLoggingEnabled" Label="Debug Live-Logging" Color="Color.Warning" />
|
||||
<MudText Typo="Typo.caption">
|
||||
Schreibt zusätzliche technische Fortschrittsmeldungen für HANA- und SAP-Lesevorgänge ins Dashboard und in die Logs.
|
||||
</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudTextField @bind-Value="_exportSettings.LocalSiteExportFolder" Label="Lokaler Standardpfad Standort-Dateien"
|
||||
HelperText="Wenn leer, wird ./output unter dem Programmverzeichnis verwendet." />
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="6">
|
||||
<MudTextField @bind-Value="_exportSettings.LocalConsolidatedExportFolder" Label="Lokaler Pfad Zentrale Datei"
|
||||
HelperText="Optional. Wenn leer, wird der Standardpfad der Standort-Dateien verwendet." />
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveExportSettings"
|
||||
StartIcon="@Icons.Material.Filled.Save">
|
||||
@@ -258,6 +272,9 @@
|
||||
existing.TimerHour = _exportSettings.TimerHour;
|
||||
existing.TimerMinute = _exportSettings.TimerMinute;
|
||||
existing.TimerEnabled = _exportSettings.TimerEnabled;
|
||||
existing.DebugLoggingEnabled = _exportSettings.DebugLoggingEnabled;
|
||||
existing.LocalSiteExportFolder = _exportSettings.LocalSiteExportFolder;
|
||||
existing.LocalConsolidatedExportFolder = _exportSettings.LocalConsolidatedExportFolder;
|
||||
existing.SapUsername = _exportSettings.SapUsername;
|
||||
existing.SapPassword = _exportSettings.SapPassword;
|
||||
existing.Bi1Username = _exportSettings.Bi1Username;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
@inject IDbContextFactory<AppDbContext> DbFactory
|
||||
@inject IHanaQueryService HanaService
|
||||
@inject ISapGatewayService SapGatewayService
|
||||
@inject IAppEventLogService AppEventLogService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IDialogService DialogService
|
||||
|
||||
@@ -149,6 +150,8 @@
|
||||
HelperText="Optional. Wenn leer, wird der zentrale Username des Quellsystems verwendet." />
|
||||
<MudTextField @bind-Value="_editingSite.PasswordOverride" Label="Password Override" InputType="InputType.Password"
|
||||
HelperText="Optional. Wenn leer, wird das zentrale Passwort des Quellsystems verwendet." />
|
||||
<MudTextField @bind-Value="_editingSite.LocalExportFolderOverride" Label="Lokaler Exportpfad Override"
|
||||
HelperText="Optional. Wenn leer, wird der zentrale Standardpfad für Standort-Dateien verwendet." />
|
||||
<MudCheckBox @bind-Value="_editingSite.IsActive" Label="Aktiv" />
|
||||
|
||||
<MudDivider Class="my-4" />
|
||||
@@ -216,7 +219,13 @@
|
||||
<MudDivider Class="my-4" />
|
||||
<MudStack Row Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-2">
|
||||
<MudText Typo="Typo.h6">SAP Joins</MudText>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddSapJoin">Join hinzufügen</MudButton>
|
||||
<MudStack Row Spacing="2">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" StartIcon="@Icons.Material.Filled.AutoFixHigh"
|
||||
OnClick="AutoMatchSapJoins">
|
||||
Auto-Match
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddSapJoin">Join hinzufügen</MudButton>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
<MudTable Items="_sapJoins" Dense Hover Striped>
|
||||
<HeaderContent>
|
||||
@@ -237,7 +246,18 @@
|
||||
}
|
||||
</MudSelect>
|
||||
</MudTd>
|
||||
<MudTd><MudTextField @bind-Value="context.LeftKeys" Dense Placeholder="VBELN,POSNR" /></MudTd>
|
||||
<MudTd>
|
||||
<MudSelect T="string"
|
||||
SelectedValues="GetSelectedJoinKeys(context.LeftKeys)"
|
||||
SelectedValuesChanged="@(values => context.LeftKeys = string.Join(',', values))"
|
||||
MultiSelection="true"
|
||||
Dense>
|
||||
@foreach (var field in GetAvailableJoinFields(context.LeftAlias, context.LeftKeys))
|
||||
{
|
||||
<MudSelectItem Value="@field">@field</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudSelect @bind-Value="context.RightAlias" Dense>
|
||||
@foreach (var alias in GetSapAliases())
|
||||
@@ -246,7 +266,18 @@
|
||||
}
|
||||
</MudSelect>
|
||||
</MudTd>
|
||||
<MudTd><MudTextField @bind-Value="context.RightKeys" Dense Placeholder="VBELN,POSNR" /></MudTd>
|
||||
<MudTd>
|
||||
<MudSelect T="string"
|
||||
SelectedValues="GetSelectedJoinKeys(context.RightKeys)"
|
||||
SelectedValuesChanged="@(values => context.RightKeys = string.Join(',', values))"
|
||||
MultiSelection="true"
|
||||
Dense>
|
||||
@foreach (var field in GetAvailableJoinFields(context.RightAlias, context.RightKeys))
|
||||
{
|
||||
<MudSelectItem Value="@field">@field</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudSelect @bind-Value="context.JoinType" Dense>
|
||||
<MudSelectItem Value="@("Left")">Left</MudSelectItem>
|
||||
@@ -260,8 +291,25 @@
|
||||
<MudDivider Class="my-4" />
|
||||
<MudStack Row Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-2">
|
||||
<MudText Typo="Typo.h6">Feldmappings ins zentrale Schema</MudText>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddSapMapping">Mapping hinzufügen</MudButton>
|
||||
<MudStack Row Spacing="2">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" StartIcon="@Icons.Material.Filled.Schema"
|
||||
OnClick="RefreshSapSourceFields" Disabled="_refreshingSapSourceFields">
|
||||
@if (_refreshingSapSourceFields)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate Class="mr-2" />
|
||||
@("Lade Felder...")
|
||||
}
|
||||
else
|
||||
{
|
||||
@("Felder aus Quellen laden")
|
||||
}
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddSapMapping">Mapping hinzufügen</MudButton>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.caption" Class="mb-2">
|
||||
Source Expressions werden aus den hinzugefügten SAP-Quellen als `Alias.Feldname` gelesen. Vorhandene manuelle Werte bleiben auswählbar.
|
||||
</MudText>
|
||||
<MudTable Items="_sapMappings" Dense Hover Striped>
|
||||
<HeaderContent>
|
||||
<MudTh>Zielfeld</MudTh>
|
||||
@@ -279,7 +327,14 @@
|
||||
}
|
||||
</MudSelect>
|
||||
</MudTd>
|
||||
<MudTd><MudTextField @bind-Value="context.SourceExpression" Dense Placeholder="VBAK.VBELN oder =SAP" /></MudTd>
|
||||
<MudTd>
|
||||
<MudSelect T="string" @bind-Value="context.SourceExpression" Dense>
|
||||
@foreach (var expression in GetAvailableSourceExpressions(context.SourceExpression))
|
||||
{
|
||||
<MudSelectItem Value="@expression">@expression</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudTd>
|
||||
<MudTd><MudCheckBox @bind-Value="context.IsRequired" Dense /></MudTd>
|
||||
<MudTd><MudCheckBox @bind-Value="context.IsActive" Dense /></MudTd>
|
||||
<MudTd><MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small" OnClick="() => RemoveSapMapping(context)" /></MudTd>
|
||||
@@ -321,6 +376,8 @@
|
||||
private List<HanaServer> _servers = new();
|
||||
private List<Site> _sites = new();
|
||||
private List<string> _sapEntitySetsCache = [];
|
||||
private List<string> _sapAvailableSourceExpressions = [];
|
||||
private Dictionary<string, List<string>> _sapSourceFieldMap = new(StringComparer.OrdinalIgnoreCase);
|
||||
private List<SapSourceDefinition> _sapSources = [];
|
||||
private List<SapJoinDefinition> _sapJoins = [];
|
||||
private List<SapFieldMapping> _sapMappings = [];
|
||||
@@ -334,6 +391,7 @@
|
||||
private bool _serverDialogVisible;
|
||||
private bool _siteDialogVisible;
|
||||
private bool _refreshingSapEntitySets;
|
||||
private bool _refreshingSapSourceFields;
|
||||
private bool _savingServer;
|
||||
private bool _savingSite;
|
||||
private readonly DialogOptions _dialogOptions = new() { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
@@ -426,6 +484,8 @@
|
||||
|
||||
private async Task TestServerConnection(HanaServer server)
|
||||
{
|
||||
await AppEventLogService.WriteAsync("HANA", "Server-Test aus UI gestartet",
|
||||
details: server.GetConnectionStringPreview());
|
||||
var result = await Task.Run(() => HanaService.TestConnectionDetailed(server));
|
||||
_connectionStatus[server.Id] = result;
|
||||
|
||||
@@ -457,6 +517,8 @@
|
||||
HanaServerId = null
|
||||
};
|
||||
_sapEntitySetsCache = [];
|
||||
_sapAvailableSourceExpressions = [];
|
||||
_sapSourceFieldMap = new(StringComparer.OrdinalIgnoreCase);
|
||||
_sapSources = [];
|
||||
_sapJoins = [];
|
||||
_sapMappings = [];
|
||||
@@ -476,6 +538,7 @@
|
||||
SourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) ? "SAP" : site.SourceSystem,
|
||||
UsernameOverride = site.UsernameOverride,
|
||||
PasswordOverride = site.PasswordOverride,
|
||||
LocalExportFolderOverride = site.LocalExportFolderOverride,
|
||||
SapServiceUrl = site.SapServiceUrl,
|
||||
SapEntitySet = site.SapEntitySet,
|
||||
SapEntitySetsCache = site.SapEntitySetsCache,
|
||||
@@ -487,6 +550,8 @@
|
||||
_sapSources = db.SapSourceDefinitions.Where(s => s.SiteId == site.Id).OrderBy(s => s.SortOrder).ThenBy(s => s.Id).ToList();
|
||||
_sapJoins = db.SapJoinDefinitions.Where(j => j.SiteId == site.Id).OrderBy(j => j.SortOrder).ThenBy(j => j.Id).ToList();
|
||||
_sapMappings = db.SapFieldMappings.Where(m => m.SiteId == site.Id).OrderBy(m => m.SortOrder).ThenBy(m => m.Id).ToList();
|
||||
_sapAvailableSourceExpressions = BuildSourceExpressionsFromMappings();
|
||||
_sapSourceFieldMap = BuildSourceFieldMapFromJoins();
|
||||
_editingSiteServer = site.HanaServer is null
|
||||
? CreateDefaultSiteServer(site)
|
||||
: CloneServer(site.HanaServer);
|
||||
@@ -522,6 +587,7 @@
|
||||
existing.SourceSystem = _editingSite.SourceSystem;
|
||||
existing.UsernameOverride = _editingSite.UsernameOverride;
|
||||
existing.PasswordOverride = _editingSite.PasswordOverride;
|
||||
existing.LocalExportFolderOverride = _editingSite.LocalExportFolderOverride;
|
||||
existing.SapServiceUrl = _editingSite.SapServiceUrl;
|
||||
existing.SapEntitySet = _editingSite.SapEntitySet;
|
||||
existing.SapEntitySetsCache = _editingSite.SapEntitySetsCache;
|
||||
@@ -629,6 +695,7 @@
|
||||
: _editingSiteServer.Name.Trim();
|
||||
_editingSite.UsernameOverride = _editingSite.UsernameOverride.Trim();
|
||||
_editingSite.PasswordOverride = _editingSite.PasswordOverride.Trim();
|
||||
_editingSite.LocalExportFolderOverride = _editingSite.LocalExportFolderOverride.Trim();
|
||||
_editingSite.SapServiceUrl = _editingSite.SapServiceUrl.Trim();
|
||||
_editingSite.SapEntitySet = _editingSite.SapEntitySet.Trim();
|
||||
_editingSiteServer.Host = _editingSiteServer.Host.Trim();
|
||||
@@ -698,6 +765,8 @@
|
||||
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: _editingSite.SapServiceUrl);
|
||||
var entitySets = await SapGatewayService.GetEntitySetsAsync(_editingSite.SapServiceUrl, username.Trim(), password.Trim());
|
||||
_sapEntitySetsCache = entitySets;
|
||||
_editingSite.SapEntitySetsCache = SerializeSapEntitySets(entitySets);
|
||||
@@ -710,10 +779,14 @@
|
||||
}
|
||||
|
||||
Snackbar.Add($"{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)
|
||||
{
|
||||
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
|
||||
{
|
||||
@@ -782,6 +855,83 @@
|
||||
});
|
||||
}
|
||||
|
||||
private void AutoMatchSapJoins()
|
||||
{
|
||||
var activeSources = _sapSources
|
||||
.Where(s => s.IsActive && !string.IsNullOrWhiteSpace(s.Alias))
|
||||
.OrderBy(s => s.SortOrder)
|
||||
.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)
|
||||
{
|
||||
_sapJoins.Remove(join);
|
||||
@@ -792,6 +942,7 @@
|
||||
_sapMappings.Add(new SapFieldMapping
|
||||
{
|
||||
TargetField = _salesRecordFields.First(),
|
||||
SourceExpression = _sapAvailableSourceExpressions.FirstOrDefault() ?? "=SAP",
|
||||
IsActive = true,
|
||||
SortOrder = _sapMappings.Count
|
||||
});
|
||||
@@ -847,4 +998,147 @@
|
||||
if (_sapSources.Count > 0 && _sapSources.All(s => !s.IsPrimary))
|
||||
_sapSources[0].IsPrimary = true;
|
||||
}
|
||||
|
||||
private async Task RefreshSapSourceFields()
|
||||
{
|
||||
if (_refreshingSapSourceFields)
|
||||
return;
|
||||
|
||||
_refreshingSapSourceFields = true;
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_editingSite.SapServiceUrl))
|
||||
throw new InvalidOperationException("SAP Service URL muss gesetzt sein.");
|
||||
|
||||
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.");
|
||||
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
var settings = await db.ExportSettings.FirstOrDefaultAsync() ?? new();
|
||||
var username = string.IsNullOrWhiteSpace(_editingSite.UsernameOverride) ? settings.SapUsername : _editingSite.UsernameOverride;
|
||||
var password = string.IsNullOrWhiteSpace(_editingSite.PasswordOverride) ? settings.SapPassword : _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(_editingSite.SapServiceUrl, 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);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add(ex.Message, Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_refreshingSapSourceFields = false;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetAvailableSourceExpressions(string? 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()
|
||||
=> _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()
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
=> keys?
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase)
|
||||
?? [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user