@page "/auswertung" @page "/auswertung/{*WorkspaceId}" @using ZahlenAnalyse.Web.Models @using ZahlenAnalyse.Web.Services @inject WorkspaceService DbService @inject IJSRuntime JSRuntime Daten-Auswertung @if (_isLoading) { } else { @foreach (var ws in _workspaces) { @ws.Name } @if (_selectedWorkspace != null) { @foreach (var dim in _selectedWorkspace.Dimensions) { if (_dimensionMaxLevels.TryGetValue(dim.Name, out int maxDepth)) { for (int i = 0; i < maxDepth; i++) { // WICHTIG für C# Closures in Schleifen: Den Index in einer lokalen Variable fangen! int levelIndex = i; string title = maxDepth == 1 ? dim.Name : $"{dim.Name} (Ebene {levelIndex + 1})"; } } }
Geldfluss (Sankey-Diagramm)
@foreach (var dim in _selectedWorkspace.Dimensions) { @dim.Name }
} }
@code { [Parameter] public string? WorkspaceId { get; set; } private List _workspaces = new(); private Workspace? _selectedWorkspace; private List _fakten = new(); private bool _isLoading = true; private bool _isInteractive = false; private bool _shouldRenderChart = false; private List _sankeyData = new(); private IReadOnlyCollection _selectedChartDimensions = new List(); private Dictionary _dimensionMaxLevels = new(); private AggregateDefinition _sumAggregation = new() { Type = AggregateType.Sum, DisplayFormat = "Summe: {value} €" }; protected override async Task OnInitializedAsync() { _workspaces = await DbService.GetWorkspacesForUserAsync(); if (!string.IsNullOrWhiteSpace(WorkspaceId)) { var preselected = _workspaces.FirstOrDefault(w => w.Id == WorkspaceId); if (preselected != null) { await OnWorkspaceSelected(preselected); } } _isLoading = false; } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { // Ab jetzt ist der Browser da und JavaScript ist erlaubt! _isInteractive = true; } if (_isInteractive && _shouldRenderChart && !_isLoading && _selectedWorkspace != null) { _shouldRenderChart = false; // Flag direkt wieder wegnehmen await DrawChartAsync(); } } private async Task OnWorkspaceSelected(Workspace ws) { _selectedWorkspace = ws; _fakten = await DbService.GetFaktenForWorkspaceAsync(ws.Id!); // Berechne die maximale Hierarchie-Tiefe für jede Dimension _dimensionMaxLevels.Clear(); foreach (var dim in ws.Dimensions) { int maxDepth = 1; foreach (var f in _fakten) { var val = GetDimensionValue(f, dim.Name); if (!string.IsNullOrWhiteSpace(val)) { var parts = val.Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length > maxDepth) maxDepth = parts.Length; } } _dimensionMaxLevels[dim.Name] = maxDepth; } _selectedChartDimensions = ws.Dimensions.Take(2).Select(d => d.Name).ToList(); await UpdateChartDataAsync(); } private string GetDimensionLevel(AnalysisFakt fakt, string dimName, int level) { var val = GetDimensionValue(fakt, dimName); if (string.IsNullOrWhiteSpace(val)) return "-"; var parts = val.Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries); // Wenn dieser spezielle Datensatz nicht so tief verschachtelt ist, gib einen Strich zurück return parts.Length > level ? parts[level].Trim() : "-"; } private async Task OnChartDimensionsChanged(IReadOnlyCollection values) { _selectedChartDimensions = values; await UpdateChartDataAsync(); } // Hilfsmethode, um das Dictionary im DataGrid sauber darzustellen private string GetDimensionValue(AnalysisFakt fakt, string dimensionName) { return fakt.Dimensions.TryGetValue(dimensionName, out var value) ? value : "-"; } private Dictionary _pathLabels = new(); private HashSet _usedLabels = new(); private async Task UpdateChartDataAsync() { _sankeyData.Clear(); _pathLabels.Clear(); _usedLabels.Clear(); if (!_fakten.Any() || _selectedWorkspace == null || !_selectedChartDimensions.Any()) { _shouldRenderChart = true; return; } var selectedDims = _selectedChartDimensions.ToList(); // --- 1. DURCHGANG: Gesamtsummen pro eindeutigem Pfad berechnen --- var pathAmounts = new Dictionary(); decimal totalWorkspaceAmount = 0; foreach (var fakt in _fakten) { totalWorkspaceAmount += fakt.Amount; var currentFullPath = _selectedWorkspace.Name; foreach (var dimName in selectedDims) { var val = GetDimensionValue(fakt, dimName); var levels = string.IsNullOrWhiteSpace(val) ? new[] { $"Ohne {dimName}" } : val.Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray(); foreach (var level in levels) { var targetFullPath = $"{currentFullPath} / {level}"; if (!pathAmounts.ContainsKey(targetFullPath)) pathAmounts[targetFullPath] = 0; pathAmounts[targetFullPath] += fakt.Amount; currentFullPath = targetFullPath; } } } // Hauptknoten (Workspace) mit Gesamtsumme versehen var workspaceLabel = GetUniqueLabel(_selectedWorkspace.Name, $"{_selectedWorkspace.Name} ({totalWorkspaceAmount.ToString("N2")} €)"); // --- 2. DURCHGANG: Diagramm-Daten mit den summierten Labels bauen --- foreach (var fakt in _fakten) { var currentFullPath = _selectedWorkspace.Name; var currentSourceLabel = workspaceLabel; foreach (var dimName in selectedDims) { var val = GetDimensionValue(fakt, dimName); var levels = string.IsNullOrWhiteSpace(val) ? new[] { $"Ohne {dimName}" } : val.Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray(); foreach (var level in levels) { var targetFullPath = $"{currentFullPath} / {level}"; // Wir holen den berechneten Gesamtbetrag für diesen spezifischen Knoten var nodeAmount = pathAmounts[targetFullPath]; var shortNameWithAmount = $"{level} ({nodeAmount.ToString("N2")} €)"; // Unser Zero-Width-Space Hack sorgt weiterhin für die Trennung im Hintergrund var targetLabel = GetUniqueLabel(targetFullPath, shortNameWithAmount); AddSankeyLink(currentSourceLabel, targetLabel, fakt.Amount); currentFullPath = targetFullPath; currentSourceLabel = targetLabel; } } } _shouldRenderChart = true; } // --- NEU: Die Magie, die unsichtbare Leerzeichen für eindeutige IDs anhängt --- private string GetUniqueLabel(string fullPath, string shortName) { // Haben wir für exakt diesen Pfad (z.B. Italien/Maut) schon ein Label? if (_pathLabels.TryGetValue(fullPath, out var existingLabel)) { return existingLabel; } string candidate = shortName; // Solange IRGENDEIN anderer Pfad dieses Label optisch schon nutzt, // hängen wir ein unsichtbares Leerzeichen (\u200B) an! while (_usedLabels.Contains(candidate)) { candidate += "\u200B"; } // Speichern und zurückgeben _pathLabels[fullPath] = candidate; _usedLabels.Add(candidate); return candidate; } // AddSankeyLink ist wieder ganz simpel und nimmt nur Strings private void AddSankeyLink(string source, string target, decimal amount) { var existing = _sankeyData.FirstOrDefault(d => (string)d[0] == source && (string)d[1] == target); if (existing != null) { existing[2] = (decimal)existing[2] + amount; } else { _sankeyData.Add(new object[] { source, target, amount }); } } private async Task DrawChartAsync() { if (!_isInteractive) return; // Ein winziger Delay stellt sicher, dass Blazor das UI fertig gerendert hat await JSRuntime.InvokeVoidAsync("drawSankeyChart", "sankey-chart", _sankeyData); } private async Task ToggleFullscreen() { // Wir übergeben die ID unseres neuen Containers await JSRuntime.InvokeVoidAsync("toggleFullscreen", "sankey-fullscreen-container"); } }