977 lines
49 KiB
Plaintext
977 lines
49 KiB
Plaintext
@page "/einkauf"
|
|
@using System.Globalization
|
|
@using TrafagSalesExporter.Models
|
|
@using TrafagSalesExporter.Services
|
|
@inject TrafagSalesExporter.Services.IUiTextService UiText
|
|
@inject IJSRuntime JsRuntime
|
|
@inject IPurchasingDashboardService PurchasingDashboardService
|
|
|
|
<PageTitle>@T("Einkauf", "Purchasing")</PageTitle>
|
|
|
|
<MudPaper Class="purchasing-hero mb-4" Elevation="0">
|
|
<div class="purchasing-hero-main">
|
|
<MudChip T="string" Size="Size.Small" Color="Color.Info" Variant="Variant.Filled">
|
|
@T("Einkauf Cockpit", "Purchasing cockpit")
|
|
</MudChip>
|
|
<MudText Typo="Typo.h3" Class="purchasing-hero-title">@T("Spend, Lieferanten, offene Verpflichtungen", "Spend, suppliers, open commitments")</MudText>
|
|
<MudText Typo="Typo.body1" Class="purchasing-hero-text">
|
|
@T("Operative Einkaufsanalyse mit Live-EKKO, klarer SAP-Datenpipeline, Simulationen und 3D-What-if-Ansicht.",
|
|
"Operational purchasing analytics with live EKKO, clear SAP data pipeline, simulations and 3D what-if view.")
|
|
</MudText>
|
|
<div class="purchasing-hero-actions">
|
|
<MudChip T="string" Size="Size.Small" Color="@(_liveState.EkkoLoaded ? Color.Success : Color.Warning)" Variant="Variant.Outlined">
|
|
EKKO @(_liveState.EkkoLoaded ? "live" : "pending")
|
|
</MudChip>
|
|
<MudChip T="string" Size="Size.Small" Color="@(_liveState.EkpoLoaded ? Color.Success : Color.Warning)" Variant="Variant.Outlined">
|
|
EKPO @(_liveState.EkpoLoaded ? "live" : "pending")
|
|
</MudChip>
|
|
<MudChip T="string" Size="Size.Small" Color="@(_liveState.EketLoaded ? Color.Success : Color.Warning)" Variant="Variant.Outlined">
|
|
EKET @(_liveState.EketLoaded ? "live" : "pending")
|
|
</MudChip>
|
|
</div>
|
|
</div>
|
|
<div class="purchasing-hero-metrics">
|
|
<div class="purchasing-radar" style="@BuildReadinessDonutStyle()">
|
|
<div>
|
|
<strong>@DataReadinessPercent.ToString("N0")%</strong>
|
|
<span>@T("Live", "Live")</span>
|
|
</div>
|
|
</div>
|
|
<div class="purchasing-hero-note">
|
|
<strong>@DataReadinessText</strong>
|
|
<span>@PurchasingStatusText</span>
|
|
</div>
|
|
</div>
|
|
</MudPaper>
|
|
|
|
<MudGrid Class="mb-4" Spacing="2">
|
|
@foreach (var card in KpiCards)
|
|
{
|
|
<MudItem xs="12" sm="6" lg="3">
|
|
<MudPaper Class="purchasing-kpi pa-3" Outlined="true">
|
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
|
<div class="purchasing-kpi-icon">
|
|
<MudIcon Icon="@card.Icon" Color="@card.Color" Size="Size.Medium" />
|
|
</div>
|
|
<div>
|
|
<MudText Typo="Typo.caption" Class="purchasing-muted">@T(card.TitleDe, card.TitleEn)</MudText>
|
|
<MudText Typo="Typo.h6">@card.Value</MudText>
|
|
<MudText Typo="Typo.caption">@T(card.DetailDe, card.DetailEn)</MudText>
|
|
</div>
|
|
</MudStack>
|
|
</MudPaper>
|
|
</MudItem>
|
|
}
|
|
</MudGrid>
|
|
|
|
<MudTabs Elevation="1" Rounded="false" PanelClass="pt-4">
|
|
<MudTabPanel Text="@T("Uebersicht", "Overview")" Icon="@Icons.Material.Filled.Dashboard">
|
|
<MudGrid Spacing="2">
|
|
<MudItem xs="12" lg="8">
|
|
<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("SAP Datenfluss", "SAP data flow")</MudText>
|
|
<MudText Typo="Typo.body2" Class="purchasing-muted">@T("Vom OData-Service bis zur Kennzahl sichtbar, welcher Baustein echt ist.", "From OData service to KPI, it is visible which block is real.")</MudText>
|
|
</div>
|
|
<MudChip T="string" Color="Color.Info" Variant="Variant.Outlined">@DataReadinessText</MudChip>
|
|
</MudStack>
|
|
<div class="purchasing-pipeline">
|
|
@foreach (var step in PipelineRows)
|
|
{
|
|
<div class="purchasing-pipeline-step @(step.IsReady ? "is-ready" : "is-waiting")">
|
|
<MudIcon Icon="@step.Icon" Color="@step.Color" />
|
|
<div>
|
|
<strong>@T(step.TitleDe, step.TitleEn)</strong>
|
|
<span>@T(step.DetailDe, step.DetailEn)</span>
|
|
</div>
|
|
<MudChip T="string" Size="Size.Small" Color="@step.Color" Variant="Variant.Outlined">@step.Value</MudChip>
|
|
</div>
|
|
}
|
|
</div>
|
|
</MudPaper>
|
|
</MudItem>
|
|
<MudItem xs="12" lg="4">
|
|
<MudPaper Class="pa-3 purchasing-overview-panel" Outlined="true">
|
|
<MudText Typo="Typo.h6" Class="mb-2">@T("Management Insights", "Management insights")</MudText>
|
|
<div class="purchasing-insights">
|
|
@foreach (var insight in ManagementInsights)
|
|
{
|
|
<div class="purchasing-insight">
|
|
<MudIcon Icon="@Icons.Material.Filled.AutoGraph" Color="Color.Info" Size="Size.Small" />
|
|
<span>@insight</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
<div class="purchasing-mini-donut-wrap">
|
|
<div class="purchasing-mini-donut" style="@BuildOverviewDonutStyle()">
|
|
<strong>@DataReadinessPercent.ToString("N0")%</strong>
|
|
</div>
|
|
<div class="purchasing-mini-legend">
|
|
<span><i class="ready"></i>@T("Live", "Live")</span>
|
|
<span><i class="waiting"></i>@T("Wartet", "Waiting")</span>
|
|
<span><i class="simulation"></i>@T("Simulation", "Simulation")</span>
|
|
</div>
|
|
</div>
|
|
</MudPaper>
|
|
</MudItem>
|
|
<MudItem xs="12">
|
|
<MudPaper Class="pa-3 purchasing-overview-panel" Outlined="true">
|
|
<MudText Typo="Typo.h6" Class="mb-2">@T("Analyseachsen", "Analysis axes")</MudText>
|
|
<div class="purchasing-axis-grid">
|
|
@foreach (var axis in AnalysisAxes)
|
|
{
|
|
<div class="purchasing-axis-card">
|
|
<strong>@T(axis.LabelDe, axis.LabelEn)</strong>
|
|
<code>@axis.Field</code>
|
|
<span>@T(axis.UsageDe, axis.UsageEn)</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
</MudPaper>
|
|
</MudItem>
|
|
</MudGrid>
|
|
</MudTabPanel>
|
|
|
|
<MudTabPanel Text="@T("Spend", "Spend")" Icon="@Icons.Material.Filled.Payments">
|
|
<PurchasingSection TitleDe="Spend total vergangen"
|
|
TitleEn="Historic total spend"
|
|
DescriptionDe="Beschaffungsvolumen in CHF nach Jahr, Lieferant, Warengruppe und Artikel. Spend-Werte brauchen EKPO; bis SAP Positionen liefert, ist die Ansicht als Simulation markiert."
|
|
DescriptionEn="Purchasing volume in CHF by year, supplier, material group and article. Spend values need EKPO; until SAP provides item rows, this view is marked as simulation."
|
|
ChartTitleDe="Spend-Verlauf nach Einkaufsdimension"
|
|
ChartTitleEn="Spend trend by purchasing dimension"
|
|
Kpis="@SpendKpis"
|
|
ChartRows="@SpendChartRows"
|
|
StatusRows="@SpendStatusRows"
|
|
DetailRows="@SpendDetailRows" />
|
|
</MudTabPanel>
|
|
|
|
<MudTabPanel Text="@T("Offene Bestellungen", "Open orders")" Icon="@Icons.Material.Filled.PendingActions">
|
|
<PurchasingSection TitleDe="Offene Bestellwerte und Mengen"
|
|
TitleEn="Open order values and quantities"
|
|
DescriptionDe="Live-Bestellkoepfe aus EKKO sind angebunden. Offene Werte und Mengen brauchen zusaetzlich EKPO/EKET."
|
|
DescriptionEn="Live purchase-order headers from EKKO are connected. Open values and quantities additionally need EKPO/EKET."
|
|
ChartTitleDe="Bestellaktivitaet und offene Positionen"
|
|
ChartTitleEn="Order activity and open items"
|
|
Kpis="@OpenOrderKpis"
|
|
ChartRows="@OpenOrderChartRows"
|
|
StatusRows="@OpenOrderStatusRows"
|
|
DetailRows="@OpenOrderDetailRows" />
|
|
</MudTabPanel>
|
|
|
|
<MudTabPanel Text="@T("Kontrakte", "Contracts")" Icon="@Icons.Material.Filled.Assignment">
|
|
<PurchasingSection TitleDe="Offene Verpflichtungen"
|
|
TitleEn="Open commitments"
|
|
DescriptionDe="Kontrakte und Restverpflichtungen werden auf EKPO/EKET aufgebaut. Der Reiter zeigt bereits die Zielkennzahlen und den aktuellen Ladezustand."
|
|
DescriptionEn="Contracts and remaining commitments are built on EKPO/EKET. This tab already shows the target KPIs and current load status."
|
|
ChartTitleDe="Kontrakt- und Verpflichtungsuebersicht"
|
|
ChartTitleEn="Contract and commitment overview"
|
|
Kpis="@ContractKpis"
|
|
ChartRows="@ContractChartRows"
|
|
StatusRows="@ContractStatusRows"
|
|
DetailRows="@ContractDetailRows" />
|
|
</MudTabPanel>
|
|
|
|
<MudTabPanel Text="@T("Lieferanten", "Suppliers")" Icon="@Icons.Material.Filled.Verified">
|
|
<PurchasingSection TitleDe="Lieferantenbewertung und Performance"
|
|
TitleEn="Supplier rating and performance"
|
|
DescriptionDe="Lieferantenbasis kommt live aus EKKO. Bewertung, Termintreue und Preisentwicklung brauchen spaeter EKPO/EKET und Reklamationsdaten."
|
|
DescriptionEn="The supplier base comes live from EKKO. Rating, delivery reliability and price development later need EKPO/EKET and claim data."
|
|
ChartTitleDe="Lieferantenbasis und Performance-Indikatoren"
|
|
ChartTitleEn="Supplier base and performance indicators"
|
|
Kpis="@SupplierKpis"
|
|
ChartRows="@SupplierChartRows"
|
|
StatusRows="@SupplierStatusRows"
|
|
DetailRows="@SupplierDetailRows" />
|
|
</MudTabPanel>
|
|
|
|
<MudTabPanel Text="@T("PBIX Vorlage", "PBIX template")" Icon="@Icons.Material.Filled.InsertChart">
|
|
<MudPaper Class="pa-3" Outlined="true">
|
|
<MudText Typo="Typo.h6" Class="mb-2">@T("Aus x.pbix uebernommene Seiten", "Pages derived from x.pbix")</MudText>
|
|
<MudTable Items="@PowerBiPages" Dense="true" Hover="true">
|
|
<HeaderContent>
|
|
<MudTh>@T("Power-BI-Seite", "Power BI page")</MudTh>
|
|
<MudTh>@T("Visuals", "Visuals")</MudTh>
|
|
<MudTh>@T("Kennzahl", "Measure")</MudTh>
|
|
<MudTh>@T("Dimensionen", "Dimensions")</MudTh>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd>@context.Page</MudTd>
|
|
<MudTd>@context.Visuals</MudTd>
|
|
<MudTd><code>@context.Measure</code></MudTd>
|
|
<MudTd>@context.Dimensions</MudTd>
|
|
</RowTemplate>
|
|
</MudTable>
|
|
</MudPaper>
|
|
</MudTabPanel>
|
|
|
|
<MudTabPanel Text="@T("3D Simulation", "3D simulation")" Icon="@Icons.Material.Filled.ViewInAr">
|
|
<MudPaper Class="pa-3 mb-3" Outlined="true">
|
|
<MudGrid Spacing="2">
|
|
<MudItem xs="12" md="3">
|
|
<MudSelect T="string" Value="_purchasing3dIndicator" ValueChanged="SetPurchasing3dIndicator" Label="@T("Indikator", "Indicator")" Dense="true">
|
|
@foreach (var option in Purchasing3dIndicators)
|
|
{
|
|
<MudSelectItem Value="@option.Key">@T(option.TitleDe, option.TitleEn)</MudSelectItem>
|
|
}
|
|
</MudSelect>
|
|
</MudItem>
|
|
<MudItem xs="12" md="2">
|
|
<MudSelect T="string" Value="_purchasing3dChartType" ValueChanged="SetPurchasing3dChartType" Label="@T("Grafik", "Chart")" Dense="true">
|
|
<MudSelectItem Value="@("bar")">@T("Balken", "Bars")</MudSelectItem>
|
|
<MudSelectItem Value="@("line")">@T("Linie", "Line")</MudSelectItem>
|
|
<MudSelectItem Value="@("surface")">@T("Flaeche", "Surface")</MudSelectItem>
|
|
<MudSelectItem Value="@("pie")">@T("Kreis", "Pie")</MudSelectItem>
|
|
</MudSelect>
|
|
</MudItem>
|
|
<MudItem xs="12" md="3">
|
|
<MudText Typo="Typo.caption">@T("Preis-/Wechselkurs-Szenario", "Price/exchange-rate scenario")</MudText>
|
|
<div class="finance-3d-range-row">
|
|
<MudButton Variant="Variant.Outlined" Size="Size.Small" OnClick="@(() => SetPurchasing3dFactorPreset(0.9d))">-10%</MudButton>
|
|
<input class="finance-3d-range"
|
|
type="range"
|
|
min="0.5"
|
|
max="1.5"
|
|
step="0.01"
|
|
value="@_purchasing3dFactor.ToString("0.00", CultureInfo.InvariantCulture)"
|
|
@oninput="SetPurchasing3dFactor" />
|
|
<MudText Typo="Typo.body2" Class="finance-3d-factor">@_purchasing3dFactor.ToString("0.00", CultureInfo.InvariantCulture)x</MudText>
|
|
</div>
|
|
<MudText Typo="Typo.caption">
|
|
@T("Delta", "Delta"): @FormatScenarioDelta()
|
|
</MudText>
|
|
</MudItem>
|
|
<MudItem xs="12" md="2">
|
|
<MudText Typo="Typo.caption">@T("Beschriftung", "Labels")</MudText>
|
|
<div class="finance-3d-range-row">
|
|
<input class="finance-3d-range"
|
|
type="range"
|
|
min="0.8"
|
|
max="2.5"
|
|
step="0.1"
|
|
value="@_purchasing3dLabelScale.ToString("0.0", CultureInfo.InvariantCulture)"
|
|
@oninput="SetPurchasing3dLabelScale" />
|
|
<MudText Typo="Typo.body2" Class="finance-3d-factor">@_purchasing3dLabelScale.ToString("0.0", CultureInfo.InvariantCulture)x</MudText>
|
|
</div>
|
|
</MudItem>
|
|
<MudItem xs="12" md="2">
|
|
<MudButton Variant="Variant.Outlined" Color="Color.Info" StartIcon="@Icons.Material.Filled.Refresh" OnClick="RenderPurchasing3dAsync">
|
|
@T("Neu zeichnen", "Redraw")
|
|
</MudButton>
|
|
</MudItem>
|
|
</MudGrid>
|
|
</MudPaper>
|
|
<MudPaper Class="pa-0 finance-3d-surface purchasing-3d-surface" Elevation="1">
|
|
<canvas @ref="_purchasing3dCanvas" class="finance-3d-canvas" style="display:block;width:100%;height:100%;touch-action:none;"></canvas>
|
|
</MudPaper>
|
|
</MudTabPanel>
|
|
</MudTabs>
|
|
|
|
@code {
|
|
private ElementReference _purchasing3dCanvas;
|
|
private PurchasingDashboardLiveState _liveState = new();
|
|
private bool _liveLoading = true;
|
|
private string _purchasing3dIndicator = "spend";
|
|
private string _purchasing3dChartType = "bar";
|
|
private double _purchasing3dFactor = 1d;
|
|
private double _purchasing3dLabelScale = 1.5d;
|
|
|
|
private IReadOnlyList<PurchasingKpiCard> KpiCards =>
|
|
[
|
|
new("Spend total", "Total spend", _liveState.EkpoLoaded ? "EKPO live" : T("wartet auf EKPO", "waiting for EKPO"), _liveState.EkpoLoaded ? "Positionsdaten verfuegbar" : "EKKO live, Positionswerte fehlen noch", _liveState.EkpoLoaded ? "Position data available" : "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("Kontrakte", "Contracts", _liveState.EketLoaded ? _liveState.ScheduleSampleCount.ToString("N0") : T("wartet auf EKET", "waiting for EKET"), _liveState.EketLoaded ? "Termin-/Einteilungsprobe verfuegbar" : "EKKO live, Terminwerte fehlen noch", _liveState.EketLoaded ? "Schedule sample available" : "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)
|
|
];
|
|
|
|
private readonly List<PurchasingAxis> AnalysisAxes =
|
|
[
|
|
new("Jahr", "Year", "EKKOSet.Bedat", "Zeitfilter und Verlauf", "Time filter and trend"),
|
|
new("Lieferant", "Supplier", "Data.Name / Data.Lieferant", "Spend und Performance pro Lieferant", "Spend and performance by supplier"),
|
|
new("Warengruppe", "Material group", "Data (2).Warengruppe / WG komplett", "Spend nach Warengruppe", "Spend by material group"),
|
|
new("Artikel", "Article", "EKPOSet.Matnr / EKPOSet.Txz01", "Artikel- und Preisentwicklung", "Article and price development"),
|
|
new("Region", "Region", "Data.Laender-/Regionenschluessel", "Regionale Spend-Verteilung", "Regional spend distribution")
|
|
];
|
|
|
|
private readonly List<PurchasingSource> SapSources =
|
|
[
|
|
new("EKKOSet", "Bestellkopf: Datum, Lieferant, Einkaufsbeleg."),
|
|
new("EKPOSet", "Bestellposition: Artikel, Text, Netwr CHF, Preis pro Stueck."),
|
|
new("eketSet", "Einteilungen/Termine: Basis fuer offene Mengen und Liefertermine."),
|
|
new("Data", "Lieferanten-Mapping und Lieferantennamen."),
|
|
new("Data (2)", "Warengruppen-Mapping und Warengruppentexte.")
|
|
];
|
|
|
|
private int DataReadinessPercent
|
|
{
|
|
get
|
|
{
|
|
var ready = 0;
|
|
if (_liveState.EkkoLoaded)
|
|
ready++;
|
|
if (_liveState.EkpoLoaded)
|
|
ready++;
|
|
if (_liveState.EketLoaded)
|
|
ready++;
|
|
return (int)Math.Round(ready / 3d * 100d);
|
|
}
|
|
}
|
|
|
|
private string DataReadinessText
|
|
=> DataReadinessPercent switch
|
|
{
|
|
>= 100 => T("voll live", "fully live"),
|
|
>= 67 => T("mehrheitlich live", "mostly live"),
|
|
>= 34 => T("teilweise live", "partly live"),
|
|
_ => T("Verbindung wird geprueft", "connection being checked")
|
|
};
|
|
|
|
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("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)
|
|
];
|
|
|
|
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("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.")
|
|
: T("Spend ist bewusst als Simulation markiert, bis EKPO Zeilen liefert.", "Spend is deliberately marked as simulation until EKPO returns rows."),
|
|
_liveState.EketLoaded
|
|
? T("Faelligkeiten und Kontrakte koennen aus EKET berechnet werden.", "Due dates and contracts can be calculated from EKET.")
|
|
: T("Faelligkeiten und Restverpflichtungen warten noch auf EKET.", "Due dates and remaining commitments are still waiting for EKET."),
|
|
T("3D What-if zeigt sofort die Auswirkung von Preis- und Wechselkursannahmen.", "3D what-if immediately shows the effect of price and exchange-rate assumptions.")
|
|
];
|
|
|
|
private IReadOnlyList<PurchasingSectionKpi> SpendKpis =>
|
|
[
|
|
new("Spend CHF", "Spend CHF", _liveState.EkpoLoaded ? "EKPO live" : FormatChf(Purchasing3dBaseRows.Sum(x => x.Spend)), _liveState.EkpoLoaded ? "aus SAP Positionen" : "Simulation bis EKPO liefert", _liveState.EkpoLoaded ? "from SAP item rows" : "simulation until EKPO delivers"),
|
|
new("Jahre", "Years", "2024-2026", "aus PBIX-Struktur", "from PBIX structure"),
|
|
new("Dimensionen", "Dimensions", "4", "Jahr, Lieferant, Warengruppe, Artikel", "year, supplier, material group, article"),
|
|
new("SAP Status", "SAP status", _liveState.EkpoLoaded ? "live" : "wartet", "EKPOSet fuer Spend notwendig", "EKPOSet required for spend")
|
|
];
|
|
|
|
private IReadOnlyList<PurchasingSectionKpi> OpenOrderKpis =>
|
|
[
|
|
new("Bestellungen", "Orders", _liveState.EkkoLoaded ? _liveState.PurchaseOrderCount.ToString("N0") : "-", "EKKO live seit Jahresbeginn", "EKKO live since start of year"),
|
|
new("Lieferanten", "Suppliers", _liveState.EkkoLoaded ? _liveState.SupplierCount.ToString("N0") : "-", "aus EKKO-Liveprobe", "from EKKO live sample"),
|
|
new("Offener Wert", "Open value", _liveState.EkpoLoaded ? "EKPO live" : FormatChf(Purchasing3dBaseRows.Sum(x => x.OpenValue)), _liveState.EkpoLoaded ? "aus SAP Positionen" : "Simulation bis EKPO liefert", _liveState.EkpoLoaded ? "from SAP item rows" : "simulation until EKPO delivers"),
|
|
new("Offene Menge", "Open quantity", _liveState.EkpoLoaded ? "EKPO live" : Purchasing3dBaseRows.Sum(x => x.OpenQuantity).ToString("N0"), _liveState.EkpoLoaded ? "aus SAP Positionen" : "Simulation bis EKPO liefert", _liveState.EkpoLoaded ? "from SAP item rows" : "simulation until EKPO delivers")
|
|
];
|
|
|
|
private IReadOnlyList<PurchasingSectionKpi> ContractKpis =>
|
|
[
|
|
new("Restwert", "Remaining value", _liveState.EketLoaded ? "EKET live" : FormatChf(Purchasing3dBaseRows.Sum(x => x.ContractValue)), _liveState.EketLoaded ? "aus SAP Einteilungen" : "Simulation bis EKET liefert", _liveState.EketLoaded ? "from SAP schedules" : "simulation until EKET delivers"),
|
|
new("Einteilungen", "Schedules", _liveState.EketLoaded ? _liveState.ScheduleSampleCount.ToString("N0") : "-", "EKET-Probe", "EKET sample"),
|
|
new("Abrufquote", "Consumption", "offen", "braucht Kontrakt- und Abrufdaten", "needs contract and call-off data"),
|
|
new("Faelligkeit", "Due date", _liveState.LatestOrderDate?.ToString("yyyy-MM-dd") ?? "-", "letztes bekanntes EKKO-Datum", "latest known EKKO date")
|
|
];
|
|
|
|
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("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 ? "EKPO live" : "wartet", "Netwr CHF/Stk braucht EKPO", "Netwr CHF/unit needs EKPO"),
|
|
new("Qualitaet", "Quality", "offen", "Reklamationsquelle noch nicht angebunden", "claim source not connected yet")
|
|
];
|
|
|
|
private readonly List<PowerBiPageInfo> PowerBiPages =
|
|
[
|
|
new("Besch.Volumen CHF/Lieferant", "Pivot, Slicer", "Sum(EKPOSet.Netwr CHF)", "Jahr, Lieferant, Warengruppe, Artikel"),
|
|
new("Eink.Vol. CHF / Lieferant Kuchen", "Pie, Slicer", "Sum(EKPOSet.Netwr CHF)", "Lieferant, Warengruppe, Jahr"),
|
|
new("Balken Vol./Lief/WG", "Column, Slicer", "Sum(EKPOSet.Netwr CHF)", "Lieferant, Warengruppe, Artikel"),
|
|
new("Diagramm Vol./WG", "Column, Slicer", "Sum(EKPOSet.Netwr CHF)", "Warengruppe, Jahr"),
|
|
new("Eink.Vol. CHF / Region", "Pie, Slicer", "Sum(EKPOSet.Netwr CHF)", "Region, Warengruppe, Jahr"),
|
|
new("Preisentwicklung CHF", "Pivot, Slicer", "Min(EKPOSet.Netwr CHF/Stk)", "Lieferant, Artikel, Jahr"),
|
|
new("Matrix Vol./WG", "Pivot, Slicer", "Sum(EKPOSet.Netwr CHF)", "Warengruppe, Lieferant, Artikel")
|
|
];
|
|
|
|
private readonly List<Purchasing3dIndicator> Purchasing3dIndicators =
|
|
[
|
|
new("spend", "Spend CHF", "Spend CHF", "CHF"),
|
|
new("openValue", "Offener Bestellwert", "Open order value", "CHF"),
|
|
new("openQuantity", "Offene Menge", "Open quantity", "Qty"),
|
|
new("contractValue", "Kontrakt-Restwert", "Contract remaining value", "CHF"),
|
|
new("supplierScore", "Lieferantenperformance", "Supplier performance", "%")
|
|
];
|
|
|
|
private readonly List<Purchasing3dBaseRow> Purchasing3dBaseRows =
|
|
[
|
|
new("Lieferant A", 2024, 1450000d, 260000d, 11800d, 420000d, 91d),
|
|
new("Lieferant A", 2025, 1680000d, 310000d, 13200d, 460000d, 93d),
|
|
new("Lieferant A", 2026, 1820000d, 335000d, 14100d, 490000d, 92d),
|
|
new("Lieferant B", 2024, 980000d, 190000d, 9300d, 260000d, 86d),
|
|
new("Lieferant B", 2025, 1120000d, 225000d, 10100d, 315000d, 88d),
|
|
new("Lieferant B", 2026, 1240000d, 250000d, 10800d, 350000d, 89d),
|
|
new("Warengruppe Sensorik", 2024, 760000d, 120000d, 6400d, 210000d, 94d),
|
|
new("Warengruppe Sensorik", 2025, 890000d, 145000d, 7100d, 230000d, 95d),
|
|
new("Warengruppe Sensorik", 2026, 940000d, 155000d, 7400d, 245000d, 94d),
|
|
new("Artikel Top 10", 2024, 520000d, 83000d, 2800d, 160000d, 90d),
|
|
new("Artikel Top 10", 2025, 610000d, 97000d, 3100d, 180000d, 91d),
|
|
new("Artikel Top 10", 2026, 680000d, 105000d, 3350d, 195000d, 92d)
|
|
];
|
|
|
|
private IReadOnlyList<PurchasingSectionChartRow> SpendChartRows
|
|
=> BuildPurchasingChartRows(x => x.Spend, FormatChf);
|
|
|
|
private IReadOnlyList<PurchasingSectionChartRow> OpenOrderChartRows
|
|
=> _liveState.EkkoLoaded
|
|
? BuildOpenOrderLiveChartRows()
|
|
: BuildPurchasingChartRows(x => x.OpenValue, FormatChf);
|
|
|
|
private IReadOnlyList<PurchasingSectionChartRow> ContractChartRows
|
|
=> BuildPurchasingChartRows(x => x.ContractValue, FormatChf);
|
|
|
|
private IReadOnlyList<PurchasingSectionChartRow> SupplierChartRows
|
|
=> BuildPurchasingChartRows(x => x.SupplierScore, value => $"{value:N1}%");
|
|
|
|
private IReadOnlyList<PurchasingSectionStatusRow> SpendStatusRows =>
|
|
[
|
|
BuildStatus("EKKO Bestellkoepfe", "EKKO purchase headers", _liveState.EkkoLoaded, _liveState.EkkoLoaded ? $"{_liveState.PurchaseOrderCount:N0}" : "-"),
|
|
BuildStatus("EKPO Positionen", "EKPO item rows", _liveState.EkpoLoaded, _liveState.EkpoLoaded ? $"{_liveState.PositionSampleCount:N0}" : "0"),
|
|
BuildStatus("Spend CHF", "Spend CHF", _liveState.EkpoLoaded, _liveState.EkpoLoaded ? "live" : "Simulation")
|
|
];
|
|
|
|
private IReadOnlyList<PurchasingSectionStatusRow> OpenOrderStatusRows =>
|
|
[
|
|
BuildStatus("Bestellkoepfe", "Purchase headers", _liveState.EkkoLoaded, _liveState.EkkoLoaded ? "live" : "-"),
|
|
BuildStatus("Offene Werte", "Open values", _liveState.EkpoLoaded, _liveState.EkpoLoaded ? "live" : "wartet auf EKPO"),
|
|
BuildStatus("Faelligkeiten", "Due dates", _liveState.EketLoaded, _liveState.EketLoaded ? "live" : "wartet auf EKET")
|
|
];
|
|
|
|
private IReadOnlyList<PurchasingSectionStatusRow> ContractStatusRows =>
|
|
[
|
|
BuildStatus("Kontraktkopf/-position", "Contract header/item", _liveState.EkpoLoaded, _liveState.EkpoLoaded ? "live" : "wartet auf EKPO"),
|
|
BuildStatus("Einteilungen", "Schedules", _liveState.EketLoaded, _liveState.EketLoaded ? $"{_liveState.ScheduleSampleCount:N0}" : "0"),
|
|
BuildStatus("Restverpflichtung", "Remaining commitment", _liveState.EkpoLoaded && _liveState.EketLoaded, _liveState.EkpoLoaded && _liveState.EketLoaded ? "live" : "Simulation")
|
|
];
|
|
|
|
private IReadOnlyList<PurchasingSectionStatusRow> SupplierStatusRows =>
|
|
[
|
|
BuildStatus("Lieferantenbasis", "Supplier base", _liveState.EkkoLoaded, _liveState.EkkoLoaded ? $"{_liveState.SupplierCount:N0}" : "-"),
|
|
BuildStatus("Preisentwicklung", "Price trend", _liveState.EkpoLoaded, _liveState.EkpoLoaded ? "live" : "wartet auf EKPO"),
|
|
BuildStatus("Termintreue", "Delivery reliability", _liveState.EketLoaded, _liveState.EketLoaded ? "live" : "wartet auf EKET")
|
|
];
|
|
|
|
private IReadOnlyList<PurchasingSectionDetailRow> SpendDetailRows =>
|
|
[
|
|
new("Spend nach Jahr", "Spend by year", _liveState.EkpoLoaded ? "SAP live" : FormatChf(Purchasing3dBaseRows.Sum(x => x.Spend)), "EKKOSet.Bedat Jahr", _liveState.EkpoLoaded ? "SAP live" : "Simulation"),
|
|
new("Spend nach Lieferant", "Spend by supplier", TopSpendLabel, "Data.Name / EKKOSet.Lifnr", _liveState.EkpoLoaded ? "SAP live" : "Simulation"),
|
|
new("Spend nach Warengruppe", "Spend by material group", TopMaterialGroupLabel, "Data (2).Warengruppe", _liveState.EkpoLoaded ? "SAP live" : "Simulation"),
|
|
new("Spend nach Artikel", "Spend by article", TopArticleLabel, "EKPOSet.Matnr / Txz01", _liveState.EkpoLoaded ? "SAP live" : "Wartet auf SAP")
|
|
];
|
|
|
|
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("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.EkpoLoaded ? "SAP live" : FormatChf(Purchasing3dBaseRows.Sum(x => x.OpenValue)), "EKPOSet.Netwr CHF", _liveState.EkpoLoaded ? "SAP live" : "Simulation"),
|
|
new("Faelligkeiten", "Due dates", _liveState.EketLoaded ? "SAP live" : "wartet auf EKET", "eketSet.Eindt", _liveState.EketLoaded ? "SAP live" : "Wartet auf SAP")
|
|
];
|
|
|
|
private IReadOnlyList<PurchasingSectionDetailRow> ContractDetailRows =>
|
|
[
|
|
new("Restverpflichtung", "Remaining commitment", _liveState.EketLoaded ? "SAP live" : FormatChf(Purchasing3dBaseRows.Sum(x => x.ContractValue)), "EKPO/EKET", _liveState.EketLoaded ? "SAP live" : "Simulation"),
|
|
new("Mengenkontrakte", "Quantity contracts", _liveState.EkpoLoaded ? "SAP live" : "wartet auf EKPO", "EKPOSet.Menge", _liveState.EkpoLoaded ? "SAP live" : "Wartet auf SAP"),
|
|
new("Abrufquote", "Consumption rate", "offen", "Kontraktmenge / Abrufmenge", "Wartet auf SAP"),
|
|
new("Faellige Verpflichtungen", "Due commitments", _liveState.EketLoaded ? "SAP live" : "wartet auf EKET", "eketSet", _liveState.EketLoaded ? "SAP live" : "Wartet auf SAP")
|
|
];
|
|
|
|
private IReadOnlyList<PurchasingSectionDetailRow> SupplierDetailRows =>
|
|
[
|
|
new("Aktive Lieferanten", "Active suppliers", _liveState.EkkoLoaded ? _liveState.SupplierCount.ToString("N0") : "-", "EKKOSet.Lifnr", _liveState.EkkoLoaded ? "SAP live" : "Wartet auf SAP"),
|
|
new("Top Spend Lieferant", "Top spend supplier", TopSpendLabel, "Lieferant / Spend CHF", _liveState.EkpoLoaded ? "SAP live" : "Simulation"),
|
|
new("Preisentwicklung", "Price trend", _liveState.EkpoLoaded ? "SAP live" : "wartet auf EKPO", "Netwr CHF/Stk", _liveState.EkpoLoaded ? "SAP live" : "Wartet auf SAP"),
|
|
new("Liefertermintreue", "Delivery reliability", _liveState.EketLoaded ? "SAP live" : "wartet auf EKET", "Termin / Rueckstand", _liveState.EketLoaded ? "SAP live" : "Wartet auf SAP")
|
|
];
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
_liveState = await PurchasingDashboardService.LoadAsync();
|
|
_liveLoading = false;
|
|
}
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender)
|
|
await RenderPurchasing3dAsync();
|
|
}
|
|
|
|
private string T(string german, string english) => UiText.Text(german, english);
|
|
private static string FormatChf(double value) => $"CHF {value:N0}";
|
|
private string TopSpendLabel => BuildTopLabel(x => x.Spend, FormatChf);
|
|
private string TopMaterialGroupLabel => BuildTopLabel(x => x.Spend, FormatChf, "Warengruppe");
|
|
private string TopArticleLabel => BuildTopLabel(x => x.Spend, FormatChf, "Artikel");
|
|
private string PurchasingStatusText
|
|
=> _liveLoading
|
|
? T("SAP-Einkaufsdaten werden geladen...", "Loading SAP purchasing data...")
|
|
: $"{_liveState.Message} {FormatLatestOrderDate()}";
|
|
|
|
private string FormatLatestOrderDate()
|
|
=> _liveState.LatestOrderDate.HasValue
|
|
? $"{T("Letztes EKKO-Datum", "Latest EKKO date")}: {_liveState.LatestOrderDate.Value:yyyy-MM-dd}."
|
|
: string.Empty;
|
|
|
|
private string BuildReadinessDonutStyle()
|
|
{
|
|
var live = DataReadinessPercent;
|
|
return $"background:conic-gradient(#2e7d32 0 {live.ToString("0", CultureInfo.InvariantCulture)}%, rgba(255,255,255,.2) {live.ToString("0", CultureInfo.InvariantCulture)}% 100%)";
|
|
}
|
|
|
|
private string BuildOverviewDonutStyle()
|
|
{
|
|
var live = DataReadinessPercent;
|
|
var simulationStart = Math.Min(100, live + 24);
|
|
return $"background:conic-gradient(#2e7d32 0 {live.ToString("0", CultureInfo.InvariantCulture)}%, #f9a825 {live.ToString("0", CultureInfo.InvariantCulture)}% {simulationStart.ToString("0", CultureInfo.InvariantCulture)}%, #cfd8dc {simulationStart.ToString("0", CultureInfo.InvariantCulture)}% 100%)";
|
|
}
|
|
|
|
private IReadOnlyList<PurchasingSectionChartRow> BuildPurchasingChartRows(Func<Purchasing3dBaseRow, double> selector, Func<double, string> formatter)
|
|
{
|
|
var rows = Purchasing3dBaseRows
|
|
.Where(row => row.Year == 2026)
|
|
.Select((row, index) => new { row.Axis, Value = selector(row), Color = PurchasingPalette[index % PurchasingPalette.Length] })
|
|
.OrderByDescending(row => row.Value)
|
|
.Take(6)
|
|
.ToList();
|
|
var max = rows.Count == 0 ? 0d : rows.Max(row => row.Value);
|
|
return rows
|
|
.Select(row => new PurchasingSectionChartRow(row.Axis, formatter(row.Value), max <= 0 ? 0 : row.Value / max * 100d, row.Color))
|
|
.ToList();
|
|
}
|
|
|
|
private IReadOnlyList<PurchasingSectionChartRow> BuildOpenOrderLiveChartRows()
|
|
{
|
|
var simulatedRows = BuildPurchasingChartRows(x => x.OpenValue, FormatChf).ToList();
|
|
simulatedRows.Insert(0, new PurchasingSectionChartRow(T("EKKO Bestellungen live", "EKKO orders live"), _liveState.PurchaseOrderCount.ToString("N0"), 100d, "#2e7d32"));
|
|
simulatedRows.Insert(1, new PurchasingSectionChartRow(T("Lieferanten live", "Suppliers live"), _liveState.SupplierCount.ToString("N0"), _liveState.PurchaseOrderCount <= 0 ? 0 : Math.Min(100d, _liveState.SupplierCount / (double)_liveState.PurchaseOrderCount * 100d), "#1565c0"));
|
|
return simulatedRows.Take(6).ToList();
|
|
}
|
|
|
|
private PurchasingSectionStatusRow BuildStatus(string labelDe, string labelEn, bool ok, string value)
|
|
=> new(labelDe, labelEn, value, ok ? Icons.Material.Filled.CheckCircle : Icons.Material.Filled.Pending, ok ? Color.Success : Color.Warning);
|
|
|
|
private string BuildTopLabel(Func<Purchasing3dBaseRow, double> selector, Func<double, string> formatter, string? requiredPrefix = null)
|
|
{
|
|
var query = Purchasing3dBaseRows.Where(row => row.Year == 2026);
|
|
if (!string.IsNullOrWhiteSpace(requiredPrefix))
|
|
query = query.Where(row => row.Axis.StartsWith(requiredPrefix, StringComparison.OrdinalIgnoreCase));
|
|
|
|
var row = query.OrderByDescending(selector).FirstOrDefault();
|
|
return row is null ? "-" : $"{row.Axis}: {formatter(selector(row))}";
|
|
}
|
|
|
|
private static readonly string[] PurchasingPalette = ["#1565c0", "#2e7d32", "#ef6c00", "#6a1b9a", "#00838f", "#ad1457"];
|
|
|
|
private async Task SetPurchasing3dIndicator(string value)
|
|
{
|
|
_purchasing3dIndicator = value;
|
|
await RenderPurchasing3dAsync();
|
|
}
|
|
|
|
private async Task SetPurchasing3dChartType(string value)
|
|
{
|
|
_purchasing3dChartType = value;
|
|
await RenderPurchasing3dAsync();
|
|
}
|
|
|
|
private async Task SetPurchasing3dFactor(ChangeEventArgs args)
|
|
{
|
|
if (double.TryParse(Convert.ToString(args.Value, CultureInfo.InvariantCulture), NumberStyles.Number, CultureInfo.InvariantCulture, out var value))
|
|
{
|
|
_purchasing3dFactor = Math.Clamp(value, 0.5d, 1.5d);
|
|
await JsRuntime.InvokeVoidAsync("trafagFinance3d.updateFactor", _purchasing3dCanvas, ScenarioAffectsPurchasingValue ? _purchasing3dFactor : 1d);
|
|
}
|
|
}
|
|
|
|
private async Task SetPurchasing3dFactorPreset(double value)
|
|
{
|
|
_purchasing3dFactor = Math.Clamp(value, 0.5d, 1.5d);
|
|
await JsRuntime.InvokeVoidAsync("trafagFinance3d.updateFactor", _purchasing3dCanvas, ScenarioAffectsPurchasingValue ? _purchasing3dFactor : 1d);
|
|
}
|
|
|
|
private async Task SetPurchasing3dLabelScale(ChangeEventArgs args)
|
|
{
|
|
if (double.TryParse(Convert.ToString(args.Value, CultureInfo.InvariantCulture), NumberStyles.Number, CultureInfo.InvariantCulture, out var value))
|
|
{
|
|
_purchasing3dLabelScale = Math.Clamp(value, 0.8d, 2.5d);
|
|
await RenderPurchasing3dAsync();
|
|
}
|
|
}
|
|
|
|
private async Task RenderPurchasing3dAsync()
|
|
{
|
|
await JsRuntime.InvokeVoidAsync("trafagFinance3d.render", _purchasing3dCanvas, BuildPurchasing3dRows(), new
|
|
{
|
|
indicator = _purchasing3dIndicator,
|
|
title = ResolvePurchasing3dIndicatorLabel(),
|
|
chartType = _purchasing3dChartType,
|
|
xAxis = T("X: Lieferant / Warengruppe / Artikel", "X: supplier / material group / article"),
|
|
yAxis = T("Y: Wert / Menge / Score", "Y: value / quantity / score"),
|
|
zAxis = T("Z: Jahr / Zeit", "Z: year / time"),
|
|
pieAxis = T("Kreis: Anteil am aktuellen Indikator", "Pie: share of current indicator"),
|
|
labelScale = _purchasing3dLabelScale,
|
|
scenarioFactor = ScenarioAffectsPurchasingValue ? _purchasing3dFactor : 1d
|
|
});
|
|
}
|
|
|
|
private IReadOnlyList<object> BuildPurchasing3dRows()
|
|
=> Purchasing3dBaseRows
|
|
.Select(row => new
|
|
{
|
|
country = row.Axis,
|
|
year = row.Year,
|
|
value = ResolvePurchasing3dValue(row)
|
|
})
|
|
.Cast<object>()
|
|
.ToList();
|
|
|
|
private double ResolvePurchasing3dValue(Purchasing3dBaseRow row) => _purchasing3dIndicator switch
|
|
{
|
|
"openValue" => row.OpenValue,
|
|
"openQuantity" => row.OpenQuantity,
|
|
"contractValue" => row.ContractValue,
|
|
"supplierScore" => row.SupplierScore,
|
|
_ => row.Spend
|
|
};
|
|
|
|
private bool ScenarioAffectsPurchasingValue => _purchasing3dIndicator is "spend" or "openValue" or "contractValue";
|
|
|
|
private string ResolvePurchasing3dIndicatorLabel()
|
|
=> T(
|
|
Purchasing3dIndicators.FirstOrDefault(x => x.Key == _purchasing3dIndicator)?.TitleDe ?? "Spend CHF",
|
|
Purchasing3dIndicators.FirstOrDefault(x => x.Key == _purchasing3dIndicator)?.TitleEn ?? "Spend CHF");
|
|
|
|
private string FormatScenarioDelta()
|
|
{
|
|
if (!ScenarioAffectsPurchasingValue)
|
|
return T("nicht auf diesen Indikator angewendet", "not applied to this indicator");
|
|
|
|
var baseTotal = Purchasing3dBaseRows.Sum(ResolvePurchasing3dValue);
|
|
var delta = baseTotal * _purchasing3dFactor - baseTotal;
|
|
return $"{delta:N0} {Purchasing3dIndicators.First(x => x.Key == _purchasing3dIndicator).Unit}";
|
|
}
|
|
|
|
private sealed record PurchasingKpiCard(string TitleDe, string TitleEn, string Value, string DetailDe, string DetailEn, string Icon, Color Color);
|
|
private sealed record PurchasingAxis(string LabelDe, string LabelEn, string Field, string UsageDe, string UsageEn);
|
|
private sealed record PurchasingSource(string Name, string Description);
|
|
private sealed record PurchasingPipelineStep(string TitleDe, string TitleEn, string Value, string DetailDe, string DetailEn, string Icon, bool IsReady, Color Color);
|
|
private sealed record PowerBiPageInfo(string Page, string Visuals, string Measure, string Dimensions);
|
|
private sealed record Purchasing3dIndicator(string Key, string TitleDe, string TitleEn, string Unit);
|
|
private sealed record Purchasing3dBaseRow(string Axis, int Year, double Spend, double OpenValue, double OpenQuantity, double ContractValue, double SupplierScore);
|
|
}
|
|
|
|
<style>
|
|
.purchasing-muted {
|
|
color: var(--mud-palette-text-secondary);
|
|
}
|
|
|
|
.purchasing-hero {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) 340px;
|
|
gap: 22px;
|
|
align-items: stretch;
|
|
padding: 24px;
|
|
color: #f7fbff;
|
|
background:
|
|
linear-gradient(135deg, rgba(11, 31, 51, .98), rgba(21, 101, 192, .86)),
|
|
linear-gradient(90deg, rgba(255,255,255,.08), rgba(255,255,255,0));
|
|
border: 1px solid rgba(255,255,255,.16);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.purchasing-hero-main {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
gap: 12px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.purchasing-hero-title {
|
|
max-width: 980px;
|
|
line-height: 1.08;
|
|
}
|
|
|
|
.purchasing-hero-text {
|
|
max-width: 860px;
|
|
color: rgba(255,255,255,.82);
|
|
}
|
|
|
|
.purchasing-hero-actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
|
|
.purchasing-hero-metrics {
|
|
display: grid;
|
|
grid-template-columns: 128px minmax(0, 1fr);
|
|
gap: 16px;
|
|
align-items: center;
|
|
padding: 16px;
|
|
background: rgba(255,255,255,.1);
|
|
border: 1px solid rgba(255,255,255,.14);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.purchasing-radar,
|
|
.purchasing-mini-donut {
|
|
position: relative;
|
|
display: grid;
|
|
place-items: center;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.purchasing-radar {
|
|
width: 128px;
|
|
height: 128px;
|
|
}
|
|
|
|
.purchasing-radar::after,
|
|
.purchasing-mini-donut::after {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 14px;
|
|
border-radius: 50%;
|
|
background: #102235;
|
|
}
|
|
|
|
.purchasing-radar > div,
|
|
.purchasing-mini-donut > strong {
|
|
position: relative;
|
|
z-index: 1;
|
|
text-align: center;
|
|
}
|
|
|
|
.purchasing-radar strong {
|
|
display: block;
|
|
font-size: 1.75rem;
|
|
line-height: 1;
|
|
}
|
|
|
|
.purchasing-radar span {
|
|
color: rgba(255,255,255,.72);
|
|
font-size: .8rem;
|
|
}
|
|
|
|
.purchasing-hero-note {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.purchasing-hero-note strong {
|
|
font-size: 1.1rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0;
|
|
}
|
|
|
|
.purchasing-hero-note span {
|
|
color: rgba(255,255,255,.76);
|
|
font-size: .9rem;
|
|
}
|
|
|
|
.purchasing-kpi {
|
|
min-height: 104px;
|
|
border-left: 4px solid var(--mud-palette-primary);
|
|
transition: transform .16s ease, box-shadow .16s ease;
|
|
}
|
|
|
|
.purchasing-kpi:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 24px rgba(0,0,0,.12);
|
|
}
|
|
|
|
.purchasing-kpi-icon {
|
|
width: 42px;
|
|
height: 42px;
|
|
display: grid;
|
|
place-items: center;
|
|
border-radius: 8px;
|
|
background: rgba(21, 101, 192, .1);
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.purchasing-overview-panel {
|
|
min-height: 100%;
|
|
}
|
|
|
|
.purchasing-pipeline {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, minmax(160px, 1fr));
|
|
gap: 12px;
|
|
}
|
|
|
|
.purchasing-pipeline-step {
|
|
display: grid;
|
|
grid-template-rows: auto minmax(96px, 1fr) auto;
|
|
gap: 10px;
|
|
padding: 14px;
|
|
border-radius: 8px;
|
|
border: 1px solid var(--mud-palette-lines-default);
|
|
background: var(--mud-palette-surface);
|
|
}
|
|
|
|
.purchasing-pipeline-step.is-ready {
|
|
border-top: 5px solid #2e7d32;
|
|
}
|
|
|
|
.purchasing-pipeline-step.is-waiting {
|
|
border-top: 5px solid #f9a825;
|
|
}
|
|
|
|
.purchasing-pipeline-step strong,
|
|
.purchasing-axis-card strong {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.purchasing-pipeline-step span,
|
|
.purchasing-axis-card span {
|
|
color: var(--mud-palette-text-secondary);
|
|
font-size: .86rem;
|
|
}
|
|
|
|
.purchasing-insights {
|
|
display: grid;
|
|
gap: 10px;
|
|
}
|
|
|
|
.purchasing-insight {
|
|
display: grid;
|
|
grid-template-columns: 26px minmax(0, 1fr);
|
|
gap: 8px;
|
|
align-items: start;
|
|
padding-bottom: 10px;
|
|
border-bottom: 1px solid var(--mud-palette-lines-default);
|
|
}
|
|
|
|
.purchasing-insight:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.purchasing-mini-donut-wrap {
|
|
display: grid;
|
|
grid-template-columns: 118px minmax(0, 1fr);
|
|
gap: 16px;
|
|
align-items: center;
|
|
margin-top: 18px;
|
|
padding-top: 16px;
|
|
border-top: 1px solid var(--mud-palette-lines-default);
|
|
}
|
|
|
|
.purchasing-mini-donut {
|
|
width: 118px;
|
|
height: 118px;
|
|
}
|
|
|
|
.purchasing-mini-donut::after {
|
|
background: var(--mud-palette-surface);
|
|
}
|
|
|
|
.purchasing-mini-donut strong {
|
|
font-size: 1.35rem;
|
|
}
|
|
|
|
.purchasing-mini-legend {
|
|
display: grid;
|
|
gap: 8px;
|
|
color: var(--mud-palette-text-secondary);
|
|
font-size: .86rem;
|
|
}
|
|
|
|
.purchasing-mini-legend span {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.purchasing-mini-legend i {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
display: inline-block;
|
|
}
|
|
|
|
.purchasing-mini-legend .ready {
|
|
background: #2e7d32;
|
|
}
|
|
|
|
.purchasing-mini-legend .waiting {
|
|
background: #cfd8dc;
|
|
}
|
|
|
|
.purchasing-mini-legend .simulation {
|
|
background: #f9a825;
|
|
}
|
|
|
|
.purchasing-axis-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(5, minmax(150px, 1fr));
|
|
gap: 12px;
|
|
}
|
|
|
|
.purchasing-axis-card {
|
|
min-height: 130px;
|
|
padding: 14px;
|
|
border: 1px solid var(--mud-palette-lines-default);
|
|
border-radius: 8px;
|
|
background: var(--mud-palette-background);
|
|
}
|
|
|
|
.purchasing-axis-card code {
|
|
display: inline-block;
|
|
margin-bottom: 8px;
|
|
white-space: normal;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.purchasing-source-row {
|
|
display: grid;
|
|
grid-template-columns: 26px minmax(0, 1fr);
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 7px 0;
|
|
border-bottom: 1px solid var(--mud-palette-lines-default);
|
|
}
|
|
|
|
.purchasing-source-row:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.purchasing-3d-surface {
|
|
height: calc(100vh - 300px);
|
|
min-height: 620px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
@@media (max-width: 1250px) {
|
|
.purchasing-hero,
|
|
.purchasing-pipeline,
|
|
.purchasing-axis-grid {
|
|
grid-template-columns: 1fr 1fr;
|
|
}
|
|
}
|
|
|
|
@@media (max-width: 760px) {
|
|
.purchasing-hero,
|
|
.purchasing-hero-metrics,
|
|
.purchasing-pipeline,
|
|
.purchasing-axis-grid,
|
|
.purchasing-mini-donut-wrap {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.purchasing-hero {
|
|
padding: 18px;
|
|
}
|
|
}
|
|
</style>
|