Add purchasing period analytics
This commit is contained in:
@@ -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}";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user