@page "/standorte"
@attribute [Authorize(Policy = TrafagSalesExporter.Security.SecurityPolicies.AdminOnly)]
@using Microsoft.AspNetCore.Components.Forms
@using System.Text.Json
@using System.Reflection
@using TrafagSalesExporter.Models
@using TrafagSalesExporter.Services
@inject IStandortePageService StandortePageService
@inject IStandorteSapEditorService SapEditorService
@inject ISnackbar Snackbar
@inject IDialogService DialogService
Standorte
Standorte
Zentrale HANA-Technik
Hier erscheinen nur Quellsysteme mit Anschlussart HANA. SAP wird zentral unter Settings -> Quellsysteme gepflegt.
Standorte mit `BI1` oder `SAGE` verwenden diese technischen HANA-Werte automatisch. Im Standort selbst bleiben nur Schema, TSC, Land und optionale Username-/Password-Overrides.
Neue HANA-Zeilen entstehen aus den zentral gepflegten Quellsystemen. Falls hier etwas fehlt, lege das Quellsystem in Settings -> Quellsysteme mit Anschlussart `HANA` an.
Quellsystem
Name
Host
Port
Verbindungsstatus
Aktionen
@context.SourceSystem
@context.Name
@context.Host
@context.Port
@if (_connectionStatus.TryGetValue(context.Id, out var status))
{
@(status.Success ? "OK" : "Fehler") - @status.Stage
}
else
{
Nicht getestet
}
Standorte (Sites)
Neuen Standort hinzufügen
Land
TSC
Schema
Quellsystem
Quelle
Aktiv
Aktionen
@context.Land
@context.TSC
@context.Schema
@context.SourceSystem
@GetConnectionTarget(context)
@if (context.IsActive)
{
}
else
{
}
Zentrale HANA-Technik bearbeiten
Abbrechen
Speichern
@(_editingSite.Id == 0 ? "Standort hinzufügen" : "Standort bearbeiten")
@if (UsesHanaConnection())
{
@if (_loadingSchemas)
{
@("Lade Schemas...")
}
else
{
@("Schemas laden")
}
@if (_availableSchemas.Count > 0)
{
@foreach (var schema in _availableSchemas)
{
@schema
}
}
Die Liste wird aus der zentralen HANA-Verbindung des Quellsystems gelesen und auf typische B1-Schemas eingeschraenkt.
}
@foreach (var system in GetAvailableSourceSystems())
{
@GetSourceSystemLabel(system)
}
@if (IsMappedSourceSite())
{
@GetMappingSectionTitle()
Quellen und Feldmappings werden grafisch gepflegt. Bei SAP Gateway sind Quellen Entity Sets; bei HANA sind Quellen Tabellen oder Views im gewaehlten Schema.
@if (IsSapSite())
{
Zentrale SAP Service URL: @GetCentralSapServiceUrlSummary(_editingSite.SourceSystem)
}
else
{
Zentrale HANA-Verbindung: @GetCentralHanaSummary(_editingSite.SourceSystem)
}
@if (_refreshingSapEntitySets)
{
@("Lade...")
}
else
{
@(IsSapSite() ? "Entity Sets refreshen" : "Tabellen/Views refreshen")
}
@if (_editingSite.SapEntitySetsRefreshedAtUtc.HasValue)
{
Letzter Refresh: @_editingSite.SapEntitySetsRefreshedAtUtc.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss")
}
Quellen
Quelle hinzufügen
Pro Quelle Alias und Entity Set bzw. HANA Tabelle/View definieren. Joins verwenden links/rechts kommagetrennte Schluesselfelder wie `VBELN,POSNR`. Feldmappings erwarten `Alias.Feldname` oder Konstanten wie `=SAP` / `=HANA`.
Alias
@(IsSapSite() ? "Entity Set" : "Tabelle/View")
Primär
Aktiv
Aktionen
@foreach (var entitySet in _sapEntitySetsCache)
{
@entitySet
}
SAP Joins
Auto-Match
Join hinzufügen
Links
Left Keys
Rechts
Right Keys
Typ
Aktiv
Aktionen
@foreach (var alias in GetSapAliases())
{
@alias
}
@foreach (var field in GetAvailableJoinFields(context.LeftAlias, context.LeftKeys))
{
@field
}
@foreach (var alias in GetSapAliases())
{
@alias
}
@foreach (var field in GetAvailableJoinFields(context.RightAlias, context.RightKeys))
{
@field
}
Left
Feldmappings ins zentrale Schema
@if (_refreshingSapSourceFields)
{
@("Lade Felder...")
}
else
{
@("Felder aus Quellen laden")
}
Mapping hinzufügen
Source Expressions werden aus den hinzugefuegten Quellen als `Alias.Feldname` gelesen. Vorhandene manuelle Werte bleiben auswaehlbar.
Zielfeld
Source Expression
Pflicht
Aktiv
Aktionen
@foreach (var field in _salesRecordFields)
{
@field
}
@foreach (var expression in GetAvailableSourceExpressions(context.SourceExpression))
{
@expression
}
}
else if (IsManualExcelSite())
{
Manueller Excel-Import
Für diesen Standort wird keine SAP- oder HANA-Verbindung verwendet. Es wird die hier hinterlegte Excel-Datei gelesen und in `CentralSalesRecords` übernommen.
Pfad pruefen
@if (_uploadingManualImport)
{
Datei wird hochgeladen...
}
@if (!string.IsNullOrWhiteSpace(_editingSite.ManualImportFilePath))
{
Datei: @_editingSite.ManualImportFilePath
Letzter Upload: @(_editingSite.ManualImportLastUploadedAtUtc?.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss") ?? "-")
}
else
{
Noch keine Datei hinterlegt.
}
Excel-Spaltenmapping
@if (_loadingManualExcelHeaders)
{
@("Lade Spalten...")
}
else
{
@("Spalten aus Excel laden")
}
Auto-Match
Mapping hinzufügen
Wenn hier Mappings gepflegt sind, werden diese vor dem Standardformat verwendet. Konstanten sind mit `=Wert` moeglich, z. B. `=Manual Excel`.
Zielfeld
Excel-Spalte / Konstante
Pflicht
Aktiv
Aktionen
@foreach (var field in _salesRecordFields)
{
@field
}
@foreach (var header in GetAvailableManualExcelHeaders(context.SourceHeader))
{
@header
}
}
else
{
HANA-Verbindung
Die technische HANA-Verbindung kommt aus der zentralen HANA-Konfiguration des Quellsystems. Im Standort selbst pflegst du nur fachliche Standortdaten und optionale Username-/Password-Overrides.
Aktive Zentralverbindung: @GetCentralHanaSummary(_editingSite.SourceSystem)
Host, Port, SSL und technische Parameter bearbeitest du oben in der zentralen HANA-Konfiguration.
}
Abbrechen
Speichern
@code {
private readonly Dictionary _connectionStatus = new();
private List _servers = new();
private List _sites = new();
private List _sourceSystemDefinitions = new();
private List _sapEntitySetsCache = [];
private List _availableSchemas = [];
private List _sapAvailableSourceExpressions = [];
private Dictionary> _sapSourceFieldMap = new(StringComparer.OrdinalIgnoreCase);
private List _sapSources = [];
private List _sapJoins = [];
private List _sapMappings = [];
private List _manualExcelMappings = [];
private List _manualExcelHeaders = [];
private readonly string[] _salesRecordFields = typeof(SalesRecord)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Select(p => p.Name)
.ToArray();
private HanaServer _editingServer = new();
private Site _editingSite = new();
private bool _serverDialogVisible;
private bool _siteDialogVisible;
private bool _refreshingSapEntitySets;
private bool _refreshingSapSourceFields;
private bool _savingServer;
private bool _savingSite;
private bool _loadingSchemas;
private bool _uploadingManualImport;
private bool _loadingManualExcelHeaders;
private readonly DialogOptions _dialogOptions = new() { MaxWidth = MaxWidth.Small, FullWidth = true };
protected override async Task OnInitializedAsync()
{
await LoadDataAsync();
}
private async Task LoadDataAsync()
{
var state = await StandortePageService.LoadAsync();
_sourceSystemDefinitions = state.SourceSystems;
_servers = state.Servers;
_sites = state.Sites;
}
private void EditServer(HanaServer server)
{
_editingServer = CloneServer(server);
_serverDialogVisible = true;
}
private async Task SaveServer()
{
if (_savingServer)
return;
_savingServer = true;
try
{
await StandortePageService.SaveServerAsync(_editingServer, GetHanaSourceSystemCodes());
_serverDialogVisible = false;
await LoadDataAsync();
Snackbar.Add("Server gespeichert", Severity.Success);
}
finally
{
_savingServer = false;
}
}
private async Task DeleteServer(HanaServer server)
{
if (IsHanaSourceSystem(server.SourceSystem))
{
Snackbar.Add($"Die zentrale HANA-Konfiguration fuer {server.SourceSystem} kann nicht geloescht werden.", Severity.Warning);
return;
}
var result = await DialogService.ShowMessageBox(
"Server löschen",
$"Server '{server.Name}' wirklich löschen?",
yesText: "Löschen", cancelText: "Abbrechen");
if (result != true) return;
try
{
await StandortePageService.DeleteServerAsync(server);
}
catch (Exception ex)
{
Snackbar.Add($"Server konnte nicht gelöscht werden: {ex.Message}", Severity.Error);
return;
}
await LoadDataAsync();
Snackbar.Add("Server gelöscht", Severity.Info);
}
private async Task TestServerConnection(HanaServer server)
{
try
{
var result = await StandortePageService.TestServerConnectionAsync(server);
_connectionStatus[server.Id] = result;
Snackbar.Add(
result.Success
? $"Verbindung zu '{server.Name}' erfolgreich."
: $"{server.Name}: {result.ExceptionType} - {result.ErrorMessage}",
result.Success ? Severity.Success : Severity.Error);
}
catch (Exception ex)
{
Snackbar.Add(ex.Message, Severity.Warning);
}
}
private static string BuildStatusTooltip(ConnectionTestResult status)
{
var stamp = status.TestedAtUtc.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
if (status.Success)
return $"Letzter Test: {stamp}\nStage: {status.Stage}\n{status.ConnectionStringPreview}";
return $"Letzter Test: {stamp}\nStage: {status.Stage}\nFehler: {status.ErrorMessage}\n{status.ConnectionStringPreview}";
}
private void AddSite()
{
_editingSite = new Site
{
IsActive = true,
SourceSystem = GetAvailableSourceSystems().FirstOrDefault()?.Code ?? "SAP",
HanaServerId = null,
ManualImportFilePath = string.Empty
};
_availableSchemas = [];
_sapEntitySetsCache = [];
_sapAvailableSourceExpressions = [];
_sapSourceFieldMap = new(StringComparer.OrdinalIgnoreCase);
_sapSources = [];
_sapJoins = [];
_sapMappings = [];
_manualExcelMappings = [];
_manualExcelHeaders = [];
_siteDialogVisible = true;
}
private void EditSite(Site site)
{
_ = EditSiteAsync(site);
}
private async Task EditSiteAsync(Site site)
{
var editorState = await StandortePageService.LoadSiteEditorAsync(site, GetAvailableSourceSystems());
_editingSite = editorState.Site;
_availableSchemas = [];
_sapEntitySetsCache = editorState.SapEntitySets;
_sapSources = editorState.SapSources;
_sapJoins = editorState.SapJoins;
_sapMappings = editorState.SapMappings;
_manualExcelMappings = editorState.ManualExcelMappings;
_manualExcelHeaders = BuildHeadersFromManualExcelMappings();
_sapAvailableSourceExpressions = BuildSourceExpressionsFromMappings();
_sapSourceFieldMap = BuildSourceFieldMapFromJoins();
_siteDialogVisible = true;
}
private async Task SaveSite()
{
if (_savingSite)
return;
_savingSite = true;
try
{
await StandortePageService.SaveSiteAsync(_editingSite, UsesHanaConnection(), IsMappedSourceSite(), IsManualExcelSite(), _sapSources, _sapJoins, _sapMappings, _manualExcelMappings, _sapEntitySetsCache);
_siteDialogVisible = false;
await LoadDataAsync();
Snackbar.Add("Standort gespeichert", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"Speichern fehlgeschlagen: {ex.Message}", Severity.Error);
}
finally
{
_savingSite = false;
}
}
private async Task DeleteSite(Site site)
{
var result = await DialogService.ShowMessageBox(
"Standort löschen",
$"Standort '{site.Land}' wirklich löschen?",
yesText: "Löschen", cancelText: "Abbrechen");
if (result != true) return;
await StandortePageService.DeleteSiteAsync(site);
await LoadDataAsync();
Snackbar.Add("Standort gelöscht", Severity.Info);
}
private static string GetServerNode(HanaServer? server)
{
if (server is null || string.IsNullOrWhiteSpace(server.Host))
return "-";
return server.Host.Contains(':', StringComparison.Ordinal) ? server.Host : $"{server.Host}:{server.Port}";
}
private static HanaServer CloneServer(HanaServer server)
{
return new HanaServer
{
Id = server.Id,
SourceSystem = server.SourceSystem,
Name = server.Name,
Host = server.Host,
Port = server.Port,
Username = string.Empty,
Password = string.Empty,
DatabaseName = server.DatabaseName,
UseSsl = server.UseSsl,
ValidateCertificate = server.ValidateCertificate,
AdditionalParams = server.AdditionalParams
};
}
private Task OnSchemaSelected(string schema)
{
_editingSite.Schema = schema;
return Task.CompletedTask;
}
private Task OnSourceSystemChanged(string value)
{
_editingSite.SourceSystem = value;
_availableSchemas = [];
return Task.CompletedTask;
}
private IEnumerable GetAvailableSourceSystems()
=> _sourceSystemDefinitions
.Where(x => x.IsActive || string.Equals(x.Code, _editingSite.SourceSystem, StringComparison.OrdinalIgnoreCase))
.OrderBy(x => x.DisplayName)
.ThenBy(x => x.Code);
private List GetHanaSourceSystemCodes()
=> _sourceSystemDefinitions
.Where(x => string.Equals(x.ConnectionKind, SourceSystemConnectionKinds.Hana, StringComparison.OrdinalIgnoreCase))
.Select(x => x.Code)
.OrderBy(x => x)
.ToList();
private string GetSourceSystemConnectionKind(string? sourceSystem)
=> _sourceSystemDefinitions
.FirstOrDefault(x => string.Equals(x.Code, sourceSystem, StringComparison.OrdinalIgnoreCase))
?.ConnectionKind
?? SourceSystemConnectionKinds.SapGateway;
private bool IsHanaSourceSystem(string? sourceSystem)
=> string.Equals(GetSourceSystemConnectionKind(sourceSystem), SourceSystemConnectionKinds.Hana, StringComparison.OrdinalIgnoreCase);
private bool IsSapSite()
=> string.Equals(GetSourceSystemConnectionKind(_editingSite.SourceSystem), SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase);
private bool IsMappedSourceSite()
=> IsSapSite() || UsesHanaConnection();
private bool IsManualExcelSite()
=> string.Equals(GetSourceSystemConnectionKind(_editingSite.SourceSystem), SourceSystemConnectionKinds.ManualExcel, StringComparison.OrdinalIgnoreCase);
private bool UsesHanaConnection() => IsHanaSourceSystem(_editingSite.SourceSystem);
private string GetMappingSectionTitle()
=> IsSapSite() ? "SAP Gateway Mapping" : "HANA Quellen und Feldmapping";
private string GetSourceSystemLabel(SourceSystemDefinition definition)
=> string.IsNullOrWhiteSpace(definition.DisplayName) ? definition.Code : $"{definition.DisplayName} ({definition.Code})";
private string GetConnectionTarget(Site site)
{
var connectionKind = GetSourceSystemConnectionKind(site.SourceSystem);
if (string.Equals(connectionKind, SourceSystemConnectionKinds.SapGateway, StringComparison.OrdinalIgnoreCase))
return GetEffectiveSapServiceUrl(site);
if (string.Equals(connectionKind, SourceSystemConnectionKinds.ManualExcel, StringComparison.OrdinalIgnoreCase))
return string.IsNullOrWhiteSpace(site.ManualImportFilePath) ? "-" : Path.GetFileName(site.ManualImportFilePath);
return GetServerNode(site.HanaServer);
}
private string GetEffectiveSapServiceUrl(Site site)
{
if (!string.IsNullOrWhiteSpace(site.SapServiceUrl))
return site.SapServiceUrl;
var sourceDefinition = _sourceSystemDefinitions
.FirstOrDefault(x => string.Equals(x.Code, site.SourceSystem, StringComparison.OrdinalIgnoreCase));
return string.IsNullOrWhiteSpace(sourceDefinition?.CentralServiceUrl) ? "-" : sourceDefinition.CentralServiceUrl;
}
private string GetCentralSapServiceUrlSummary(string sourceSystem)
{
var sourceDefinition = _sourceSystemDefinitions
.FirstOrDefault(x => string.Equals(x.Code, sourceSystem, StringComparison.OrdinalIgnoreCase));
return string.IsNullOrWhiteSpace(sourceDefinition?.CentralServiceUrl) ? "-" : sourceDefinition.CentralServiceUrl;
}
private string GetCentralHanaSummary(string sourceSystem)
{
var normalizedSourceSystem = string.IsNullOrWhiteSpace(sourceSystem) ? string.Empty : sourceSystem.Trim().ToUpperInvariant();
var centralServer = _servers.FirstOrDefault(x => x.SourceSystem == normalizedSourceSystem);
if (centralServer is null || string.IsNullOrWhiteSpace(centralServer.Host))
return $"keine zentrale HANA-Konfiguration fuer {normalizedSourceSystem}";
return $"{centralServer.Name} | {GetServerNode(centralServer)}";
}
private async Task LoadAvailableSchemasAsync()
{
if (_loadingSchemas)
return;
_loadingSchemas = true;
try
{
_availableSchemas = await StandortePageService.LoadAvailableSchemasAsync(_editingSite);
if (_availableSchemas.Count == 0)
{
Snackbar.Add("Keine passenden Schemas gefunden.", Severity.Info);
return;
}
if (string.IsNullOrWhiteSpace(_editingSite.Schema) ||
!_availableSchemas.Contains(_editingSite.Schema, StringComparer.OrdinalIgnoreCase))
{
_editingSite.Schema = _availableSchemas[0];
}
Snackbar.Add($"{_availableSchemas.Count} Schemas geladen.", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"Schemas laden fehlgeschlagen: {ex.Message}", Severity.Error);
}
finally
{
_loadingSchemas = false;
}
}
private async Task RefreshSapEntitySets()
{
if (_refreshingSapEntitySets)
return;
_refreshingSapEntitySets = true;
try
{
var result = await StandortePageService.RefreshSapEntitySetsAsync(_editingSite);
_sapEntitySetsCache = result.EntitySets;
_editingSite.SapEntitySetsCache = SerializeSapEntitySets(result.EntitySets);
_editingSite.SapEntitySetsRefreshedAtUtc = result.RefreshedAtUtc;
if (!string.IsNullOrWhiteSpace(_editingSite.SapEntitySet) &&
!_sapEntitySetsCache.Contains(_editingSite.SapEntitySet, StringComparer.OrdinalIgnoreCase))
{
_editingSite.SapEntitySet = string.Empty;
}
Snackbar.Add($"{result.EntitySets.Count} SAP Entity Sets geladen.", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add(ex.Message, Severity.Error);
}
finally
{
_refreshingSapEntitySets = false;
}
}
private void CloseServerDialog()
{
if (_savingServer)
return;
_serverDialogVisible = false;
}
private void CloseSiteDialog()
{
if (_savingSite || _refreshingSapEntitySets || _uploadingManualImport || _loadingManualExcelHeaders)
return;
_siteDialogVisible = false;
}
private async Task UploadManualImportFileAsync(InputFileChangeEventArgs args)
{
if (_uploadingManualImport)
return;
var file = args.File;
if (file is null)
return;
_uploadingManualImport = true;
try
{
var extension = Path.GetExtension(file.Name);
if (!string.Equals(extension, ".xlsx", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Bitte eine Excel-Datei mit Endung .xlsx auswählen.");
}
var uploadDirectory = Path.Combine(AppContext.BaseDirectory, "manual-imports");
Directory.CreateDirectory(uploadDirectory);
var safeBaseName = string.Concat(Path.GetFileNameWithoutExtension(file.Name).Select(ch =>
char.IsLetterOrDigit(ch) || ch == '-' || ch == '_' ? ch : '_'));
if (string.IsNullOrWhiteSpace(safeBaseName))
safeBaseName = "manual_import";
var targetPath = Path.Combine(uploadDirectory, $"{safeBaseName}_{Guid.NewGuid():N}{extension}");
await using (var sourceStream = file.OpenReadStream(maxAllowedSize: 50 * 1024 * 1024))
await using (var targetStream = File.Create(targetPath))
{
await sourceStream.CopyToAsync(targetStream);
}
_editingSite.ManualImportFilePath = targetPath;
_editingSite.ManualImportLastUploadedAtUtc = DateTime.UtcNow;
Snackbar.Add("Excel-Datei hochgeladen.", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"Upload fehlgeschlagen: {ex.Message}", Severity.Error);
}
finally
{
_uploadingManualImport = false;
}
}
private async Task ValidateManualImportPathAsync()
{
try
{
_editingSite.ManualImportLastUploadedAtUtc = await StandortePageService.ValidateManualImportPathAsync(_editingSite.ManualImportFilePath);
Snackbar.Add("Dateipfad ist gueltig und die Excel-Datei ist erreichbar.", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"Pfadpruefung fehlgeschlagen: {ex.Message}", Severity.Error);
}
}
private async Task LoadManualExcelHeadersAsync()
{
if (_loadingManualExcelHeaders)
return;
_loadingManualExcelHeaders = true;
try
{
_manualExcelHeaders = await StandortePageService.LoadManualExcelHeadersAsync(_editingSite.ManualImportFilePath);
Snackbar.Add($"{_manualExcelHeaders.Count} Excel-Spalten geladen.", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"Spalten laden fehlgeschlagen: {ex.Message}", Severity.Error);
}
finally
{
_loadingManualExcelHeaders = false;
}
}
private void AddManualExcelMapping()
{
_manualExcelMappings.Add(new ManualExcelColumnMapping
{
TargetField = _salesRecordFields.First(),
SourceHeader = GetAvailableManualExcelHeaders(null).FirstOrDefault() ?? string.Empty,
IsActive = true,
SortOrder = _manualExcelMappings.Count
});
}
private void RemoveManualExcelMapping(ManualExcelColumnMapping mapping)
=> _manualExcelMappings.Remove(mapping);
private void AutoMatchManualExcelMappings()
{
if (_manualExcelHeaders.Count == 0)
{
Snackbar.Add("Bitte zuerst 'Spalten aus Excel laden' ausfuehren.", Severity.Warning);
return;
}
var suggestions = BuildManualExcelAutoMatchSuggestions();
var addedOrUpdated = 0;
foreach (var (targetField, sourceHeader) in suggestions)
{
var existing = _manualExcelMappings.FirstOrDefault(m =>
string.Equals(m.TargetField, targetField, StringComparison.OrdinalIgnoreCase));
if (existing is null)
{
_manualExcelMappings.Add(new ManualExcelColumnMapping
{
TargetField = targetField,
SourceHeader = sourceHeader,
IsActive = true,
IsRequired = IsImportantManualExcelField(targetField),
SortOrder = _manualExcelMappings.Count
});
}
else
{
existing.SourceHeader = sourceHeader;
existing.IsActive = true;
}
addedOrUpdated++;
}
Snackbar.Add(
addedOrUpdated == 0 ? "Keine passenden Spalten gefunden." : $"{addedOrUpdated} Mapping-Vorschlaege gesetzt.",
addedOrUpdated == 0 ? Severity.Info : Severity.Success);
}
private List<(string TargetField, string SourceHeader)> BuildManualExcelAutoMatchSuggestions()
{
var headerByNormalized = _manualExcelHeaders
.GroupBy(NormalizeHeader, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
var aliases = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
[nameof(SalesRecord.ExtractionDate)] = ["Export-Datum", "Extraction Date"],
[nameof(SalesRecord.InvoiceNumber)] = ["Belegnummer", "Invoice Number"],
[nameof(SalesRecord.PositionOnInvoice)] = ["Position", "Position on invoice"],
[nameof(SalesRecord.Material)] = ["ArtikelNummer", "Material", "Groesse"],
[nameof(SalesRecord.Name)] = ["ArtikelBezeichnung", "Name"],
[nameof(SalesRecord.ProductGroup)] = ["Warengruppen-Bezeichnung", "Product Group"],
[nameof(SalesRecord.Quantity)] = ["Anz. VE", "Quantity"],
[nameof(SalesRecord.SupplierNumber)] = ["Lieferanten Nummer", "Supplier number"],
[nameof(SalesRecord.SupplierName)] = ["Name Lieferant", "Supplier name"],
[nameof(SalesRecord.SupplierCountry)] = ["Land Lieferant", "Supplier country"],
[nameof(SalesRecord.CustomerNumber)] = ["AdressNummer-Kunde", "Customer number"],
[nameof(SalesRecord.CustomerName)] = ["Name Kunde", "Customer name"],
[nameof(SalesRecord.CustomerCountry)] = ["Land Kunde", "Customer country"],
[nameof(SalesRecord.CustomerIndustry)] = ["Branche", "Customer Industry"],
[nameof(SalesRecord.StandardCost)] = ["EinstandsPreis", "Standard cost"],
[nameof(SalesRecord.StandardCostCurrency)] = ["Währung", "Waehrung", "Standard Cost Currency"],
[nameof(SalesRecord.PurchaseOrderNumber)] = ["BestellNummer", "Purchase Order number"],
[nameof(SalesRecord.SalesPriceValue)] = ["NettoPreisGesamtX", "Sales Price/Value"],
[nameof(SalesRecord.SalesCurrency)] = ["Währung", "Waehrung", "Sales Currency"],
[nameof(SalesRecord.DocumentCurrency)] = ["Währung", "Waehrung", "Document Currency"],
[nameof(SalesRecord.CompanyCurrency)] = ["Währung", "Waehrung", "Company Currency"],
[nameof(SalesRecord.Incoterms2020)] = ["Versandbedingung", "Incoterms 2020"],
[nameof(SalesRecord.SalesResponsibleEmployee)] = ["AdressNummer_V", "Sales responsible employee"],
[nameof(SalesRecord.InvoiceDate)] = ["Belegdatum-Rechnung", "invoice date"],
[nameof(SalesRecord.OrderDate)] = ["BelegDatum Auftrag", "order date"]
};
var result = new List<(string TargetField, string SourceHeader)>();
foreach (var (targetField, sourceAliases) in aliases)
{
foreach (var alias in sourceAliases)
{
if (headerByNormalized.TryGetValue(NormalizeHeader(alias), out var actualHeader))
{
result.Add((targetField, actualHeader));
break;
}
}
}
result.Add((nameof(SalesRecord.DocumentType), "=Manual Excel"));
return result;
}
private IEnumerable GetAvailableManualExcelHeaders(string? currentValue)
{
var values = new List(_manualExcelHeaders);
values.Add("=Manual Excel");
if (!string.IsNullOrWhiteSpace(currentValue) && !values.Contains(currentValue, StringComparer.OrdinalIgnoreCase))
values.Insert(0, currentValue);
return values
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(x => x.StartsWith('=') ? 1 : 0)
.ThenBy(x => x, StringComparer.OrdinalIgnoreCase)
.ToList();
}
private List BuildHeadersFromManualExcelMappings()
=> _manualExcelMappings
.Select(m => m.SourceHeader)
.Where(x => !string.IsNullOrWhiteSpace(x) && !x.Trim().StartsWith('='))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
.ToList();
private static bool IsImportantManualExcelField(string targetField)
=> targetField is nameof(SalesRecord.InvoiceNumber) or
nameof(SalesRecord.SalesPriceValue) or
nameof(SalesRecord.InvoiceDate);
private static string NormalizeHeader(string value)
{
var chars = value
.Where(char.IsLetterOrDigit)
.Select(char.ToLowerInvariant)
.ToArray();
return new string(chars);
}
private static List ParseSapEntitySets(string json)
{
if (string.IsNullOrWhiteSpace(json))
return [];
try
{
return JsonSerializer.Deserialize>(json) ?? [];
}
catch
{
return [];
}
}
private static string SerializeSapEntitySets(List entitySets)
=> JsonSerializer.Serialize(entitySets);
private void AddSapSource()
{
SapEditorService.AddSapSource(_sapSources, _sapEntitySetsCache);
}
private void RemoveSapSource(SapSourceDefinition source)
{
SapEditorService.RemoveSapSource(_sapSources, source);
}
private void AddSapJoin()
{
SapEditorService.AddSapJoin(_sapJoins);
}
private void AutoMatchSapJoins()
{
var result = SapEditorService.AutoMatchSapJoins(_sapSources, _sapJoins, _sapSourceFieldMap);
SapEditorService.NormalizeSapConfigCollections(_sapSources, _sapJoins, _sapMappings);
Snackbar.Add(result.Message, result.Success ? Severity.Success : result.Warning ? Severity.Warning : Severity.Info);
}
private void RemoveSapJoin(SapJoinDefinition join)
{
SapEditorService.RemoveSapJoin(_sapJoins, join);
}
private void AddSapMapping()
{
SapEditorService.AddSapMapping(_sapMappings, _salesRecordFields, _sapAvailableSourceExpressions);
}
private void RemoveSapMapping(SapFieldMapping mapping)
{
SapEditorService.RemoveSapMapping(_sapMappings, mapping);
}
private IEnumerable GetSapAliases()
=> SapEditorService.GetSapAliases(_sapSources);
private void NormalizeSapConfigCollections()
=> SapEditorService.NormalizeSapConfigCollections(_sapSources, _sapJoins, _sapMappings);
private async Task RefreshSapSourceFields()
{
if (_refreshingSapSourceFields)
return;
_refreshingSapSourceFields = true;
try
{
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 Quellen mit Alias und Entity Set/Tabelle.");
var result = await StandortePageService.RefreshSapSourceFieldsAsync(_editingSite, activeSources, _sapMappings);
_sapAvailableSourceExpressions = result.SourceExpressions;
_sapSourceFieldMap = result.SourceFieldMap;
Snackbar.Add($"{_sapAvailableSourceExpressions.Count} Source Expressions geladen.", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add(ex.Message, Severity.Error);
}
finally
{
_refreshingSapSourceFields = false;
}
}
private IEnumerable GetAvailableSourceExpressions(string? currentValue)
=> SapEditorService.GetAvailableSourceExpressions(_sapAvailableSourceExpressions, currentValue);
private List BuildSourceExpressionsFromMappings()
=> SapEditorService.BuildSourceExpressionsFromMappings(_sapMappings);
private Dictionary> BuildSourceFieldMapFromJoins()
=> SapEditorService.BuildSourceFieldMapFromJoins(_sapJoins);
private IEnumerable GetAvailableJoinFields(string? alias, string? currentKeys)
=> SapEditorService.GetAvailableJoinFields(_sapSourceFieldMap, alias, currentKeys);
private static HashSet GetSelectedJoinKeys(string? keys)
=> keys?
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(x => !string.IsNullOrWhiteSpace(x))
.ToHashSet(StringComparer.OrdinalIgnoreCase)
?? [];
}