Files
NumericalAnalysisTool/Components/Pages/Auswertung.razor
T
2026-05-29 13:52:32 +02:00

270 lines
10 KiB
Plaintext

@page "/auswertung"
@page "/auswertung/{*WorkspaceId}"
@using ZahlenAnalyse.Web.Models
@using ZahlenAnalyse.Web.Services
@inject WorkspaceService DbService
@inject IJSRuntime JSRuntime
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8 mb-8">
<MudText Typo="Typo.h4" Class="mb-6">Daten-Auswertung</MudText>
@if (_isLoading)
{
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
}
else
{
<MudPaper Class="pa-6 mb-6" Elevation="1">
<MudSelect T="Workspace"
Label="Workspace auswählen"
Variant="Variant.Outlined"
Value="_selectedWorkspace"
ValueChanged="OnWorkspaceSelected"
ToStringFunc="@(w => w?.Name)">
@foreach (var ws in _workspaces)
{
<MudSelectItem Value="ws">@ws.Name</MudSelectItem>
}
</MudSelect>
</MudPaper>
@if (_selectedWorkspace != null)
{
<MudGrid>
<MudItem xs="12">
<MudDataGrid Items="@_fakten"
Groupable="true"
Filterable="true"
Hideable="true"
Elevation="1">
<Columns>
<PropertyColumn Property="x => x.Date" Title="Datum" Format="dd.MM.yyyy" />
@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})";
<PropertyColumn Property="@(x => GetDimensionLevel(x, dim.Name, levelIndex))" Title="@title" />
}
}
}
<PropertyColumn Property="x => x.Amount" Title="Betrag" AggregateDefinition="_sumAggregation" Format="N2" />
</Columns>
</MudDataGrid>
</MudItem>
<MudItem xs="12">
<MudPaper Class="pa-6 mt-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-4">Geldfluss (Sankey-Diagramm)</MudText>
<MudSelect T="string"
Label="Sichtweise (Klick-Reihenfolge bestimmt den Fluss)"
MultiSelection="true"
SelectedValues="_selectedChartDimensions"
SelectedValuesChanged="OnChartDimensionsChanged"
Variant="Variant.Outlined"
Class="mb-6">
@foreach (var dim in _selectedWorkspace.Dimensions)
{
<MudSelectItem T="string" Value="@dim.Name">@dim.Name</MudSelectItem>
}
</MudSelect>
<div id="sankey-chart" style="width: 100%; height: 400px;"></div>
</MudPaper>
</MudItem>
</MudGrid>
}
}
</MudContainer>
@code {
[Parameter] public string? WorkspaceId { get; set; }
private List<Workspace> _workspaces = new();
private Workspace? _selectedWorkspace;
private List<AnalysisFakt> _fakten = new();
private bool _isLoading = true;
private List<object[]> _sankeyData = new();
private IReadOnlyCollection<string> _selectedChartDimensions = new List<string>();
private Dictionary<string, int> _dimensionMaxLevels = new();
private AggregateDefinition<AnalysisFakt> _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<string> 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<string, string> _pathLabels = new();
private HashSet<string> _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);
}
}