@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 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; } 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(); // Wichtig: Beim Neuzeichnen zurücksetzen _usedLabels.Clear(); if (!_fakten.Any() || _selectedWorkspace == null || !_selectedChartDimensions.Any()) { await DrawChartAsync(); return; } var selectedDims = _selectedChartDimensions.ToList(); // Den Workspace-Namen als allerersten Knoten generieren var workspaceLabel = GetUniqueLabel(_selectedWorkspace.Name, _selectedWorkspace.Name); 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) { // Der logische, lange Pfad (z.B. "Workspace / Italien / Maut") var targetFullPath = $"{currentFullPath} / {level}"; // Wir holen uns den kurzen Namen (z.B. "Maut") inkl. nötiger unsichtbarer Leerzeichen var targetLabel = GetUniqueLabel(targetFullPath, level); AddSankeyLink(currentSourceLabel, targetLabel, fakt.Amount); currentFullPath = targetFullPath; currentSourceLabel = targetLabel; } } } await DrawChartAsync(); } // --- 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() { // Ein winziger Delay stellt sicher, dass Blazor das UI fertig gerendert hat await Task.Delay(50); await JSRuntime.InvokeVoidAsync("drawSankeyChart", "sankey-chart", _sankeyData); } }