1172 lines
62 KiB
Plaintext
1172 lines
62 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("Ideen", "Ideas")" Icon="@Icons.Material.Filled.Lightbulb">
|
|
<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("Weitere Einkaufsanalysen", "Additional purchasing analytics")</MudText>
|
|
<MudText Typo="Typo.body2" Class="purchasing-muted">
|
|
@T("Analysen, die dem Einkauf neben PowerBI mehr Steuerung, Risiko- und Sparpotenzial zeigen.",
|
|
"Analytics that give purchasing more steering, risk and savings potential beyond Power BI.")
|
|
</MudText>
|
|
</div>
|
|
<MudChip T="string" Color="Color.Info" Variant="Variant.Outlined">
|
|
@T("Roadmap", "Roadmap")
|
|
</MudChip>
|
|
</MudStack>
|
|
<div class="purchasing-idea-grid">
|
|
@foreach (var idea in PurchasingIdeas)
|
|
{
|
|
<div class="purchasing-idea-card">
|
|
<div class="purchasing-idea-icon">
|
|
<MudIcon Icon="@idea.Icon" Color="@idea.Color" />
|
|
</div>
|
|
<div class="purchasing-idea-content">
|
|
<div class="purchasing-idea-title">
|
|
<strong>@T(idea.TitleDe, idea.TitleEn)</strong>
|
|
<MudChip T="string" Size="Size.Small" Color="@idea.Color" Variant="Variant.Outlined">@T(idea.StatusDe, idea.StatusEn)</MudChip>
|
|
</div>
|
|
<span>@T(idea.DescriptionDe, idea.DescriptionEn)</span>
|
|
<div class="purchasing-idea-meta">
|
|
<code>@idea.RequiredData</code>
|
|
<span>@T("Nutzen", "Value"): @T(idea.ValueDe, idea.ValueEn)</span>
|
|
</div>
|
|
</div>
|
|
</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("Prioritaet", "Priority")</MudText>
|
|
<div class="purchasing-priority-stack">
|
|
@foreach (var priority in PurchasingIdeaPriorities)
|
|
{
|
|
<div class="purchasing-priority-row">
|
|
<MudIcon Icon="@priority.Icon" Color="@priority.Color" Size="Size.Small" />
|
|
<div>
|
|
<strong>@T(priority.TitleDe, priority.TitleEn)</strong>
|
|
<span>@T(priority.DetailDe, priority.DetailEn)</span>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</MudPaper>
|
|
</MudItem>
|
|
<MudItem xs="12">
|
|
<MudPaper Class="pa-3 purchasing-overview-panel" Outlined="true">
|
|
<MudText Typo="Typo.h6" Class="mb-2">@T("Kennzahlen-Katalog fuer den naechsten Ausbau", "KPI catalogue for the next build-out")</MudText>
|
|
<MudTable Items="@PurchasingIdeaKpis" Dense="true" Hover="true" Striped="true">
|
|
<HeaderContent>
|
|
<MudTh>@T("Analyse", "Analysis")</MudTh>
|
|
<MudTh>@T("Kennzahl", "KPI")</MudTh>
|
|
<MudTh>@T("Dimension", "Dimension")</MudTh>
|
|
<MudTh>@T("Datenbasis", "Data basis")</MudTh>
|
|
<MudTh>@T("Status", "Status")</MudTh>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd>@T(context.AnalysisDe, context.AnalysisEn)</MudTd>
|
|
<MudTd><strong>@T(context.KpiDe, context.KpiEn)</strong></MudTd>
|
|
<MudTd>@context.Dimension</MudTd>
|
|
<MudTd><code>@context.Source</code></MudTd>
|
|
<MudTd>
|
|
<MudChip T="string" Size="Size.Small" Color="@context.Color" Variant="Variant.Outlined">@T(context.StatusDe, context.StatusEn)</MudChip>
|
|
</MudTd>
|
|
</RowTemplate>
|
|
</MudTable>
|
|
</MudPaper>
|
|
</MudItem>
|
|
</MudGrid>
|
|
</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 IReadOnlyList<PurchasingIdea> PurchasingIdeas =>
|
|
[
|
|
new("Lieferantenrisiko", "Supplier risk", "Kombiniert Abhaengigkeit, Single-Source-Anteil, offene Bestellungen und Lieferperformance zu einem Risiko-Score.", "Combines dependency, single-source share, open orders and delivery performance into one risk score.", "EKKO, EKPO, EKET, LFA1", "Engpaesse und Lieferantenabhaengigkeit frueh sehen", "see shortages and supplier dependency early", _liveState.EkpoLoaded && _liveState.EketLoaded ? "berechenbar" : "wartet auf EKPO/EKET", _liveState.EkpoLoaded && _liveState.EketLoaded ? "calculable" : "waiting for EKPO/EKET", Icons.Material.Filled.WarningAmber, _liveState.EkpoLoaded && _liveState.EketLoaded ? Color.Success : Color.Warning),
|
|
new("Preisabweichung", "Price variance", "Zeigt Preissteigerungen pro Artikel/Lieferant gegen Vorjahr, Budget oder letzten Einkaufspreis.", "Shows price increases by article/supplier against prior year, budget or last purchase price.", "EKPO, EKKO, FX", "Sparpotenziale und Ausreisser direkt sichtbar", "savings potential and outliers visible immediately", _liveState.EkpoLoaded ? "berechenbar" : "wartet auf EKPO", _liveState.EkpoLoaded ? "calculable" : "waiting for EKPO", Icons.Material.Filled.TrendingUp, _liveState.EkpoLoaded ? Color.Success : Color.Warning),
|
|
new("Maverick Buying", "Maverick buying", "Findet Bestellungen ausserhalb bevorzugter Lieferanten, Rahmenvertraege oder Warengruppenregeln.", "Finds orders outside preferred suppliers, contracts or material-group rules.", "EKKO, EKPO, Kontrakte", "Compliance und Buendelung verbessern", "improve compliance and bundling", "Konzept", "concept", Icons.Material.Filled.Policy, Color.Info),
|
|
new("Rahmenvertragsnutzung", "Contract utilisation", "Zeigt Kontraktmenge, Abrufmenge, Restmenge, Laufzeit und drohenden Verfall.", "Shows contract quantity, call-off quantity, remaining quantity, term and expiry risk.", "EKKO, EKPO, EKET", "Restverpflichtungen aktiv steuern", "actively manage remaining commitments", _liveState.EketLoaded ? "teilweise" : "wartet auf EKET", _liveState.EketLoaded ? "partial" : "waiting for EKET", Icons.Material.Filled.AssignmentTurnedIn, _liveState.EketLoaded ? Color.Success : Color.Warning),
|
|
new("Working Capital", "Working capital", "Verbindet offene Bestellungen, Liefertermine und Zahlungs-/Bestandswirkung zu Cash-Ausblick.", "Connects open orders, delivery dates and payment/inventory impact into a cash outlook.", "EKPO, EKET, FI/AP", "Cashbedarf aus Einkauf vorhersagen", "forecast purchasing cash needs", "Konzept", "concept", Icons.Material.Filled.AccountBalanceWallet, Color.Info),
|
|
new("Datenqualitaet", "Data quality", "Prueft fehlende Lieferanten, Warengruppen, Artikeltexte, Waehrung, Preisbasis und Dubletten.", "Checks missing suppliers, material groups, article texts, currency, price basis and duplicates.", "EKKO, EKPO, Mapping", "Vertrauen in Kennzahlen sichern", "secure trust in KPIs", _liveState.EkkoLoaded ? "startklar" : "wartet", _liveState.EkkoLoaded ? "ready" : "waiting", Icons.Material.Filled.FactCheck, _liveState.EkkoLoaded ? Color.Success : Color.Warning)
|
|
];
|
|
|
|
private IReadOnlyList<PurchasingIdeaPriority> PurchasingIdeaPriorities =>
|
|
[
|
|
new("1. EKPO/EKET Daten reparieren", "1. Repair EKPO/EKET data", "Ohne Positionen fehlen echte Spend-, Artikel-, Warengruppen- und Termindaten.", "Without item rows, real spend, article, material-group and schedule data are missing.", Icons.Material.Filled.BuildCircle, Color.Warning),
|
|
new("2. Preisabweichung aktivieren", "2. Activate price variance", "Sehr hoher Managementnutzen, sobald EKPO Werte liefert.", "Very high management value as soon as EKPO provides values.", Icons.Material.Filled.TrendingUp, Color.Info),
|
|
new("3. Lieferantenrisiko aufbauen", "3. Build supplier risk", "Kombiniert Performance, offene Werte und Abhaengigkeit.", "Combines performance, open values and dependency.", Icons.Material.Filled.Security, Color.Info),
|
|
new("4. Contract Cockpit ausbauen", "4. Extend contract cockpit", "Mengenkontrakte und Restverpflichtungen brauchen EKET/EKPO.", "Quantity contracts and remaining commitments need EKET/EKPO.", Icons.Material.Filled.Assignment, Color.Info)
|
|
];
|
|
|
|
private IReadOnlyList<PurchasingIdeaKpi> PurchasingIdeaKpis =>
|
|
[
|
|
new("Lieferantenrisiko", "Supplier risk", "Risiko-Score 0-100", "Risk score 0-100", "Lieferant / Warengruppe / Artikel", "EKKO+EKPO+EKET", _liveState.EkpoLoaded && _liveState.EketLoaded ? "bereit" : "wartet auf Tabellen", _liveState.EkpoLoaded && _liveState.EketLoaded ? "ready" : "waiting for tables", _liveState.EkpoLoaded && _liveState.EketLoaded ? Color.Success : Color.Warning),
|
|
new("Preisabweichung", "Price variance", "Preisdelta % / CHF", "price delta % / CHF", "Artikel / Lieferant / Jahr", "EKPO", _liveState.EkpoLoaded ? "bereit" : "wartet auf EKPO", _liveState.EkpoLoaded ? "ready" : "waiting for EKPO", _liveState.EkpoLoaded ? Color.Success : Color.Warning),
|
|
new("Maverick Buying", "Maverick buying", "Anteil ausserhalb Vertrag", "share outside contract", "Einkaeufer / Lieferant / Warengruppe", "EKKO+EKPO+Kontrakt", "Konzept", "concept", Color.Info),
|
|
new("Rahmenvertragsnutzung", "Contract utilisation", "Abrufquote %", "consumption rate %", "Kontrakt / Lieferant / Artikel", "EKPO+EKET", _liveState.EketLoaded ? "teilweise" : "wartet auf EKET", _liveState.EketLoaded ? "partial" : "waiting for EKET", _liveState.EketLoaded ? Color.Success : Color.Warning),
|
|
new("Working Capital", "Working capital", "Cash Forecast CHF", "cash forecast CHF", "Monat / Lieferant / Warengruppe", "EKPO+EKET+FI", "Konzept", "concept", Color.Info),
|
|
new("Datenqualitaet", "Data quality", "Mapping-Abdeckung %", "mapping coverage %", "Tabelle / Feld / Land", "EKKO+EKPO+Mapping", _liveState.EkkoLoaded ? "startklar" : "wartet", _liveState.EkkoLoaded ? "ready" : "waiting", _liveState.EkkoLoaded ? Color.Success : Color.Warning)
|
|
];
|
|
|
|
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 PurchasingIdea(string TitleDe, string TitleEn, string DescriptionDe, string DescriptionEn, string RequiredData, string ValueDe, string ValueEn, string StatusDe, string StatusEn, string Icon, Color Color);
|
|
private sealed record PurchasingIdeaPriority(string TitleDe, string TitleEn, string DetailDe, string DetailEn, string Icon, Color Color);
|
|
private sealed record PurchasingIdeaKpi(string AnalysisDe, string AnalysisEn, string KpiDe, string KpiEn, string Dimension, string Source, string StatusDe, string StatusEn, Color Color);
|
|
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-idea-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(260px, 1fr));
|
|
gap: 12px;
|
|
}
|
|
|
|
.purchasing-idea-card {
|
|
display: grid;
|
|
grid-template-columns: 46px minmax(0, 1fr);
|
|
gap: 12px;
|
|
padding: 14px;
|
|
border: 1px solid var(--mud-palette-lines-default);
|
|
border-radius: 8px;
|
|
background: var(--mud-palette-surface);
|
|
min-height: 170px;
|
|
}
|
|
|
|
.purchasing-idea-icon {
|
|
width: 42px;
|
|
height: 42px;
|
|
display: grid;
|
|
place-items: center;
|
|
border-radius: 8px;
|
|
background: rgba(21,101,192,.1);
|
|
}
|
|
|
|
.purchasing-idea-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 9px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.purchasing-idea-title {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: start;
|
|
gap: 10px;
|
|
}
|
|
|
|
.purchasing-idea-content > span,
|
|
.purchasing-idea-meta span,
|
|
.purchasing-priority-row span {
|
|
color: var(--mud-palette-text-secondary);
|
|
font-size: .88rem;
|
|
}
|
|
|
|
.purchasing-idea-meta {
|
|
display: grid;
|
|
gap: 6px;
|
|
margin-top: auto;
|
|
}
|
|
|
|
.purchasing-idea-meta code {
|
|
white-space: normal;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.purchasing-priority-stack {
|
|
display: grid;
|
|
gap: 10px;
|
|
}
|
|
|
|
.purchasing-priority-row {
|
|
display: grid;
|
|
grid-template-columns: 28px minmax(0, 1fr);
|
|
gap: 10px;
|
|
align-items: start;
|
|
padding: 11px 0;
|
|
border-bottom: 1px solid var(--mud-palette-lines-default);
|
|
}
|
|
|
|
.purchasing-priority-row:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.purchasing-priority-row strong {
|
|
display: block;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.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-idea-grid,
|
|
.purchasing-mini-donut-wrap {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.purchasing-hero {
|
|
padding: 18px;
|
|
}
|
|
}
|
|
</style>
|