@page "/standorte"
@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 (IsSapSite())
{
SAP Gateway
Die Service-URL zeigt auf den OData-Service. Die verfügbaren Entity Sets werden nur per Knopfdruck aktualisiert und lokal zwischengespeichert.
Zentrale SAP Service URL: @GetCentralSapServiceUrlSummary(_editingSite.SourceSystem)
@if (_refreshingSapEntitySets)
{
@("Lade...")
}
else
{
@("Quellen refreshen")
}
@if (_editingSite.SapEntitySetsRefreshedAtUtc.HasValue)
{
Letzter Refresh: @_editingSite.SapEntitySetsRefreshedAtUtc.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss")
}
SAP Quellen
Quelle hinzufügen
Pro Quelle Alias und Entity Set definieren. Joins verwenden links/rechts kommagetrennte Schlüsselfelder wie `VBELN,POSNR`. Feldmappings erwarten `Alias.Feldname` oder Konstanten wie `=SAP`.
Alias
Entity Set
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 hinzugefügten SAP-Quellen als `Alias.Feldname` gelesen. Vorhandene manuelle Werte bleiben auswählbar.
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.
}
}
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 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 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 = [];
_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;
_sapAvailableSourceExpressions = BuildSourceExpressionsFromMappings();
_sapSourceFieldMap = BuildSourceFieldMapFromJoins();
_siteDialogVisible = true;
}
private async Task SaveSite()
{
if (_savingSite)
return;
_savingSite = true;
try
{
await StandortePageService.SaveSiteAsync(_editingSite, UsesHanaConnection(), IsSapSite(), _sapSources, _sapJoins, _sapMappings, _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 IsManualExcelSite()
=> string.Equals(GetSourceSystemConnectionKind(_editingSite.SourceSystem), SourceSystemConnectionKinds.ManualExcel, StringComparison.OrdinalIgnoreCase);
private bool UsesHanaConnection() => IsHanaSourceSystem(_editingSite.SourceSystem);
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)
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 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 SAP-Quellen mit Alias und Entity Set.");
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)
?? [];
}