Add purchasing period analytics

This commit is contained in:
2026-06-05 13:54:36 +02:00
parent 43250a4abc
commit aa6d0d0804
4 changed files with 598 additions and 44 deletions
@@ -61,6 +61,35 @@
</div>
</MudPaper>
<MudPaper Class="pa-3 mb-4 purchasing-filter-panel" Outlined="true">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Spacing="2">
<div>
<MudText Typo="Typo.subtitle1">@T("Zeitraum", "Period")</MudText>
<MudText Typo="Typo.body2" Class="purchasing-muted">
@T("Alle Einkauf-Kennzahlen, Top-Listen und 3D-Daten werden auf diesen Zeitraum abgegrenzt.",
"All purchasing KPIs, top lists and 3D data are limited to this period.")
</MudText>
</div>
<MudChip T="string" Color="Color.Info" Variant="Variant.Outlined">@FilterLabel</MudChip>
</MudStack>
<div class="purchasing-filter-grid">
<label>
<span>@T("Von Monat", "From month")</span>
<input type="month" value="@_filterFromMonth" @onchange="SetFilterFromMonth" />
</label>
<label>
<span>@T("Bis Monat", "To month")</span>
<input type="month" value="@_filterToMonth" @onchange="SetFilterToMonth" />
</label>
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.FilterAlt" OnClick="ApplyPurchasingFilterAsync" Disabled="_liveLoading">
@T("Anwenden", "Apply")
</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Info" StartIcon="@Icons.Material.Filled.Update" OnClick="SetLastThreeYearsAsync" Disabled="_liveLoading">
@T("Letzte 3 Jahre", "Last 3 years")
</MudButton>
</div>
</MudPaper>
<MudGrid Class="mb-4" Spacing="2">
@foreach (var card in KpiCards)
{
@@ -390,6 +419,53 @@
</MudPaper>
</MudItem>
}
else
{
<MudItem xs="12">
<PurchasingSection TitleDe="@SelectedIdeaAnalysisTitleDe"
TitleEn="@SelectedIdeaAnalysisTitleEn"
DescriptionDe="@SelectedIdeaAnalysisDescriptionDe"
DescriptionEn="@SelectedIdeaAnalysisDescriptionEn"
ChartTitleDe="@SelectedIdeaChartTitleDe"
ChartTitleEn="@SelectedIdeaChartTitleEn"
Kpis="@SelectedIdeaKpis"
ChartRows="@SelectedIdeaChartRows"
StatusRows="@SelectedIdeaStatusRows"
DetailRows="@SelectedIdeaDetailRows" />
</MudItem>
<MudItem xs="12">
<MudPaper Class="pa-3 purchasing-overview-panel" Outlined="true">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-3">
<div>
<MudText Typo="Typo.h6">@T("Detail-Hotlist", "Detail hotlist")</MudText>
<MudText Typo="Typo.body2" Class="purchasing-muted">
@T("Direkt aus dem Einkauf-Cache berechnet, keine Simulation.",
"Calculated directly from the purchasing cache, no simulation.")
</MudText>
</div>
<MudChip T="string" Color="Color.Success" Variant="Variant.Outlined">@SelectedIdeaRows.Count @T("Zeilen", "rows")</MudChip>
</MudStack>
<MudTable Items="@SelectedIdeaRows" Dense="true" Hover="true" Striped="true">
<HeaderContent>
<MudTh>@T("Objekt", "Object")</MudTh>
<MudTh>@T("Wert", "Value")</MudTh>
<MudTh>@T("Detail", "Detail")</MudTh>
<MudTh>@T("Ampel", "Status")</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Label</MudTd>
<MudTd><strong>@context.Value</strong></MudTd>
<MudTd>@context.Detail</MudTd>
<MudTd>
<MudChip T="string" Size="Size.Small" Color="@ResolveAnalysisSeverityColor(context.Severity)" Variant="Variant.Outlined">
@context.Severity
</MudChip>
</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
</MudItem>
}
</MudGrid>
break;
case "kennzahlen":
@@ -583,6 +659,10 @@
private double _purchasing3dFactor = 1d;
private double _purchasing3dLabelScale = 1.5d;
private string _lastRendered3dUri = string.Empty;
private string _filterFromMonth = BuildDefaultFromMonth();
private string _filterToMonth = BuildDefaultToMonth();
private string FilterLabel => $"{_filterFromMonth} - {_filterToMonth}";
private string CurrentPurchasingPage
{
@@ -605,7 +685,7 @@
private IReadOnlyList<PurchasingKpiCard> KpiCards =>
[
new("Spend total", "Total spend", _liveState.EkpoLoaded ? FormatChf(_liveState.SpendChfSample) : T("wartet auf EKPO", "waiting for EKPO"), _liveState.UsesCache ? "Einkauf Cache Vollwerte" : _liveState.EkpoLoaded ? "EKPO-Live-Sample" : "EKKO live, Positionswerte fehlen noch", _liveState.UsesCache ? "purchasing cache full values" : _liveState.EkpoLoaded ? "EKPO live sample" : "EKKO live, position values still missing", Icons.Material.Filled.Payments, Color.Primary),
new("Offene Bestellungen", "Open orders", _liveState.EkkoLoaded ? _liveState.PurchaseOrderCount.ToString("N0") : "-", _liveState.EkkoLoaded ? "EKKO-Belege seit Jahresbeginn" : "Noch nicht geladen", _liveState.EkkoLoaded ? "EKKO orders since start of year" : "Not loaded yet", Icons.Material.Filled.PendingActions, Color.Warning),
new("Offene Bestellungen", "Open orders", _liveState.EkkoLoaded ? _liveState.PurchaseOrderCount.ToString("N0") : "-", _liveState.EkkoLoaded ? $"EKKO-Belege im Zeitraum {FilterLabel}" : "Noch nicht geladen", _liveState.EkkoLoaded ? $"EKKO orders in period {FilterLabel}" : "Not loaded yet", Icons.Material.Filled.PendingActions, Color.Warning),
new("Kontrakte", "Contracts", _liveState.EketLoaded ? FormatChf(_liveState.ContractValueSample) : T("wartet auf EKET", "waiting for EKET"), _liveState.UsesCache ? "Restwert aus Einkauf Cache" : _liveState.EketLoaded ? "Restwert aus EKET/EKPO-Sample" : "EKKO live, Terminwerte fehlen noch", _liveState.UsesCache ? "Remaining value from purchasing cache" : _liveState.EketLoaded ? "Remaining value from EKET/EKPO sample" : "EKKO live, schedule values still missing", Icons.Material.Filled.Assignment, Color.Info),
new("Lieferantenperformance", "Supplier performance", _liveState.EkkoLoaded ? _liveState.SupplierCount.ToString("N0") : "-", _liveState.EkkoLoaded ? "Lieferanten in EKKO-Liveprobe" : "Noch nicht geladen", _liveState.EkkoLoaded ? "Suppliers in EKKO live sample" : "Not loaded yet", Icons.Material.Filled.Verified, Color.Success)
];
@@ -654,7 +734,7 @@
private IReadOnlyList<PurchasingPipelineStep> PipelineRows =>
[
new("EKKO Bestellkopf", "EKKO purchase header", _liveState.EkkoLoaded ? $"{_liveState.PurchaseOrderCount:N0}" : "-", _liveState.EkkoLoaded ? "Bestellungen seit Jahresbeginn sind live verfuegbar" : "Bestellkopf wartet auf SAP", _liveState.EkkoLoaded ? "Orders since start of year are available live" : "Purchase header is waiting for SAP", Icons.Material.Filled.ReceiptLong, _liveState.EkkoLoaded, _liveState.EkkoLoaded ? Color.Success : Color.Warning),
new("EKKO Bestellkopf", "EKKO purchase header", _liveState.EkkoLoaded ? $"{_liveState.PurchaseOrderCount:N0}" : "-", _liveState.EkkoLoaded ? $"Bestellungen im Zeitraum {FilterLabel}" : "Bestellkopf wartet auf SAP", _liveState.EkkoLoaded ? $"Orders in period {FilterLabel}" : "Purchase header is waiting for SAP", Icons.Material.Filled.ReceiptLong, _liveState.EkkoLoaded, _liveState.EkkoLoaded ? Color.Success : Color.Warning),
new("EKPO Positionen", "EKPO item rows", _liveState.EkpoLoaded ? $"{_liveState.PositionSampleCount:N0}" : "0", _liveState.EkpoLoaded ? "Spend, Artikel und Warengruppen koennen echt berechnet werden" : "Spend und offene Werte bleiben Simulation", _liveState.EkpoLoaded ? "Spend, articles and material groups can be calculated real" : "Spend and open values remain simulation", Icons.Material.Filled.Inventory2, _liveState.EkpoLoaded, _liveState.EkpoLoaded ? Color.Success : Color.Warning),
new("EKET Termine", "EKET schedules", _liveState.EketLoaded ? $"{_liveState.ScheduleSampleCount:N0}" : "0", _liveState.EketLoaded ? "Faelligkeiten und Terminstatus koennen echt berechnet werden" : "Faelligkeiten und Kontrakte warten auf SAP", _liveState.EketLoaded ? "Due dates and schedule status can be calculated real" : "Due dates and contracts wait for SAP", Icons.Material.Filled.EventAvailable, _liveState.EketLoaded, _liveState.EketLoaded ? Color.Success : Color.Warning),
new("Dashboard Layer", "Dashboard layer", _liveState.EkkoLoaded ? "aktiv" : "bereit", "Livewerte, Simulation und 3D-Analyse werden getrennt ausgewiesen", "Live values, simulation and 3D analysis are shown separately", Icons.Material.Filled.DashboardCustomize, true, Color.Info)
@@ -663,7 +743,7 @@
private IReadOnlyList<string> ManagementInsights =>
[
_liveState.EkkoLoaded
? T($"{_liveState.PurchaseOrderCount:N0} Einkaufsbelege sind fuer 2026 live im Cockpit.", $"{_liveState.PurchaseOrderCount:N0} purchase orders are live in the cockpit for 2026.")
? T($"{_liveState.PurchaseOrderCount:N0} Einkaufsbelege sind im Zeitraum {FilterLabel} im Cockpit.", $"{_liveState.PurchaseOrderCount:N0} purchase orders are in the cockpit for {FilterLabel}.")
: T("EKKO wird geladen; danach erscheinen Bestellungen und Lieferanten live.", "EKKO is loading; orders and suppliers appear live afterwards."),
_liveState.EkpoLoaded
? T("Spend, Artikel und Warengruppen koennen nun aus SAP-Positionen kommen.", "Spend, articles and material groups can now come from SAP item rows.")
@@ -684,7 +764,7 @@
private IReadOnlyList<PurchasingSectionKpi> OpenOrderKpis =>
[
new("Bestellungen", "Orders", _liveState.EkkoLoaded ? _liveState.PurchaseOrderCount.ToString("N0") : "-", "EKKO live seit Jahresbeginn", "EKKO live since start of year"),
new("Bestellungen", "Orders", _liveState.EkkoLoaded ? _liveState.PurchaseOrderCount.ToString("N0") : "-", $"EKKO im Zeitraum {FilterLabel}", $"EKKO in period {FilterLabel}"),
new("Lieferanten", "Suppliers", _liveState.EkkoLoaded ? _liveState.SupplierCount.ToString("N0") : "-", "aus EKKO-Liveprobe", "from EKKO live sample"),
new("Offener Wert", "Open value", _liveState.EketLoaded ? FormatChf(_liveState.OpenValueSample) : FormatChf(Purchasing3dBaseRows.Sum(x => x.OpenValue)), _liveState.EketLoaded ? "aus SAP Terminen/Positionen" : "Simulation bis EKET liefert", _liveState.EketLoaded ? "from SAP schedules/items" : "simulation until EKET delivers"),
new("Offene Menge", "Open quantity", _liveState.EketLoaded ? _liveState.OpenQuantitySample.ToString("N0") : Purchasing3dBaseRows.Sum(x => x.OpenQuantity).ToString("N0"), _liveState.EketLoaded ? "aus SAP Terminen" : "Simulation bis EKET liefert", _liveState.EketLoaded ? "from SAP schedules" : "simulation until EKET delivers")
@@ -700,7 +780,7 @@
private IReadOnlyList<PurchasingSectionKpi> SupplierKpis =>
[
new("Aktive Lieferanten", "Active suppliers", _liveState.EkkoLoaded ? _liveState.SupplierCount.ToString("N0") : "-", "EKKO live seit Jahresbeginn", "EKKO live since start of year"),
new("Aktive Lieferanten", "Active suppliers", _liveState.EkkoLoaded ? _liveState.SupplierCount.ToString("N0") : "-", $"EKKO im Zeitraum {FilterLabel}", $"EKKO in period {FilterLabel}"),
new("Performance Score", "Performance score", $"{Purchasing3dBaseRows.Average(x => x.SupplierScore):N1}%", "Simulation bis Bewertungsdaten kommen", "simulation until rating data arrives"),
new("Preisindikator", "Price indicator", _liveState.EkpoLoaded ? FormatChf(_liveState.SpendChfSample) : "wartet", "Netwr CHF/Stk braucht EKPO", "Netwr CHF/unit needs EKPO"),
new("Qualitaet", "Quality", "offen", "Reklamationsquelle noch nicht angebunden", "claim source not connected yet")
@@ -1096,6 +1176,10 @@
new("openValue", "Offener Bestellwert", "Open order value", "CHF"),
new("openQuantity", "Offene Menge", "Open quantity", "Qty"),
new("contractValue", "Kontrakt-Restwert", "Contract remaining value", "CHF"),
new("deliveryRisk", "Liefertermin-Risiko", "Delivery due-date risk", "CHF"),
new("priceVariance", "Preisabweichung", "Price variance", "CHF"),
new("spendConcentration", "Spend-Konzentration", "Spend concentration", "CHF"),
new("dataQuality", "Datenqualitaet", "Data quality", "Issues"),
new("supplierScore", "Lieferantenperformance", "Supplier performance", "%")
];
@@ -1133,6 +1217,152 @@
private IReadOnlyList<PurchasingSectionChartRow> SupplierChartRows
=> BuildPurchasingChartRows(x => x.SupplierScore, value => $"{value:N1}%");
private IReadOnlyList<PurchasingIdeaAnalysisRow> SelectedIdeaRows => CurrentPurchasingPage switch
{
"ideen/liefertermin-risiko" => _liveState.DeliveryRiskRows,
"ideen/preisabweichung" => _liveState.PriceVarianceRows,
"ideen/spend-konzentration" => _liveState.SpendConcentrationRows,
"ideen/datenqualitaet" => _liveState.DataQualityRows,
_ => []
};
private IReadOnlyList<PurchasingSectionChartRow> SelectedIdeaChartRows => CurrentPurchasingPage switch
{
"ideen/liefertermin-risiko" => BuildLiveChartRows(_liveState.DeliveryRiskChartRows, FormatChf),
"ideen/preisabweichung" => BuildLiveChartRows(_liveState.PriceVarianceChartRows, FormatChf),
"ideen/spend-konzentration" => BuildLiveChartRows(_liveState.SpendConcentrationChartRows, FormatChf),
"ideen/datenqualitaet" => BuildLiveChartRows(_liveState.DataQualityChartRows, value => value.ToString("N0")),
_ => []
};
private IReadOnlyList<PurchasingSectionKpi> SelectedIdeaKpis => CurrentPurchasingPage switch
{
"ideen/liefertermin-risiko" =>
[
new("Ueberfaellig", "Overdue", FormatChartValue("Ueberfaellig", _liveState.DeliveryRiskChartRows, FormatChf), "offener Wert", "open value"),
new("0-7 Tage", "0-7 days", FormatChartValue("0-7 Tage", _liveState.DeliveryRiskChartRows, FormatChf), "kurzfristig faellig", "due short-term"),
new("Hotlist", "Hotlist", _liveState.DeliveryRiskRows.Count.ToString("N0"), "Lieferant / Artikel", "supplier / article"),
new("Datenbasis", "Data basis", _liveState.EketLoaded ? "EKET Cache" : "-", "offene Mengen", "open quantities")
],
"ideen/preisabweichung" =>
[
new("Preissteigerungen", "Price increases", _liveState.PriceVarianceRows.Count.ToString("N0"), "2026 gegen 2025", "2026 vs 2025"),
new("Top Wirkung", "Top impact", _liveState.PriceVarianceChartRows.Count > 0 ? FormatChf(_liveState.PriceVarianceChartRows.Max(x => x.Value)) : "-", "CHF Effekt", "CHF effect"),
new("Lieferanten", "Suppliers", _liveState.PriceVarianceChartRows.Count.ToString("N0"), "mit Abweichung", "with variance"),
new("Datenbasis", "Data basis", _liveState.EkpoLoaded ? "EKPO + EKKO" : "-", "Stueckpreis", "unit price")
],
"ideen/spend-konzentration" =>
[
new("Top 10 Spend", "Top 10 spend", FormatChf(_liveState.SpendConcentrationChartRows.Sum(x => x.Value)), "Lieferanten-Pareto", "supplier pareto"),
new("Top Anteil", "Top share", FormatTopShare(), "vom Gesamtspend", "of total spend"),
new("Lieferanten", "Suppliers", _liveState.SupplierCount.ToString("N0"), "EKKO Cache", "EKKO cache"),
new("Datenbasis", "Data basis", _liveState.EkpoLoaded ? "EKPO + EKKO" : "-", "Spend netto", "net spend")
],
"ideen/datenqualitaet" =>
[
new("Pruefungen", "Checks", _liveState.DataQualityRows.Count.ToString("N0"), "Pflichtfelder", "required fields"),
new("Fehler gesamt", "Total issues", _liveState.DataQualityChartRows.Sum(x => x.Value).ToString("N0"), "Cache-Zeilen", "cache rows"),
new("EKPO", "EKPO", _liveState.PositionSampleCount.ToString("N0"), "Positionen", "items"),
new("EKKO", "EKKO", _liveState.PurchaseOrderCount.ToString("N0"), "Bestellkoepfe", "headers")
],
_ => []
};
private IReadOnlyList<PurchasingSectionStatusRow> SelectedIdeaStatusRows =>
[
BuildStatus("Cache", "Cache", _liveState.UsesCache, _liveState.UsesCache ? _liveState.CacheStatus : "leer"),
BuildStatus("EKKO", "EKKO", _liveState.EkkoLoaded, _liveState.PurchaseOrderCount.ToString("N0")),
BuildStatus("EKPO", "EKPO", _liveState.EkpoLoaded, _liveState.PositionSampleCount.ToString("N0")),
BuildStatus("EKET", "EKET", _liveState.EketLoaded, _liveState.ScheduleSampleCount.ToString("N0"))
];
private IReadOnlyList<PurchasingSectionDetailRow> SelectedIdeaDetailRows => CurrentPurchasingPage switch
{
"ideen/liefertermin-risiko" =>
[
new("Faelligkeits-Buckets", "Due-date buckets", $"{_liveState.DeliveryRiskChartRows.Count:N0}", "EKET.Eindt", "SAP live"),
new("Offener Wert", "Open value", FormatChf(_liveState.OpenValueSample), "EKET/EKPO", "SAP live"),
new("Hotlist", "Hotlist", $"{_liveState.DeliveryRiskRows.Count:N0}", "Lieferant / Artikel", "SAP live"),
new("Berechnung", "Calculation", "Menge - WEMNG", "EKET", "SAP live")
],
"ideen/preisabweichung" =>
[
new("Vergleich", "Comparison", "2026 vs 2025", "EKKO.Bedat", "SAP live"),
new("Preis", "Price", "NETWR / MENGE", "EKPO", "SAP live"),
new("CHF Wirkung", "CHF impact", FormatChf(_liveState.PriceVarianceChartRows.Sum(x => x.Value)), "Preisdelta * Menge", "SAP live"),
new("Hotlist", "Hotlist", $"{_liveState.PriceVarianceRows.Count:N0}", "Lieferant / Artikel", "SAP live")
],
"ideen/spend-konzentration" =>
[
new("Gesamtspend", "Total spend", FormatChf(_liveState.SpendChfSample), "EKPO.Netwr", "SAP live"),
new("Top Lieferanten", "Top suppliers", $"{_liveState.SpendConcentrationRows.Count:N0}", "EKKO.Lifnr", "SAP live"),
new("Top Anteil", "Top share", FormatTopShare(), "Pareto", "SAP live"),
new("Warengruppen", "Material groups", TopMaterialGroupLabel, "EKPO.Matkl", "SAP live")
],
"ideen/datenqualitaet" =>
[
new("Fehlender Lieferant", "Missing supplier", FormatChartValue("fehlender Lieferant", _liveState.DataQualityChartRows, value => value.ToString("N0")), "EKKO.Lifnr", "SAP live"),
new("Fehlende Warengruppe", "Missing material group", FormatChartValue("fehlende Warengruppe", _liveState.DataQualityChartRows, value => value.ToString("N0")), "EKPO.Matkl", "SAP live"),
new("Nullmenge", "Zero quantity", FormatChartValue("Nullmenge", _liveState.DataQualityChartRows, value => value.ToString("N0")), "EKPO.Menge", "SAP live"),
new("Nullwert", "Zero value", FormatChartValue("Nullwert", _liveState.DataQualityChartRows, value => value.ToString("N0")), "EKPO.Netwr", "SAP live")
],
_ => []
};
private string SelectedIdeaAnalysisTitleDe => CurrentPurchasingPage switch
{
"ideen/liefertermin-risiko" => "Liefertermin-Risiko produktiv",
"ideen/preisabweichung" => "Preisabweichung produktiv",
"ideen/spend-konzentration" => "Spend-Konzentration produktiv",
"ideen/datenqualitaet" => "Datenqualitaet produktiv",
_ => "Einkauf Analyse"
};
private string SelectedIdeaAnalysisTitleEn => CurrentPurchasingPage switch
{
"ideen/liefertermin-risiko" => "Delivery due-date risk productive",
"ideen/preisabweichung" => "Price variance productive",
"ideen/spend-konzentration" => "Spend concentration productive",
"ideen/datenqualitaet" => "Data quality productive",
_ => "Purchasing analysis"
};
private string SelectedIdeaAnalysisDescriptionDe => CurrentPurchasingPage switch
{
"ideen/liefertermin-risiko" => "Offene EKET-Mengen werden bewertet und nach Faelligkeit, Lieferant und Artikel priorisiert.",
"ideen/preisabweichung" => "Netto-Stueckpreise werden 2026 gegen 2025 verglichen und nach CHF-Wirkung sortiert.",
"ideen/spend-konzentration" => "Lieferantenspend wird als Pareto ausgewertet, um Abhaengigkeit und Buendelungspotenzial zu zeigen.",
"ideen/datenqualitaet" => "Pflichtfelder und Nullwerte im Einkauf-Cache werden als Qualitaetsampel gezaehlt.",
_ => "Echte Analyse aus dem Einkauf-Cache."
};
private string SelectedIdeaAnalysisDescriptionEn => CurrentPurchasingPage switch
{
"ideen/liefertermin-risiko" => "Open EKET quantities are valued and prioritised by due date, supplier and article.",
"ideen/preisabweichung" => "Net unit prices are compared 2026 against 2025 and sorted by CHF impact.",
"ideen/spend-konzentration" => "Supplier spend is evaluated as pareto to show dependency and bundling potential.",
"ideen/datenqualitaet" => "Required fields and zero values in the purchasing cache are counted as quality indicators.",
_ => "Real analysis from the purchasing cache."
};
private string SelectedIdeaChartTitleDe => CurrentPurchasingPage switch
{
"ideen/liefertermin-risiko" => "Offener Wert nach Faelligkeit",
"ideen/preisabweichung" => "Preissteigerungswirkung nach Lieferant",
"ideen/spend-konzentration" => "Top Lieferanten Spend",
"ideen/datenqualitaet" => "Datenqualitaetsfehler",
_ => "Analyse"
};
private string SelectedIdeaChartTitleEn => CurrentPurchasingPage switch
{
"ideen/liefertermin-risiko" => "Open value by due date",
"ideen/preisabweichung" => "Price increase impact by supplier",
"ideen/spend-konzentration" => "Top supplier spend",
"ideen/datenqualitaet" => "Data quality issues",
_ => "Analysis"
};
private IReadOnlyList<PurchasingSectionStatusRow> SpendStatusRows =>
[
BuildStatus("EKKO Bestellkoepfe", "EKKO purchase headers", _liveState.EkkoLoaded, _liveState.EkkoLoaded ? $"{_liveState.PurchaseOrderCount:N0}" : "-"),
@@ -1171,7 +1401,7 @@
private IReadOnlyList<PurchasingSectionDetailRow> OpenOrderDetailRows =>
[
new("Bestellungen seit Jahresbeginn", "Orders since start of year", _liveState.EkkoLoaded ? _liveState.PurchaseOrderCount.ToString("N0") : "-", "EKKOSet.Bedat", _liveState.EkkoLoaded ? "SAP live" : "Wartet auf SAP"),
new("Bestellungen im Zeitraum", "Orders in period", _liveState.EkkoLoaded ? _liveState.PurchaseOrderCount.ToString("N0") : "-", "EKKOSet.Bedat", _liveState.EkkoLoaded ? "SAP live" : "Wartet auf SAP"),
new("Lieferanten mit Bestellung", "Suppliers with order", _liveState.EkkoLoaded ? _liveState.SupplierCount.ToString("N0") : "-", "EKKOSet.Lifnr", _liveState.EkkoLoaded ? "SAP live" : "Wartet auf SAP"),
new("Offener Bestellwert", "Open order value", _liveState.EketLoaded ? FormatChf(_liveState.OpenValueSample) : FormatChf(Purchasing3dBaseRows.Sum(x => x.OpenValue)), "EKPO/EKET", _liveState.EketLoaded ? "SAP live" : "Simulation"),
new("Faelligkeiten", "Due dates", _liveState.EketLoaded ? "SAP live" : "wartet auf EKET", "eketSet.Eindt", _liveState.EketLoaded ? "SAP live" : "Wartet auf SAP")
@@ -1196,7 +1426,7 @@
protected override async Task OnInitializedAsync()
{
Navigation.LocationChanged += HandleLocationChanged;
_liveState = await PurchasingDashboardService.LoadAsync();
_liveState = await PurchasingDashboardService.LoadAsync(BuildCurrentFilter());
_refreshStatus = await PurchasingDataRefreshService.GetStatusAsync();
_liveLoading = false;
}
@@ -1226,6 +1456,77 @@
private string T(string german, string english) => UiText.Text(german, english);
private static string FormatChf(double value) => $"CHF {value:N0}";
private static string FormatChf(decimal value) => $"CHF {value:N0}";
private static string BuildDefaultFromMonth()
{
var today = DateTime.Today;
return $"{today.Year - 2}-01";
}
private static string BuildDefaultToMonth()
=> DateTime.Today.ToString("yyyy-MM", CultureInfo.InvariantCulture);
private PurchasingDashboardFilter BuildCurrentFilter()
{
var from = ParseMonth(_filterFromMonth, new DateTime(DateTime.Today.Year - 2, 1, 1));
var toMonth = ParseMonth(_filterToMonth, DateTime.Today);
var to = new DateTime(toMonth.Year, toMonth.Month, DateTime.DaysInMonth(toMonth.Year, toMonth.Month));
if (to < from)
from = new DateTime(to.Year, 1, 1);
if (to > DateTime.Today)
to = DateTime.Today;
return new PurchasingDashboardFilter(from, to);
}
private static DateTime ParseMonth(string value, DateTime fallback)
=> DateTime.TryParseExact(value, "yyyy-MM", CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsed)
? new DateTime(parsed.Year, parsed.Month, 1)
: fallback;
private void SetFilterFromMonth(ChangeEventArgs args)
=> _filterFromMonth = Convert.ToString(args.Value, CultureInfo.InvariantCulture) ?? _filterFromMonth;
private void SetFilterToMonth(ChangeEventArgs args)
=> _filterToMonth = Convert.ToString(args.Value, CultureInfo.InvariantCulture) ?? _filterToMonth;
private async Task ApplyPurchasingFilterAsync()
{
_liveLoading = true;
_liveState = await PurchasingDashboardService.LoadAsync(BuildCurrentFilter());
_liveLoading = false;
if (CurrentPurchasingPage == "3d")
await RenderPurchasing3dAsync();
}
private async Task SetLastThreeYearsAsync()
{
_filterFromMonth = BuildDefaultFromMonth();
_filterToMonth = BuildDefaultToMonth();
await ApplyPurchasingFilterAsync();
}
private static string FormatChartValue(string label, IReadOnlyList<PurchasingLiveChartPoint> rows, Func<decimal, string> formatter)
=> formatter(rows.FirstOrDefault(row => row.Label.Equals(label, StringComparison.OrdinalIgnoreCase))?.Value ?? 0m);
private string FormatTopShare()
{
if (_liveState.SpendChfSample <= 0)
return "-";
var share = _liveState.SpendConcentrationChartRows.Sum(row => row.Value) / _liveState.SpendChfSample * 100m;
return $"{share:N1}%";
}
private static Color ResolveAnalysisSeverityColor(string severity)
=> severity switch
{
"High" => Color.Error,
"Medium" => Color.Warning,
"Low" => Color.Success,
_ => Color.Info
};
private string TopSpendLabel => _liveState.EkpoLoaded && !string.IsNullOrWhiteSpace(_liveState.TopSupplierLabel) ? _liveState.TopSupplierLabel : BuildTopLabel(x => x.Spend, FormatChf);
private string TopMaterialGroupLabel => _liveState.EkpoLoaded && !string.IsNullOrWhiteSpace(_liveState.TopMaterialGroupLabel) ? _liveState.TopMaterialGroupLabel : BuildTopLabel(x => x.Spend, FormatChf, "Warengruppe");
private string TopArticleLabel => _liveState.EkpoLoaded && !string.IsNullOrWhiteSpace(_liveState.TopArticleLabel) ? _liveState.TopArticleLabel : BuildTopLabel(x => x.Spend, FormatChf, "Artikel");
@@ -1257,7 +1558,7 @@
try
{
_refreshStatus = await PurchasingDataRefreshService.RunFullLoadAsync();
_liveState = await PurchasingDashboardService.LoadAsync();
_liveState = await PurchasingDashboardService.LoadAsync(BuildCurrentFilter());
}
finally
{
@@ -1271,7 +1572,7 @@
try
{
_refreshStatus = await PurchasingDataRefreshService.RunDeltaAsync();
_liveState = await PurchasingDashboardService.LoadAsync();
_liveState = await PurchasingDashboardService.LoadAsync(BuildCurrentFilter());
}
finally
{
@@ -1396,7 +1697,23 @@
}
private IReadOnlyList<object> BuildPurchasing3dRows()
=> Purchasing3dBaseRows
{
var liveRows = ResolvePurchasing3dLiveRows();
if (liveRows.Count > 0)
{
var year = _liveState.PeriodTo?.Year ?? DateTime.Today.Year;
return liveRows
.Select(row => new
{
country = row.Label,
year,
value = (double)row.Value
})
.Cast<object>()
.ToList();
}
return Purchasing3dBaseRows
.Select(row => new
{
country = row.Axis,
@@ -1405,6 +1722,19 @@
})
.Cast<object>()
.ToList();
}
private IReadOnlyList<PurchasingLiveChartPoint> ResolvePurchasing3dLiveRows() => _purchasing3dIndicator switch
{
"spend" => _liveState.SpendChartRows,
"openValue" => _liveState.OpenValueChartRows,
"contractValue" => _liveState.ContractChartRows,
"deliveryRisk" => _liveState.DeliveryRiskChartRows,
"priceVariance" => _liveState.PriceVarianceChartRows,
"spendConcentration" => _liveState.SpendConcentrationChartRows,
"dataQuality" => _liveState.DataQualityChartRows,
_ => []
};
private double ResolvePurchasing3dValue(Purchasing3dBaseRow row) => _purchasing3dIndicator switch
{
@@ -1415,7 +1745,7 @@
_ => row.Spend
};
private bool ScenarioAffectsPurchasingValue => _purchasing3dIndicator is "spend" or "openValue" or "contractValue";
private bool ScenarioAffectsPurchasingValue => _purchasing3dIndicator is "spend" or "openValue" or "contractValue" or "deliveryRisk" or "priceVariance" or "spendConcentration";
private string ResolvePurchasing3dIndicatorLabel()
=> T(
@@ -1427,7 +1757,10 @@
if (!ScenarioAffectsPurchasingValue)
return T("nicht auf diesen Indikator angewendet", "not applied to this indicator");
var baseTotal = Purchasing3dBaseRows.Sum(ResolvePurchasing3dValue);
var liveRows = ResolvePurchasing3dLiveRows();
var baseTotal = liveRows.Count > 0
? (double)liveRows.Sum(row => row.Value)
: Purchasing3dBaseRows.Sum(ResolvePurchasing3dValue);
var delta = baseTotal * _purchasing3dFactor - baseTotal;
return $"{delta:N0} {Purchasing3dIndicators.First(x => x.Key == _purchasing3dIndicator).Unit}";
}