@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 OData 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-/CSV-Import Für diesen Standort wird keine SAP- oder HANA-Verbindung verwendet. Es wird die hier hinterlegte Excel-/CSV-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 OData 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) && !string.Equals(extension, ".csv", StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException("Bitte eine Excel- oder CSV-Datei mit Endung .xlsx oder .csv auswaehlen."); } 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) ?? []; }