diff --git a/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor b/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor index 2555cb3..a1590a0 100644 --- a/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor +++ b/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor @@ -96,7 +96,7 @@ @context.Year - @context.CountryKey + @FormatCountryWithFlag(context.CountryKey) @context.Currency @FormatValue(context.NetSalesActual, context.Currency) @context.IncludedRows.ToString("N0") @@ -158,7 +158,7 @@ @context.Status - @context.CountryKey + @FormatCountryWithFlag(context.CountryKey) @context.Tscs @context.SourceSystems @context.Currency @@ -220,7 +220,7 @@ @context.Status - @context.CountryKey + @FormatCountryWithFlag(context.CountryKey) @context.Currency @FormatValue(context.NetSalesActual, context.Currency) @FormatNullableValue(context.ReferenceValue, context.Currency) @@ -251,7 +251,7 @@ @T("Grund", "Reason") - @context.CountryKey + @FormatCountryWithFlag(context.CountryKey) @context.Tsc @context.InvoiceNumber @context.DocumentType @@ -319,12 +319,39 @@ - @T("Umsatz nach Produktsparte", "Sales by product division") - + + + @T("Umsatz nach Produktsparte", "Sales by product division") + + + + @foreach (var option in _productFinanceGroupingOptions) + { + @T(option.GermanLabel, option.EnglishLabel) + } + + + + + @T("Top 10 anzeigen", "Show top 10") + + + + @T("Produktsparte", "Product division") - @T("Produktfamilie", "Product family") - PAPH1 + @if (ShowProductFamilyColumn) + { + @T("Produktfamilie", "Product family") + } + @if (ShowProductHierarchyColumn) + { + PAPH1 + } @T("Umsatz", "Sales") @T("Anteil", "Share") @T("Materialien", "Materials") @@ -333,13 +360,19 @@ @BuildCodeText(context.ProductDivisionCode, context.ProductDivisionText) - @BuildCodeText(context.ProductFamilyCode, context.ProductFamilyText) - @BuildCodeText(context.ProductHierarchyCode, context.ProductHierarchyText) + @if (ShowProductFamilyColumn) + { + @BuildCodeText(context.ProductFamilyCode, context.ProductFamilyText) + } + @if (ShowProductHierarchyColumn) + { + @BuildCodeText(context.ProductHierarchyCode, context.ProductHierarchyText) + } @FormatValue(context.NetSalesActual, context.Currency) @FormatPercent(context.SharePercent) @context.MaterialCount.ToString("N0") @context.RowCount.ToString("N0") - @context.Countries + @FormatCountriesWithFlags(context.Countries) @T("Keine zugeordneten Spartenumsaetze fuer diese Filter.", "No assigned division sales for these filters.") @@ -361,7 +394,7 @@ @T("Abdeckung", "Coverage") - @context.CountryKey + @FormatCountryWithFlag(context.CountryKey) @context.Tsc @FormatValue(context.TotalValue, context.Currency) @FormatValue(context.AssignedValue, context.Currency) @@ -435,7 +468,7 @@ @T("Trefferquote", "Match rate") - @context.CountryKey + @FormatCountryWithFlag(context.CountryKey) @context.Tsc @context.DistinctMaterialCount.ToString("N0") @context.MatchedMaterialCount.ToString("N0") @@ -468,7 +501,7 @@ @context.Status - @context.CountryKey + @FormatCountryWithFlag(context.CountryKey) @context.Tsc @context.Material @context.ArticleName @@ -844,6 +877,12 @@ new(ManagementCockpitCurrencyOptions.Usd, "USD"), new(ManagementCockpitCurrencyOptions.Native, "Original") ]; + private readonly List _productFinanceGroupingOptions = + [ + new(ProductFinanceGroupLevels.Hierarchy, "PAPH1 Detail", "PAPH1 detail"), + new(ProductFinanceGroupLevels.Family, "Produktfamilie", "Product family"), + new(ProductFinanceGroupLevels.Division, "Produktsparte", "Product division") + ]; private string? _selectedFilePath; private ManagementCockpitResult? _result; private ManagementCockpitCentralResult? _centralResult; @@ -866,6 +905,12 @@ private bool _analyzingFinance; private int _activeFinanceTabIndex; private int _activeDivisionTabIndex; + private string _productFinanceGroupLevel = ProductFinanceGroupLevels.Hierarchy; + private bool _limitProductFinanceTop10; + + private bool ShowProductFamilyColumn => _productFinanceGroupLevel != ProductFinanceGroupLevels.Division; + + private bool ShowProductHierarchyColumn => _productFinanceGroupLevel == ProductFinanceGroupLevels.Hierarchy; protected override void OnParametersSet() { @@ -998,6 +1043,84 @@ _centralTscFilter = null; } + private void ToggleProductFinanceTop10() + { + _limitProductFinanceTop10 = !_limitProductFinanceTop10; + } + + private IReadOnlyList BuildProductFinanceRows() + { + if (_financeResult is null) + return []; + + var sourceRows = _financeResult.ProductDivisionFinanceRows; + var totalsByCurrency = sourceRows + .GroupBy(row => row.Currency, StringComparer.OrdinalIgnoreCase) + .ToDictionary(group => group.Key, group => group.Sum(row => row.NetSalesActual), StringComparer.OrdinalIgnoreCase); + + var rows = sourceRows + .GroupBy(row => BuildProductFinanceGroupKey(row)) + .Select(group => + { + var value = group.Sum(row => row.NetSalesActual); + totalsByCurrency.TryGetValue(group.Key.Currency, out var total); + + return new ManagementProductDivisionFinanceRow + { + ProductDivisionCode = group.Key.ProductDivisionCode, + ProductDivisionText = group.Key.ProductDivisionText, + ProductFamilyCode = group.Key.ProductFamilyCode, + ProductFamilyText = group.Key.ProductFamilyText, + ProductHierarchyCode = group.Key.ProductHierarchyCode, + ProductHierarchyText = group.Key.ProductHierarchyText, + Currency = group.Key.Currency, + NetSalesActual = value, + SharePercent = PercentOf(value, total), + MaterialCount = group.Sum(row => row.MaterialCount), + RowCount = group.Sum(row => row.RowCount), + Countries = JoinCountries(group.Select(row => row.Countries)) + }; + }) + .OrderByDescending(row => Math.Abs(row.NetSalesActual)) + .ThenBy(row => row.ProductDivisionCode, StringComparer.OrdinalIgnoreCase) + .ThenBy(row => row.ProductFamilyCode, StringComparer.OrdinalIgnoreCase) + .ThenBy(row => row.ProductHierarchyCode, StringComparer.OrdinalIgnoreCase) + .ToList(); + + return _limitProductFinanceTop10 ? rows.Take(10).ToList() : rows; + } + + private ProductFinanceGroupKey BuildProductFinanceGroupKey(ManagementProductDivisionFinanceRow row) + { + return _productFinanceGroupLevel switch + { + ProductFinanceGroupLevels.Division => new ProductFinanceGroupKey( + row.ProductDivisionCode, + row.ProductDivisionText, + string.Empty, + string.Empty, + string.Empty, + string.Empty, + row.Currency), + ProductFinanceGroupLevels.Family => new ProductFinanceGroupKey( + row.ProductDivisionCode, + row.ProductDivisionText, + row.ProductFamilyCode, + row.ProductFamilyText, + string.Empty, + string.Empty, + row.Currency), + _ => new ProductFinanceGroupKey( + row.ProductDivisionCode, + row.ProductDivisionText, + row.ProductFamilyCode, + row.ProductFamilyText, + row.ProductHierarchyCode, + row.ProductHierarchyText, + row.Currency) + }; + } + private static Severity MapSeverity(string severity) => severity switch { "Warning" => Severity.Warning, @@ -1024,6 +1147,9 @@ private static string FormatPercent(decimal? value) => value.HasValue ? $"{value.Value:N1}%" : "-"; + private static decimal PercentOf(decimal value, decimal total) + => total == 0m ? 0m : value * 100m / total; + private static string FormatDateTime(DateTime? value) => value.HasValue ? value.Value.ToLocalTime().ToString("dd.MM.yyyy HH:mm") : "-"; @@ -1071,6 +1197,36 @@ return string.IsNullOrWhiteSpace(text) ? code : $"{code} - {text}"; } + private static string JoinCountries(IEnumerable countryValues) + { + var countries = countryValues + .SelectMany(value => value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(value => value, StringComparer.OrdinalIgnoreCase) + .Select(FormatCountryWithFlag); + + return string.Join(", ", countries); + } + + private static string FormatCountriesWithFlags(string countries) + => string.IsNullOrWhiteSpace(countries) + ? "-" + : JoinCountries([countries]); + + private static string FormatCountryWithFlag(string country) + { + if (string.IsNullOrWhiteSpace(country)) + return "-"; + + var normalized = country.Trim().ToUpperInvariant(); + if (normalized.Length != 2 || normalized.Any(character => character is < 'A' or > 'Z')) + return country; + + var flag = string.Concat(normalized.Select(character => char.ConvertFromUtf32(0x1F1E6 + character - 'A'))); + return $"{flag} {normalized}"; + } + private void SetSelectedCentralAdditionalValueFields(IEnumerable values) { _selectedCentralAdditionalValueFields = values @@ -1090,9 +1246,25 @@ : $"{formattedValue} / {value.MissingExchangeRateCount} ohne Kurs"; } + private string T(string german, string english) => UiText.Text(german, english); + + private static class ProductFinanceGroupLevels + { + public const string Hierarchy = "hierarchy"; + public const string Family = "family"; + public const string Division = "division"; + } + + private sealed record ProductFinanceGroupingOption(string Key, string GermanLabel, string EnglishLabel); + + private sealed record ProductFinanceGroupKey( + string ProductDivisionCode, + string ProductDivisionText, + string ProductFamilyCode, + string ProductFamilyText, + string ProductHierarchyCode, + string ProductHierarchyText, + string Currency); + private sealed record CurrencySelectOption(string Key, string Label); } - -@code { - private string T(string german, string english) => UiText.Text(german, english); -}