cockpit vorbereitung

This commit is contained in:
2026-04-15 16:22:48 +02:00
parent 264e64bbf5
commit d02f4abb57
5 changed files with 432 additions and 5 deletions
@@ -33,6 +33,37 @@
</MudGrid> </MudGrid>
</MudPaper> </MudPaper>
<MudPaper Class="pa-4 mb-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-3">Zentrale Roh-Auswertung</MudText>
<MudAlert Severity="Severity.Info" Dense Variant="Variant.Outlined" Class="mb-3">
Diese Sicht arbeitet direkt auf `CentralSalesRecords` und zeigt nur fachlich neutrale Rohkennzahlen. Kein Intercompany-Filter, keine CHF-Umrechnung, kein Budget, keine Spartenlogik.
</MudAlert>
<MudGrid>
<MudItem xs="12" md="4">
<MudSelect T="int" @bind-Value="_selectedCentralYear" Label="Jahr" Dense>
@foreach (var year in _centralYears)
{
<MudSelectItem Value="@year">@year</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12" md="4">
<MudSelect T="int?" @bind-Value="_selectedCentralMonth" Label="Monat (optional)" Dense Clearable>
@foreach (var month in Enumerable.Range(1, 12))
{
<MudSelectItem Value="@((int?)month)">@($"{month:D2}")</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12" md="4">
<MudButton Variant="Variant.Filled" Color="Color.Secondary" OnClick="AnalyzeCentral"
StartIcon="@Icons.Material.Filled.QueryStats" Disabled="_analyzingCentral || _selectedCentralYear == 0">
@(_analyzingCentral ? "Analysiere..." : "Zentrale Auswertung laden")
</MudButton>
</MudItem>
</MudGrid>
</MudPaper>
@if (_result is not null) @if (_result is not null)
{ {
<MudGrid Class="mb-4"> <MudGrid Class="mb-4">
@@ -91,16 +122,147 @@
</MudPaper> </MudPaper>
} }
@if (_centralResult is not null)
{
<MudGrid Class="mb-4">
<MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">Zeilen</MudText><MudText Typo="Typo.h6">@_centralResult.Summary.RowCount.ToString("N0")</MudText></MudPaper></MudItem>
<MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">Rechnungen</MudText><MudText Typo="Typo.h6">@_centralResult.Summary.InvoiceCount.ToString("N0")</MudText></MudPaper></MudItem>
<MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">Standorte</MudText><MudText Typo="Typo.h6">@_centralResult.Summary.SiteCount.ToString("N0")</MudText></MudPaper></MudItem>
<MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">Länder</MudText><MudText Typo="Typo.h6">@_centralResult.Summary.CountryCount.ToString("N0")</MudText></MudPaper></MudItem>
<MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">Währungen</MudText><MudText Typo="Typo.h6">@_centralResult.Summary.CurrencyCount.ToString("N0")</MudText></MudPaper></MudItem>
<MudItem xs="12" md="2"><MudPaper Class="pa-4"><MudText Typo="Typo.caption">Periode</MudText><MudText Typo="Typo.h6">@BuildPeriodLabel(_centralResult)</MudText></MudPaper></MudItem>
</MudGrid>
<MudPaper Class="pa-4 mb-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-2">Hinweise</MudText>
@foreach (var notice in _centralResult.Notices)
{
<MudAlert Severity="Severity.Info" Dense Variant="Variant.Outlined" Class="mb-2">@notice</MudAlert>
}
</MudPaper>
<MudGrid Class="mb-4">
<MudItem xs="12" md="6">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-2">Jahresumsatz 2025/2026</MudText>
<MudTable Items="_centralResult.YearlyTotals" Dense Hover Striped>
<HeaderContent>
<MudTh>Jahr</MudTh>
<MudTh>Währung</MudTh>
<MudTh>Umsatz</MudTh>
<MudTh>Zeilen</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Year</MudTd>
<MudTd>@context.Currency</MudTd>
<MudTd>@context.SalesValue.ToString("N2")</MudTd>
<MudTd>@context.RowCount.ToString("N0")</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-2">Monatsumsatz</MudText>
<MudTable Items="_centralResult.MonthlyTotals" Dense Hover Striped>
<HeaderContent>
<MudTh>Monat</MudTh>
<MudTh>Währung</MudTh>
<MudTh>Umsatz</MudTh>
<MudTh>Zeilen</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Label</MudTd>
<MudTd>@context.Currency</MudTd>
<MudTd>@context.SalesValue.ToString("N2")</MudTd>
<MudTd>@context.RowCount.ToString("N0")</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
</MudItem>
</MudGrid>
<MudGrid Class="mb-4">
<MudItem xs="12" md="6">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-2">Tagesumsatz im ausgewählten Monat</MudText>
<MudTable Items="_centralResult.DailyTotals" Dense Hover Striped>
<HeaderContent>
<MudTh>Tag</MudTh>
<MudTh>Währung</MudTh>
<MudTh>Umsatz</MudTh>
<MudTh>Zeilen</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Label</MudTd>
<MudTd>@context.Currency</MudTd>
<MudTd>@context.SalesValue.ToString("N2")</MudTd>
<MudTd>@context.RowCount.ToString("N0")</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText Typo="Typo.caption">Für die Tagessicht bitte zusätzlich einen Monat wählen.</MudText>
</NoRecordsContent>
</MudTable>
</MudPaper>
</MudItem>
<MudItem xs="12" md="6">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-2">Umsatz nach Quelle</MudText>
<MudTable Items="_centralResult.SourceSystemTotals" Dense Hover Striped>
<HeaderContent>
<MudTh>Quelle</MudTh>
<MudTh>Währung</MudTh>
<MudTh>Umsatz</MudTh>
<MudTh>Rechnungen</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Label</MudTd>
<MudTd>@context.Currency</MudTd>
<MudTd>@context.SalesValue.ToString("N2")</MudTd>
<MudTd>@context.InvoiceCount.ToString("N0")</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
</MudItem>
</MudGrid>
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" Class="mb-2">Umsatz nach Land</MudText>
<MudTable Items="_centralResult.CountryTotals" Dense Hover Striped>
<HeaderContent>
<MudTh>Land</MudTh>
<MudTh>Währung</MudTh>
<MudTh>Umsatz</MudTh>
<MudTh>Rechnungen</MudTh>
<MudTh>Zeilen</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Label</MudTd>
<MudTd>@context.Currency</MudTd>
<MudTd>@context.SalesValue.ToString("N2")</MudTd>
<MudTd>@context.InvoiceCount.ToString("N0")</MudTd>
<MudTd>@context.RowCount.ToString("N0")</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
}
@code { @code {
private List<ManagementCockpitFileOption> _files = []; private List<ManagementCockpitFileOption> _files = [];
private List<int> _centralYears = [];
private string? _selectedFilePath; private string? _selectedFilePath;
private ManagementCockpitResult? _result; private ManagementCockpitResult? _result;
private ManagementCockpitCentralResult? _centralResult;
private int _selectedCentralYear;
private int? _selectedCentralMonth;
private bool _loadingFiles; private bool _loadingFiles;
private bool _analyzing; private bool _analyzing;
private bool _analyzingCentral;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await ReloadFiles(); await ReloadFiles();
await ReloadCentralYears();
} }
private async Task ReloadFiles() private async Task ReloadFiles()
@@ -117,6 +279,13 @@
} }
} }
private async Task ReloadCentralYears()
{
_centralYears = await CockpitService.GetAvailableCentralYearsAsync();
if (_selectedCentralYear == 0)
_selectedCentralYear = _centralYears.LastOrDefault();
}
private async Task Analyze() private async Task Analyze()
{ {
if (string.IsNullOrWhiteSpace(_selectedFilePath)) if (string.IsNullOrWhiteSpace(_selectedFilePath))
@@ -137,10 +306,38 @@
} }
} }
private async Task AnalyzeCentral()
{
if (_selectedCentralYear == 0)
return;
_analyzingCentral = true;
try
{
_centralResult = await CockpitService.AnalyzeCentralAsync(_selectedCentralYear, _selectedCentralMonth);
}
catch (Exception ex)
{
Snackbar.Add($"Zentrale Auswertung konnte nicht erzeugt werden: {ex.Message}", Severity.Error);
}
finally
{
_analyzingCentral = false;
}
}
private static Severity MapSeverity(string severity) => severity switch private static Severity MapSeverity(string severity) => severity switch
{ {
"Warning" => Severity.Warning, "Warning" => Severity.Warning,
"Error" => Severity.Error, "Error" => Severity.Error,
_ => Severity.Info _ => Severity.Info
}; };
private static string BuildPeriodLabel(ManagementCockpitCentralResult result)
{
if (result.Summary.PeriodStart is null || result.Summary.PeriodEnd is null)
return "-";
return $"{result.Summary.PeriodStart.Value:dd.MM.yyyy} - {result.Summary.PeriodEnd.Value:dd.MM.yyyy}";
}
} }
@@ -497,13 +497,35 @@
if (result != true) return; if (result != true) return;
try
{
using var db = await DbFactory.CreateDbContextAsync(); using var db = await DbFactory.CreateDbContextAsync();
var linkedSites = await db.Sites
.Where(s => s.HanaServerId == server.Id)
.OrderBy(s => s.Land)
.Select(s => $"{s.Land} ({s.TSC})")
.ToListAsync();
if (linkedSites.Count > 0)
{
Snackbar.Add(
$"Server kann nicht gelöscht werden. Noch verknüpfte Standorte: {string.Join(", ", linkedSites)}",
Severity.Warning);
return;
}
var entity = await db.HanaServers.FindAsync(server.Id); var entity = await db.HanaServers.FindAsync(server.Id);
if (entity is not null) if (entity is not null)
{ {
db.HanaServers.Remove(entity); db.HanaServers.Remove(entity);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
} }
}
catch (Exception ex)
{
Snackbar.Add($"Server konnte nicht gelöscht werden: {ex.Message}", Severity.Error);
return;
}
await LoadDataAsync(); await LoadDataAsync();
Snackbar.Add("Server gelöscht", Severity.Info); Snackbar.Add("Server gelöscht", Severity.Info);
@@ -48,3 +48,52 @@ public class ManagementCockpitResult
public List<ManagementCockpitTopItem> TopSalesEmployees { get; set; } = []; public List<ManagementCockpitTopItem> TopSalesEmployees { get; set; } = [];
public Dictionary<string, int> DataQualityCounts { get; set; } = new(StringComparer.OrdinalIgnoreCase); public Dictionary<string, int> DataQualityCounts { get; set; } = new(StringComparer.OrdinalIgnoreCase);
} }
public class ManagementCockpitCentralFilter
{
public int Year { get; set; }
public int? Month { get; set; }
}
public class ManagementCockpitCentralSummary
{
public int RowCount { get; set; }
public int InvoiceCount { get; set; }
public int SiteCount { get; set; }
public int CountryCount { get; set; }
public int CurrencyCount { get; set; }
public DateTime? PeriodStart { get; set; }
public DateTime? PeriodEnd { get; set; }
}
public class ManagementCockpitTimeValueRow
{
public string Label { get; set; } = string.Empty;
public int? Year { get; set; }
public int? Month { get; set; }
public int? Day { get; set; }
public string Currency { get; set; } = string.Empty;
public decimal SalesValue { get; set; }
public int RowCount { get; set; }
}
public class ManagementCockpitDimensionValueRow
{
public string Label { get; set; } = string.Empty;
public string Currency { get; set; } = string.Empty;
public decimal SalesValue { get; set; }
public int RowCount { get; set; }
public int InvoiceCount { get; set; }
}
public class ManagementCockpitCentralResult
{
public ManagementCockpitCentralFilter Filter { get; set; } = new();
public ManagementCockpitCentralSummary Summary { get; set; } = new();
public List<string> Notices { get; set; } = [];
public List<ManagementCockpitTimeValueRow> YearlyTotals { get; set; } = [];
public List<ManagementCockpitTimeValueRow> MonthlyTotals { get; set; } = [];
public List<ManagementCockpitTimeValueRow> DailyTotals { get; set; } = [];
public List<ManagementCockpitDimensionValueRow> SourceSystemTotals { get; set; } = [];
public List<ManagementCockpitDimensionValueRow> CountryTotals { get; set; } = [];
}
@@ -6,4 +6,6 @@ public interface IManagementCockpitService
{ {
Task<List<ManagementCockpitFileOption>> GetAvailableFilesAsync(); Task<List<ManagementCockpitFileOption>> GetAvailableFilesAsync();
Task<ManagementCockpitResult> AnalyzeAsync(string filePath); Task<ManagementCockpitResult> AnalyzeAsync(string filePath);
Task<List<int>> GetAvailableCentralYearsAsync();
Task<ManagementCockpitCentralResult> AnalyzeCentralAsync(int year, int? month);
} }
@@ -106,6 +106,152 @@ public class ManagementCockpitService : IManagementCockpitService
return Task.FromResult(result); return Task.FromResult(result);
} }
public async Task<List<int>> GetAvailableCentralYearsAsync()
{
using var db = await _dbFactory.CreateDbContextAsync();
var years = await db.CentralSalesRecords
.Select(r => r.InvoiceDate.HasValue ? r.InvoiceDate.Value.Year : r.ExtractionDate.Year)
.Distinct()
.OrderBy(x => x)
.ToListAsync();
return years;
}
public async Task<ManagementCockpitCentralResult> AnalyzeCentralAsync(int year, int? month)
{
using var db = await _dbFactory.CreateDbContextAsync();
var baseRows = await db.CentralSalesRecords
.Select(r => new CentralCockpitRow
{
SourceSystem = r.SourceSystem,
Land = r.Land,
Tsc = r.Tsc,
InvoiceNumber = r.InvoiceNumber,
SalesCurrency = string.IsNullOrWhiteSpace(r.SalesCurrency) ? "-" : r.SalesCurrency,
SalesValue = r.SalesPriceValue,
PeriodDate = r.InvoiceDate ?? r.ExtractionDate
})
.ToListAsync();
if (baseRows.Count == 0)
throw new InvalidOperationException("Die zentrale Tabelle enthält noch keine Datensätze.");
var selectedRows = baseRows
.Where(r => r.PeriodDate.Year == year && (!month.HasValue || r.PeriodDate.Month == month.Value))
.ToList();
if (selectedRows.Count == 0)
throw new InvalidOperationException("Für den gewählten Zeitraum gibt es keine Datensätze in der zentralen Tabelle.");
var yearlyRows = baseRows
.Where(r => r.PeriodDate.Year == 2025 || r.PeriodDate.Year == 2026)
.ToList();
var dailyBaseRows = selectedRows
.Where(r => month.HasValue)
.ToList();
return new ManagementCockpitCentralResult
{
Filter = new ManagementCockpitCentralFilter
{
Year = year,
Month = month
},
Summary = new ManagementCockpitCentralSummary
{
RowCount = selectedRows.Count,
InvoiceCount = selectedRows.Select(x => x.InvoiceNumber).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct(StringComparer.OrdinalIgnoreCase).Count(),
SiteCount = selectedRows.Select(x => x.Tsc).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct(StringComparer.OrdinalIgnoreCase).Count(),
CountryCount = selectedRows.Select(x => x.Land).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct(StringComparer.OrdinalIgnoreCase).Count(),
CurrencyCount = selectedRows.Select(x => x.SalesCurrency).Distinct(StringComparer.OrdinalIgnoreCase).Count(),
PeriodStart = selectedRows.Min(x => x.PeriodDate),
PeriodEnd = selectedRows.Max(x => x.PeriodDate)
},
Notices =
[
"Roh-Auswertung aus CentralSalesRecords.",
"Keine Intercompany-Bereinigung angewendet.",
"Keine CHF-Umrechnung angewendet. Umsatz bleibt in Sales Currency.",
"Kein Budget- und kein Spartemapping angewendet.",
"Periodenlogik basiert auf Invoice Date, falls vorhanden, sonst auf Extraction Date."
],
YearlyTotals = yearlyRows
.GroupBy(x => new { x.PeriodDate.Year, x.SalesCurrency })
.OrderBy(g => g.Key.Year)
.ThenBy(g => g.Key.SalesCurrency, StringComparer.OrdinalIgnoreCase)
.Select(g => new ManagementCockpitTimeValueRow
{
Label = g.Key.Year.ToString(),
Year = g.Key.Year,
Currency = g.Key.SalesCurrency,
SalesValue = g.Sum(x => x.SalesValue),
RowCount = g.Count()
})
.ToList(),
MonthlyTotals = selectedRows
.GroupBy(x => new { x.PeriodDate.Year, x.PeriodDate.Month, x.SalesCurrency })
.OrderBy(g => g.Key.Year)
.ThenBy(g => g.Key.Month)
.ThenBy(g => g.Key.SalesCurrency, StringComparer.OrdinalIgnoreCase)
.Select(g => new ManagementCockpitTimeValueRow
{
Label = $"{g.Key.Year:D4}-{g.Key.Month:D2}",
Year = g.Key.Year,
Month = g.Key.Month,
Currency = g.Key.SalesCurrency,
SalesValue = g.Sum(x => x.SalesValue),
RowCount = g.Count()
})
.ToList(),
DailyTotals = dailyBaseRows
.GroupBy(x => new { x.PeriodDate.Year, x.PeriodDate.Month, x.PeriodDate.Day, x.SalesCurrency })
.OrderBy(g => g.Key.Year)
.ThenBy(g => g.Key.Month)
.ThenBy(g => g.Key.Day)
.ThenBy(g => g.Key.SalesCurrency, StringComparer.OrdinalIgnoreCase)
.Select(g => new ManagementCockpitTimeValueRow
{
Label = $"{g.Key.Year:D4}-{g.Key.Month:D2}-{g.Key.Day:D2}",
Year = g.Key.Year,
Month = g.Key.Month,
Day = g.Key.Day,
Currency = g.Key.SalesCurrency,
SalesValue = g.Sum(x => x.SalesValue),
RowCount = g.Count()
})
.ToList(),
SourceSystemTotals = selectedRows
.GroupBy(x => new { x.SourceSystem, x.SalesCurrency })
.OrderBy(g => g.Key.SourceSystem, StringComparer.OrdinalIgnoreCase)
.ThenBy(g => g.Key.SalesCurrency, StringComparer.OrdinalIgnoreCase)
.Select(g => new ManagementCockpitDimensionValueRow
{
Label = g.Key.SourceSystem,
Currency = g.Key.SalesCurrency,
SalesValue = g.Sum(x => x.SalesValue),
RowCount = g.Count(),
InvoiceCount = g.Select(x => x.InvoiceNumber).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct(StringComparer.OrdinalIgnoreCase).Count()
})
.ToList(),
CountryTotals = selectedRows
.GroupBy(x => new { x.Land, x.SalesCurrency })
.OrderByDescending(g => g.Sum(x => x.SalesValue))
.ThenBy(g => g.Key.Land, StringComparer.OrdinalIgnoreCase)
.ThenBy(g => g.Key.SalesCurrency, StringComparer.OrdinalIgnoreCase)
.Select(g => new ManagementCockpitDimensionValueRow
{
Label = g.Key.Land,
Currency = g.Key.SalesCurrency,
SalesValue = g.Sum(x => x.SalesValue),
RowCount = g.Count(),
InvoiceCount = g.Select(x => x.InvoiceNumber).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct(StringComparer.OrdinalIgnoreCase).Count()
})
.ToList()
};
}
private static IEnumerable<string> GetCandidateDirectories(ExportSettings settings) private static IEnumerable<string> GetCandidateDirectories(ExportSettings settings)
{ {
yield return Path.Combine(AppContext.BaseDirectory, "output"); yield return Path.Combine(AppContext.BaseDirectory, "output");
@@ -384,4 +530,15 @@ public class ManagementCockpitService : IManagementCockpitService
public decimal EstimatedCostTotal { get; set; } public decimal EstimatedCostTotal { get; set; }
public decimal EstimatedMarginTotal { get; set; } public decimal EstimatedMarginTotal { get; set; }
} }
private class CentralCockpitRow
{
public string SourceSystem { get; set; } = string.Empty;
public string Land { get; set; } = string.Empty;
public string Tsc { get; set; } = string.Empty;
public string InvoiceNumber { get; set; } = string.Empty;
public string SalesCurrency { get; set; } = string.Empty;
public decimal SalesValue { get; set; }
public DateTime PeriodDate { get; set; }
}
} }