diff --git a/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor b/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor
index b4df433..411b1d8 100644
--- a/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor
+++ b/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor
@@ -33,6 +33,37 @@
+
+ Zentrale Roh-Auswertung
+
+ Diese Sicht arbeitet direkt auf `CentralSalesRecords` und zeigt nur fachlich neutrale Rohkennzahlen. Kein Intercompany-Filter, keine CHF-Umrechnung, kein Budget, keine Spartenlogik.
+
+
+
+
+ @foreach (var year in _centralYears)
+ {
+ @year
+ }
+
+
+
+
+ @foreach (var month in Enumerable.Range(1, 12))
+ {
+ @($"{month:D2}")
+ }
+
+
+
+
+ @(_analyzingCentral ? "Analysiere..." : "Zentrale Auswertung laden")
+
+
+
+
+
@if (_result is not null)
{
@@ -91,16 +122,147 @@
}
+@if (_centralResult is not null)
+{
+
+ Zeilen@_centralResult.Summary.RowCount.ToString("N0")
+ Rechnungen@_centralResult.Summary.InvoiceCount.ToString("N0")
+ Standorte@_centralResult.Summary.SiteCount.ToString("N0")
+ Länder@_centralResult.Summary.CountryCount.ToString("N0")
+ Währungen@_centralResult.Summary.CurrencyCount.ToString("N0")
+ Periode@BuildPeriodLabel(_centralResult)
+
+
+
+ Hinweise
+ @foreach (var notice in _centralResult.Notices)
+ {
+ @notice
+ }
+
+
+
+
+
+ Jahresumsatz 2025/2026
+
+
+ Jahr
+ Währung
+ Umsatz
+ Zeilen
+
+
+ @context.Year
+ @context.Currency
+ @context.SalesValue.ToString("N2")
+ @context.RowCount.ToString("N0")
+
+
+
+
+
+
+ Monatsumsatz
+
+
+ Monat
+ Währung
+ Umsatz
+ Zeilen
+
+
+ @context.Label
+ @context.Currency
+ @context.SalesValue.ToString("N2")
+ @context.RowCount.ToString("N0")
+
+
+
+
+
+
+
+
+
+ Tagesumsatz im ausgewählten Monat
+
+
+ Tag
+ Währung
+ Umsatz
+ Zeilen
+
+
+ @context.Label
+ @context.Currency
+ @context.SalesValue.ToString("N2")
+ @context.RowCount.ToString("N0")
+
+
+ Für die Tagessicht bitte zusätzlich einen Monat wählen.
+
+
+
+
+
+
+ Umsatz nach Quelle
+
+
+ Quelle
+ Währung
+ Umsatz
+ Rechnungen
+
+
+ @context.Label
+ @context.Currency
+ @context.SalesValue.ToString("N2")
+ @context.InvoiceCount.ToString("N0")
+
+
+
+
+
+
+
+ Umsatz nach Land
+
+
+ Land
+ Währung
+ Umsatz
+ Rechnungen
+ Zeilen
+
+
+ @context.Label
+ @context.Currency
+ @context.SalesValue.ToString("N2")
+ @context.InvoiceCount.ToString("N0")
+ @context.RowCount.ToString("N0")
+
+
+
+}
+
@code {
private List _files = [];
+ private List _centralYears = [];
private string? _selectedFilePath;
private ManagementCockpitResult? _result;
+ private ManagementCockpitCentralResult? _centralResult;
+ private int _selectedCentralYear;
+ private int? _selectedCentralMonth;
private bool _loadingFiles;
private bool _analyzing;
+ private bool _analyzingCentral;
protected override async Task OnInitializedAsync()
{
await ReloadFiles();
+ await ReloadCentralYears();
}
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()
{
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
{
"Warning" => Severity.Warning,
"Error" => Severity.Error,
_ => 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}";
+ }
}
diff --git a/TrafagSalesExporter/Components/Pages/Standorte.razor b/TrafagSalesExporter/Components/Pages/Standorte.razor
index 95a91b9..99d0fdc 100644
--- a/TrafagSalesExporter/Components/Pages/Standorte.razor
+++ b/TrafagSalesExporter/Components/Pages/Standorte.razor
@@ -497,12 +497,34 @@
if (result != true) return;
- using var db = await DbFactory.CreateDbContextAsync();
- var entity = await db.HanaServers.FindAsync(server.Id);
- if (entity is not null)
+ try
{
- db.HanaServers.Remove(entity);
- await db.SaveChangesAsync();
+ 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);
+ if (entity is not null)
+ {
+ db.HanaServers.Remove(entity);
+ await db.SaveChangesAsync();
+ }
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"Server konnte nicht gelöscht werden: {ex.Message}", Severity.Error);
+ return;
}
await LoadDataAsync();
diff --git a/TrafagSalesExporter/Models/ManagementCockpitModels.cs b/TrafagSalesExporter/Models/ManagementCockpitModels.cs
index e47fad3..762d2b5 100644
--- a/TrafagSalesExporter/Models/ManagementCockpitModels.cs
+++ b/TrafagSalesExporter/Models/ManagementCockpitModels.cs
@@ -48,3 +48,52 @@ public class ManagementCockpitResult
public List TopSalesEmployees { get; set; } = [];
public Dictionary 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 Notices { get; set; } = [];
+ public List YearlyTotals { get; set; } = [];
+ public List MonthlyTotals { get; set; } = [];
+ public List DailyTotals { get; set; } = [];
+ public List SourceSystemTotals { get; set; } = [];
+ public List CountryTotals { get; set; } = [];
+}
diff --git a/TrafagSalesExporter/Services/IManagementCockpitService.cs b/TrafagSalesExporter/Services/IManagementCockpitService.cs
index 1774d83..0fad099 100644
--- a/TrafagSalesExporter/Services/IManagementCockpitService.cs
+++ b/TrafagSalesExporter/Services/IManagementCockpitService.cs
@@ -6,4 +6,6 @@ public interface IManagementCockpitService
{
Task> GetAvailableFilesAsync();
Task AnalyzeAsync(string filePath);
+ Task> GetAvailableCentralYearsAsync();
+ Task AnalyzeCentralAsync(int year, int? month);
}
diff --git a/TrafagSalesExporter/Services/ManagementCockpitService.cs b/TrafagSalesExporter/Services/ManagementCockpitService.cs
index d3cb297..9dc17cf 100644
--- a/TrafagSalesExporter/Services/ManagementCockpitService.cs
+++ b/TrafagSalesExporter/Services/ManagementCockpitService.cs
@@ -106,6 +106,152 @@ public class ManagementCockpitService : IManagementCockpitService
return Task.FromResult(result);
}
+ public async Task> 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 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 GetCandidateDirectories(ExportSettings settings)
{
yield return Path.Combine(AppContext.BaseDirectory, "output");
@@ -384,4 +530,15 @@ public class ManagementCockpitService : IManagementCockpitService
public decimal EstimatedCostTotal { 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; }
+ }
}