@page "/management-cockpit" @using TrafagSalesExporter.Models @using TrafagSalesExporter.Services @inject IManagementCockpitPageService CockpitPageService @inject ISnackbar Snackbar @inject IUiTextService UiText @T("Management Cockpit", "Management Cockpit") @T("Management Cockpit", "Management Cockpit") @foreach (var file in _files) { @file.DisplayName } @T("Dateien laden", "Load files") @(_analyzing ? T("Analysiere...", "Analyzing...") : T("Cockpit erzeugen", "Build cockpit")) @T("Zentrale Roh-Auswertung", "Central raw analysis") @T("Diese Sicht arbeitet direkt auf `CentralSalesRecords` und zeigt nur fachlich neutrale Rohkennzahlen. Kein Intercompany-Filter, keine CHF-Umrechnung, kein Budget, keine Spartenlogik.", "This view works directly on `CentralSalesRecords` and shows only neutral raw metrics. No intercompany filter, no CHF conversion, no budget, no divisional logic.") @foreach (var year in _centralYears) { @year } @foreach (var month in Enumerable.Range(1, 12)) { @($"{month:D2}") } @(_analyzingCentral ? T("Analysiere...", "Analyzing...") : T("Zentrale Auswertung laden", "Load central analysis")) @if (_result is not null) { @T("Land", "Country")@_result.Summary.Land TSC@_result.Summary.Tsc @T("Umsatz", "Sales")@_result.Summary.SalesValueTotal.ToString("N2") @T("Geschaetzte Marge", "Estimated margin")@($"{_result.Summary.EstimatedMarginPercent:F1}%") @T("Management Aussagen", "Management statements") @foreach (var finding in _result.Findings) { @finding.Title: @finding.Detail } @T("Top Kunden", "Top customers") @foreach (var item in _result.TopCustomers) { @($"{item.Label}: {item.Value:N2} ({item.SharePercent:F1}%)") } @T("Top Produktgruppen", "Top product groups") @foreach (var item in _result.TopProductGroups) { @($"{item.Label}: {item.Value:N2} ({item.SharePercent:F1}%)") } @T("Top Sales Owner", "Top sales owner") @foreach (var item in _result.TopSalesEmployees) { @($"{item.Label}: {item.Value:N2} ({item.SharePercent:F1}%)") } @T("Datenqualitaet", "Data quality") @foreach (var entry in _result.DataQualityCounts.OrderByDescending(x => x.Value)) { @($"{entry.Key}: {entry.Value}") } } @if (_centralResult is not null) { @T("Zeilen", "Rows")@_centralResult.Summary.RowCount.ToString("N0") @T("Rechnungen", "Invoices")@_centralResult.Summary.InvoiceCount.ToString("N0") @T("Standorte", "Sites")@_centralResult.Summary.SiteCount.ToString("N0") @T("Laender", "Countries")@_centralResult.Summary.CountryCount.ToString("N0") @T("Waehrungen", "Currencies")@_centralResult.Summary.CurrencyCount.ToString("N0") @T("Periode", "Period")@BuildPeriodLabel(_centralResult) @T("Cockpit Manometer", "Cockpit gauges") @T("Verdichtete Kennzahlen aus der zentralen Rohsicht. Die Manometer zeigen Anteile, Dichte und Abdeckung, ohne Waehrungsumrechnung oder Budgetlogik.", "Condensed metrics from the central raw view. The gauges show shares, density and coverage without currency conversion or budget logic.") @foreach (var gauge in BuildCentralGauges(_centralResult)) { @gauge.Title
@gauge.DisplayValue @gauge.Subtitle
}
@T("Hinweise", "Notes") @foreach (var notice in _centralResult.Notices) { @notice } @T("Jahresumsatz 2025/2026", "Yearly sales 2025/2026") @T("Jahr", "Year") @T("Waehrung", "Currency") @T("Umsatz", "Sales") @T("Zeilen", "Rows") @context.Year @context.Currency @context.SalesValue.ToString("N2") @context.RowCount.ToString("N0") @T("Monatsumsatz", "Monthly sales") @T("Monat", "Month") @T("Waehrung", "Currency") @T("Umsatz", "Sales") @T("Zeilen", "Rows") @context.Label @context.Currency @context.SalesValue.ToString("N2") @context.RowCount.ToString("N0") @T("Tagesumsatz im ausgewaehlten Monat", "Daily sales in selected month") @T("Tag", "Day") @T("Waehrung", "Currency") @T("Umsatz", "Sales") @T("Zeilen", "Rows") @context.Label @context.Currency @context.SalesValue.ToString("N2") @context.RowCount.ToString("N0") @T("Fuer die Tagessicht bitte zusaetzlich einen Monat waehlen.", "Please select a month as well for the daily view.") @T("Umsatz nach Quelle", "Sales by source") @T("Quelle", "Source") @T("Waehrung", "Currency") @T("Umsatz", "Sales") @T("Rechnungen", "Invoices") @context.Label @context.Currency @context.SalesValue.ToString("N2") @context.InvoiceCount.ToString("N0") @T("Umsatz nach Land", "Sales by country") @T("Land", "Country") @T("Waehrung", "Currency") @T("Umsatz", "Sales") @T("Rechnungen", "Invoices") @T("Zeilen", "Rows") @context.Label @context.Currency @context.SalesValue.ToString("N2") @context.InvoiceCount.ToString("N0") @context.RowCount.ToString("N0") } @code { private List _files = []; private List _centralYears = []; private const string GaugeArcPath = "M 30 110 A 80 80 0 0 1 190 110"; 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() { var state = await CockpitPageService.InitializeAsync(_selectedFilePath, _selectedCentralYear); _files = state.Files; _centralYears = state.CentralYears; _selectedFilePath = state.SelectedFilePath; _selectedCentralYear = state.SelectedCentralYear; } private async Task ReloadFiles() { _loadingFiles = true; try { _files = await CockpitPageService.LoadFilesAsync(); _selectedFilePath ??= _files.FirstOrDefault()?.Path; } finally { _loadingFiles = false; } } private async Task ReloadCentralYears() { _centralYears = await CockpitPageService.LoadCentralYearsAsync(); if (_selectedCentralYear == 0) _selectedCentralYear = _centralYears.LastOrDefault(); } private async Task Analyze() { if (string.IsNullOrWhiteSpace(_selectedFilePath)) return; _analyzing = true; try { _result = await CockpitPageService.AnalyzeAsync(_selectedFilePath); } catch (Exception ex) { Snackbar.Add(string.Format(T("Cockpit konnte nicht erzeugt werden: {0}", "Could not build cockpit: {0}"), ex.Message), Severity.Error); } finally { _analyzing = false; } } private async Task AnalyzeCentral() { if (_selectedCentralYear == 0) return; _analyzingCentral = true; try { _centralResult = await CockpitPageService.AnalyzeCentralAsync(_selectedCentralYear, _selectedCentralMonth); } catch (Exception ex) { Snackbar.Add(string.Format(T("Zentrale Auswertung konnte nicht erzeugt werden: {0}", "Could not build central analysis: {0}"), 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}"; } private List BuildCentralGauges(ManagementCockpitCentralResult result) { var invoiceDensity = result.Summary.RowCount == 0 ? 0m : result.Summary.InvoiceCount * 100m / result.Summary.RowCount; var sourceDominance = result.SourceSystemTotals.Count == 0 ? 0m : result.SourceSystemTotals.Max(x => x.RowCount) * 100m / Math.Max(1, result.Summary.RowCount); var countryDominance = result.CountryTotals.Count == 0 ? 0m : result.CountryTotals.Max(x => x.RowCount) * 100m / Math.Max(1, result.Summary.RowCount); var periodCoverage = BuildPeriodCoveragePercent(result); var topCountrySalesShare = BuildTopSalesSharePercent(result.CountryTotals); var topSourceSalesShare = BuildTopSalesSharePercent(result.SourceSystemTotals); var currencyComplexity = result.Summary.CurrencyCount <= 1 ? 0m : Math.Min(100m, (result.Summary.CurrencyCount - 1) * 25m); var peakVsAverageMonth = BuildPeakVsAverageMonthPercent(result); return [ new CentralGaugeModel { Title = T("Rechnungsdichte", "Invoice density"), Percent = invoiceDensity, DisplayValue = $"{invoiceDensity:F0}%", Subtitle = T("Rechnungen pro 100 Zeilen", "Invoices per 100 rows"), Color = "#1f8a70" }, new CentralGaugeModel { Title = T("Quellen-Dominanz", "Source dominance"), Percent = sourceDominance, DisplayValue = $"{sourceDominance:F0}%", Subtitle = T("Groesste Quelle nach Zeilen", "Largest source by rows"), Color = "#d9822b" }, new CentralGaugeModel { Title = T("Land-Dominanz", "Country dominance"), Percent = countryDominance, DisplayValue = $"{countryDominance:F0}%", Subtitle = T("Groesstes Land nach Zeilen", "Largest country by rows"), Color = "#c4496b" }, new CentralGaugeModel { Title = T("Perioden-Abdeckung", "Period coverage"), Percent = periodCoverage, DisplayValue = $"{periodCoverage:F0}%", Subtitle = BuildPeriodGaugeSubtitle(result), Color = "#3d7ff0" }, new CentralGaugeModel { Title = T("Top-Land Umsatz", "Top country sales"), Percent = topCountrySalesShare, DisplayValue = $"{topCountrySalesShare:F0}%", Subtitle = T("Anteil des umsatzstaerksten Landes", "Share of top-selling country"), Color = "#7f56d9" }, new CentralGaugeModel { Title = T("Top-Quelle Umsatz", "Top source sales"), Percent = topSourceSalesShare, DisplayValue = $"{topSourceSalesShare:F0}%", Subtitle = T("Anteil der staerksten Quelle", "Share of strongest source"), Color = "#0f9fb5" }, new CentralGaugeModel { Title = T("Waehrungs-Komplexitaet", "Currency complexity"), Percent = currencyComplexity, DisplayValue = result.Summary.CurrencyCount.ToString("N0"), Subtitle = T("Anzahl Waehrungen im Zeitraum", "Number of currencies in period"), Color = "#b54708" }, new CentralGaugeModel { Title = T("Monat gegen Peak", "Month vs peak"), Percent = peakVsAverageMonth, DisplayValue = $"{peakVsAverageMonth:F0}%", Subtitle = T("Durchschnittsmonat relativ zum Peak", "Average month relative to peak"), Color = "#d92d20" } ]; } private static decimal BuildPeriodCoveragePercent(ManagementCockpitCentralResult result) { if (result.Summary.PeriodStart is null || result.Summary.PeriodEnd is null) return 0m; if (result.Filter.Month.HasValue) { var daysInMonth = DateTime.DaysInMonth(result.Filter.Year, result.Filter.Month.Value); var coveredDays = result.DailyTotals .Select(x => x.Day) .Where(x => x.HasValue) .Distinct() .Count(); return daysInMonth == 0 ? 0m : coveredDays * 100m / daysInMonth; } var coveredMonths = result.MonthlyTotals .Select(x => x.Month) .Where(x => x.HasValue) .Distinct() .Count(); return coveredMonths * 100m / 12m; } private string BuildPeriodGaugeSubtitle(ManagementCockpitCentralResult result) => result.Filter.Month.HasValue ? T("Tage mit Daten im Monat", "Days with data in month") : T("Monate mit Daten im Jahr", "Months with data in year"); private static decimal BuildTopSalesSharePercent(IEnumerable rows) { var materialized = rows.ToList(); if (materialized.Count == 0) return 0m; var total = materialized.Sum(x => x.SalesValue); if (total == 0) return 0m; return materialized.Max(x => x.SalesValue) * 100m / total; } private static decimal BuildPeakVsAverageMonthPercent(ManagementCockpitCentralResult result) { var monthRows = result.MonthlyTotals.ToList(); if (monthRows.Count == 0) return 0m; var groupedMonths = monthRows .GroupBy(x => x.Label, StringComparer.OrdinalIgnoreCase) .Select(g => g.Sum(x => x.SalesValue)) .ToList(); if (groupedMonths.Count == 0) return 0m; var peak = groupedMonths.Max(); if (peak == 0) return 0m; var average = groupedMonths.Average(); return Math.Min(100m, average * 100m / peak); } private static string BuildGaugeDashArray(decimal percent) => $"{Math.Clamp(percent, 0m, 100m).ToString("F2", System.Globalization.CultureInfo.InvariantCulture)} 100"; private static string BuildGaugeNeedleX(decimal percent) => GetGaugePoint(percent, 68d).X.ToString("F2", System.Globalization.CultureInfo.InvariantCulture); private static string BuildGaugeNeedleY(decimal percent) => GetGaugePoint(percent, 68d).Y.ToString("F2", System.Globalization.CultureInfo.InvariantCulture); private static (double X, double Y) GetGaugePoint(decimal percent, double radius = 80d) { var clamped = Math.Clamp((double)percent, 0d, 100d); var angle = Math.PI * (1d - clamped / 100d); var x = 110d + radius * Math.Cos(angle); var y = 110d - radius * Math.Sin(angle); return (x, y); } private sealed class CentralGaugeModel { public string Title { get; set; } = string.Empty; public decimal Percent { get; set; } public string DisplayValue { get; set; } = string.Empty; public string Subtitle { get; set; } = string.Empty; public string Color { get; set; } = "#3d7ff0"; } } @code { private string T(string german, string english) => UiText.Text(german, english); }