From 4abe66c283343c1839e6c378828cab540f22b07b Mon Sep 17 00:00:00 2001 From: Trond Schertel Date: Fri, 29 May 2026 13:52:32 +0200 Subject: [PATCH] Diagram --- Components/App.razor | 37 ++++ Components/Pages/Auswertung.razor | 270 ++++++++++++++++++++++++++++++ Components/Pages/Home.razor | 2 +- Services/WorkspaceService.cs | 15 +- 4 files changed, 321 insertions(+), 3 deletions(-) create mode 100644 Components/Pages/Auswertung.razor diff --git a/Components/App.razor b/Components/App.razor index 8d0590b..5b568b1 100644 --- a/Components/App.razor +++ b/Components/App.razor @@ -20,6 +20,43 @@ + + + diff --git a/Components/Pages/Auswertung.razor b/Components/Pages/Auswertung.razor new file mode 100644 index 0000000..e777697 --- /dev/null +++ b/Components/Pages/Auswertung.razor @@ -0,0 +1,270 @@ +@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); + } +} \ No newline at end of file diff --git a/Components/Pages/Home.razor b/Components/Pages/Home.razor index 2ed2baa..6b633c7 100644 --- a/Components/Pages/Home.razor +++ b/Components/Pages/Home.razor @@ -77,7 +77,7 @@ Daten erfassen - + Auswertung diff --git a/Services/WorkspaceService.cs b/Services/WorkspaceService.cs index f23d5c6..65b2541 100644 --- a/Services/WorkspaceService.cs +++ b/Services/WorkspaceService.cs @@ -70,8 +70,7 @@ public class WorkspaceService public async Task> GetWorkspacesForUserAsync() { - // Hier können wir jetzt auch die OwnerId direkt aus dem Token ziehen! - // Du musst sie nicht mehr von der UI aus übergeben. + var authState = await _authStateProvider.GetAuthenticationStateAsync(); var ownerId = await GetUserIdAsync(); @@ -107,4 +106,16 @@ public class WorkspaceService await session.StoreAsync(fakt); await session.SaveChangesAsync(); } + + public async Task> GetFaktenForWorkspaceAsync(string workspaceId) + { + using var session = _store.OpenAsyncSession(); + var ownerId = await GetUserIdAsync(); + + + return await session.Query() + .Where(f => f.WorkspaceId == workspaceId && f.OwnerId == ownerId) + .OrderByDescending(f => f.Date) + .ToListAsync(); + } } \ No newline at end of file