From d02f4abb57b47c9e007af1c80a0bc103ffcc003f Mon Sep 17 00:00:00 2001 From: metacube Date: Wed, 15 Apr 2026 16:22:48 +0200 Subject: [PATCH] cockpit vorbereitung --- .../Components/Pages/ManagementCockpit.razor | 197 ++++++++++++++++++ .../Components/Pages/Standorte.razor | 32 ++- .../Models/ManagementCockpitModels.cs | 49 +++++ .../Services/IManagementCockpitService.cs | 2 + .../Services/ManagementCockpitService.cs | 157 ++++++++++++++ 5 files changed, 432 insertions(+), 5 deletions(-) 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; } + } }