diff --git a/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor b/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor
index e2c6320..55b4c1d 100644
--- a/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor
+++ b/TrafagSalesExporter/Components/Pages/ManagementCockpit.razor
@@ -284,6 +284,95 @@
+
+
+
+
+ @T("Gesamtumsatz", "Total sales")
+ @FormatValue(_financeResult.ProductFinanceSummary.TotalValue, _financeResult.ProductFinanceSummary.DisplayCurrency)
+
+
+
+
+ @T("Zugeordneter Umsatz", "Assigned sales")
+ @FormatValue(_financeResult.ProductFinanceSummary.AssignedValue, _financeResult.ProductFinanceSummary.DisplayCurrency)
+ @FormatPercent(_financeResult.ProductFinanceSummary.AssignedValuePercent)
+
+
+
+
+ @T("Nicht zugeordnet", "Unassigned")
+ @FormatValue(_financeResult.ProductFinanceSummary.UnassignedValue, _financeResult.ProductFinanceSummary.DisplayCurrency)
+ @FormatPercent(_financeResult.ProductFinanceSummary.UnassignedValuePercent)
+
+
+
+
+ @T("Nicht im Stamm", "Not in master")
+ @FormatValue(_financeResult.ProductFinanceSummary.MissingReferenceValue, _financeResult.ProductFinanceSummary.DisplayCurrency)
+ @FormatPercent(_financeResult.ProductFinanceSummary.MissingReferenceValuePercent)
+
+
+
+
+
+ @T("Umsatz nach Produktsparte", "Sales by product division")
+
+
+ @T("Produktsparte", "Product division")
+ @T("Produktfamilie", "Product family")
+ PAPH1
+ @T("Umsatz", "Sales")
+ @T("Anteil", "Share")
+ @T("Materialien", "Materials")
+ @T("Zeilen", "Rows")
+ @T("Laender", "Countries")
+
+
+ @BuildCodeText(context.ProductDivisionCode, context.ProductDivisionText)
+ @BuildCodeText(context.ProductFamilyCode, context.ProductFamilyText)
+ @BuildCodeText(context.ProductHierarchyCode, context.ProductHierarchyText)
+ @FormatValue(context.NetSalesActual, context.Currency)
+ @FormatPercent(context.SharePercent)
+ @context.MaterialCount.ToString("N0")
+ @context.RowCount.ToString("N0")
+ @context.Countries
+
+
+ @T("Keine zugeordneten Spartenumsaetze fuer diese Filter.", "No assigned division sales for these filters.")
+
+
+
+
+
+ @T("Umsatzabdeckung nach Land", "Sales coverage by country")
+
+
+ @T("Land", "Country")
+ TSC
+ @T("Gesamt", "Total")
+ @T("Zugeordnet", "Assigned")
+ @T("Nicht zugeordnet", "Unassigned")
+ @T("Nicht im Stamm", "Not in master")
+ @T("Material fehlt", "Material missing")
+ @T("Abdeckung", "Coverage")
+
+
+ @context.CountryKey
+ @context.Tsc
+ @FormatValue(context.TotalValue, context.Currency)
+ @FormatValue(context.AssignedValue, context.Currency)
+ @FormatValue(context.UnassignedValue, context.Currency)
+ @FormatValue(context.MissingReferenceValue, context.Currency)
+ @FormatValue(context.MissingMaterialValue, context.Currency)
+ @FormatPercent(context.AssignedValuePercent)
+
+
+ @T("Keine Umsatzabdeckung fuer diese Filter.", "No sales coverage for these filters.")
+
+
+
+
diff --git a/TrafagSalesExporter/Models/ManagementCockpitModels.cs b/TrafagSalesExporter/Models/ManagementCockpitModels.cs
index 90bc973..39e9f4c 100644
--- a/TrafagSalesExporter/Models/ManagementCockpitModels.cs
+++ b/TrafagSalesExporter/Models/ManagementCockpitModels.cs
@@ -225,6 +225,48 @@ public class ManagementProductAssignmentSummary
public int ReferenceMaterialCount { get; set; }
}
+public class ManagementProductFinanceSummary
+{
+ public decimal TotalValue { get; set; }
+ public decimal AssignedValue { get; set; }
+ public decimal UnassignedValue { get; set; }
+ public decimal MissingReferenceValue { get; set; }
+ public decimal MissingMaterialValue { get; set; }
+ public decimal AssignedValuePercent { get; set; }
+ public decimal UnassignedValuePercent { get; set; }
+ public decimal MissingReferenceValuePercent { get; set; }
+ public string DisplayCurrency { get; set; } = string.Empty;
+}
+
+public class ManagementProductDivisionFinanceRow
+{
+ public string ProductDivisionCode { get; set; } = string.Empty;
+ public string ProductDivisionText { get; set; } = string.Empty;
+ public string ProductFamilyCode { get; set; } = string.Empty;
+ public string ProductFamilyText { get; set; } = string.Empty;
+ public string ProductHierarchyCode { get; set; } = string.Empty;
+ public string ProductHierarchyText { get; set; } = string.Empty;
+ public string Currency { get; set; } = string.Empty;
+ public decimal NetSalesActual { get; set; }
+ public decimal SharePercent { get; set; }
+ public int MaterialCount { get; set; }
+ public int RowCount { get; set; }
+ public string Countries { get; set; } = string.Empty;
+}
+
+public class ManagementProductFinanceCountryRow
+{
+ public string CountryKey { get; set; } = string.Empty;
+ public string Tsc { get; set; } = string.Empty;
+ public string Currency { get; set; } = string.Empty;
+ public decimal TotalValue { get; set; }
+ public decimal AssignedValue { get; set; }
+ public decimal UnassignedValue { get; set; }
+ public decimal MissingReferenceValue { get; set; }
+ public decimal MissingMaterialValue { get; set; }
+ public decimal AssignedValuePercent { get; set; }
+}
+
public class ManagementProductAssignmentCountryRow
{
public string CountryKey { get; set; } = string.Empty;
@@ -278,6 +320,9 @@ public class ManagementFinanceSummaryResult
public List DataStatusRows { get; set; } = [];
public List CreditCandidates { get; set; } = [];
public List DataQualityRows { get; set; } = [];
+ public ManagementProductFinanceSummary ProductFinanceSummary { get; set; } = new();
+ public List ProductDivisionFinanceRows { get; set; } = [];
+ public List ProductFinanceCountryRows { get; set; } = [];
public ManagementProductAssignmentSummary ProductAssignmentSummary { get; set; } = new();
public List ProductAssignmentCountryRows { get; set; } = [];
public List ProductAssignmentRows { get; set; } = [];
diff --git a/TrafagSalesExporter/Services/ManagementCockpitService.cs b/TrafagSalesExporter/Services/ManagementCockpitService.cs
index e9f8076..a99f72f 100644
--- a/TrafagSalesExporter/Services/ManagementCockpitService.cs
+++ b/TrafagSalesExporter/Services/ManagementCockpitService.cs
@@ -460,6 +460,7 @@ public class ManagementCockpitService : IManagementCockpitService
var dataStatusRows = await BuildFinanceDataStatusRowsAsync(db);
var countryRows = BuildFinanceCountryStatusRows(scopedRows, referenceByKey);
var productAssignmentRows = BuildProductAssignmentRows(scopedRows, allRows);
+ var productFinanceSummary = BuildProductFinanceSummary(productAssignmentRows, resultCurrencies);
return new ManagementFinanceSummaryResult
{
@@ -497,6 +498,9 @@ public class ManagementCockpitService : IManagementCockpitService
DataStatusRows = dataStatusRows,
CreditCandidates = BuildFinanceCreditCandidates(scopedRows),
DataQualityRows = BuildFinanceDataQualityRows(scopedRows),
+ ProductFinanceSummary = productFinanceSummary,
+ ProductDivisionFinanceRows = BuildProductDivisionFinanceRows(productAssignmentRows),
+ ProductFinanceCountryRows = BuildProductFinanceCountryRows(productAssignmentRows),
ProductAssignmentSummary = BuildProductAssignmentSummary(productAssignmentRows),
ProductAssignmentCountryRows = BuildProductAssignmentCountryRows(productAssignmentRows),
ProductAssignmentRows = productAssignmentRows
@@ -711,6 +715,101 @@ public class ManagementCockpitService : IManagementCockpitService
.Count()
};
+ private static ManagementProductFinanceSummary BuildProductFinanceSummary(
+ IReadOnlyCollection rows,
+ IReadOnlyCollection currencies)
+ {
+ var total = rows.Sum(row => row.NetSalesActual);
+ var assigned = rows.Where(row => row.Status == ProductAssignmentStatuses.Assigned).Sum(row => row.NetSalesActual);
+ var unassigned = rows.Where(row => row.Status == ProductAssignmentStatuses.Unassigned).Sum(row => row.NetSalesActual);
+ var missingReference = rows.Where(row => row.Status == ProductAssignmentStatuses.NoReference).Sum(row => row.NetSalesActual);
+ var missingMaterial = rows.Where(row => row.Status == ProductAssignmentStatuses.MissingMaterial).Sum(row => row.NetSalesActual);
+
+ return new ManagementProductFinanceSummary
+ {
+ TotalValue = total,
+ AssignedValue = assigned,
+ UnassignedValue = unassigned,
+ MissingReferenceValue = missingReference,
+ MissingMaterialValue = missingMaterial,
+ AssignedValuePercent = PercentOf(assigned, total),
+ UnassignedValuePercent = PercentOf(unassigned, total),
+ MissingReferenceValuePercent = PercentOf(missingReference, total),
+ DisplayCurrency = BuildDisplayCurrencyLabel(currencies)
+ };
+ }
+
+ private static List BuildProductDivisionFinanceRows(IEnumerable rows)
+ {
+ var assignedRows = rows
+ .Where(row => row.Status == ProductAssignmentStatuses.Assigned)
+ .ToList();
+ var totalsByCurrency = assignedRows
+ .GroupBy(row => row.Currency, StringComparer.OrdinalIgnoreCase)
+ .ToDictionary(group => group.Key, group => group.Sum(row => row.NetSalesActual), StringComparer.OrdinalIgnoreCase);
+
+ return assignedRows
+ .GroupBy(row => new
+ {
+ row.ProductDivisionCode,
+ row.ProductDivisionText,
+ row.ProductFamilyCode,
+ row.ProductFamilyText,
+ row.ProductHierarchyCode,
+ row.ProductHierarchyText,
+ row.Currency
+ })
+ .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.Select(row => row.Material).Distinct(StringComparer.OrdinalIgnoreCase).Count(),
+ RowCount = group.Sum(row => row.RowCount),
+ Countries = JoinDistinct(group.Select(row => row.CountryKey))
+ };
+ })
+ .OrderByDescending(row => Math.Abs(row.NetSalesActual))
+ .ThenBy(row => row.ProductDivisionCode, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+ }
+
+ private static List BuildProductFinanceCountryRows(IEnumerable rows)
+ => rows
+ .GroupBy(row => new { row.CountryKey, row.Tsc, row.Currency })
+ .Select(group =>
+ {
+ var rowList = group.ToList();
+ var total = rowList.Sum(row => row.NetSalesActual);
+ var assigned = rowList.Where(row => row.Status == ProductAssignmentStatuses.Assigned).Sum(row => row.NetSalesActual);
+ return new ManagementProductFinanceCountryRow
+ {
+ CountryKey = group.Key.CountryKey,
+ Tsc = group.Key.Tsc,
+ Currency = group.Key.Currency,
+ TotalValue = total,
+ AssignedValue = assigned,
+ UnassignedValue = rowList.Where(row => row.Status == ProductAssignmentStatuses.Unassigned).Sum(row => row.NetSalesActual),
+ MissingReferenceValue = rowList.Where(row => row.Status == ProductAssignmentStatuses.NoReference).Sum(row => row.NetSalesActual),
+ MissingMaterialValue = rowList.Where(row => row.Status == ProductAssignmentStatuses.MissingMaterial).Sum(row => row.NetSalesActual),
+ AssignedValuePercent = PercentOf(assigned, total)
+ };
+ })
+ .OrderBy(row => row.CountryKey, StringComparer.OrdinalIgnoreCase)
+ .ThenBy(row => row.Tsc, StringComparer.OrdinalIgnoreCase)
+ .ThenBy(row => row.Currency, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
private static List BuildProductAssignmentCountryRows(IEnumerable rows)
=> rows
.GroupBy(row => new { row.CountryKey, row.Tsc })
@@ -774,6 +873,9 @@ public class ManagementCockpitService : IManagementCockpitService
private static string NormalizeMaterialKey(string value)
=> string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim().ToUpperInvariant();
+ private static decimal PercentOf(decimal value, decimal total)
+ => total == 0m ? 0m : value * 100m / total;
+
private static ManagementFinanceDataQualityRow BuildQualityRow(string issue, int count, int totalRows)
{
var share = totalRows == 0 ? 0m : count / (decimal)totalRows;
diff --git a/TrafagSalesExporter/TrafagSalesExporter.Tests/ManagementCockpitServiceTests.cs b/TrafagSalesExporter/TrafagSalesExporter.Tests/ManagementCockpitServiceTests.cs
index 9da2858..0d9d555 100644
--- a/TrafagSalesExporter/TrafagSalesExporter.Tests/ManagementCockpitServiceTests.cs
+++ b/TrafagSalesExporter/TrafagSalesExporter.Tests/ManagementCockpitServiceTests.cs
@@ -368,6 +368,29 @@ public class ManagementCockpitServiceTests : IDisposable
row.Tsc == "TRDE" &&
row.MatchedMaterialCount == 1 &&
row.UnassignedMaterialCount == 1);
+
+ Assert.Equal(260m, result.ProductFinanceSummary.TotalValue);
+ Assert.Equal(180m, result.ProductFinanceSummary.AssignedValue);
+ Assert.Equal(30m, result.ProductFinanceSummary.UnassignedValue);
+ Assert.Equal(50m, result.ProductFinanceSummary.MissingReferenceValue);
+ Assert.Equal(180m * 100m / 260m, result.ProductFinanceSummary.AssignedValuePercent);
+
+ Assert.Contains(result.ProductDivisionFinanceRows, row =>
+ row.ProductDivisionCode == "0001" &&
+ row.Currency == "EUR" &&
+ row.NetSalesActual == 80m &&
+ row.MaterialCount == 1 &&
+ row.Countries == "DE");
+ Assert.Contains(result.ProductDivisionFinanceRows, row =>
+ row.ProductDivisionCode == "0001" &&
+ row.Currency == "CHF" &&
+ row.NetSalesActual == 100m);
+
+ var deFinanceCoverage = Assert.Single(result.ProductFinanceCountryRows, row => row.CountryKey == "DE" && row.Tsc == "TRDE");
+ Assert.Equal(100m, deFinanceCoverage.TotalValue);
+ Assert.Equal(80m, deFinanceCoverage.AssignedValue);
+ Assert.Equal(20m, deFinanceCoverage.UnassignedValue);
+ Assert.Equal(80m, deFinanceCoverage.AssignedValuePercent);
}
private async Task SeedCentralRowsAsync(params CentralSalesRecord[] rows)