Files
Ai/TrafagSalesExporter/Components/Pages/PurchasingDashboard.razor
T

419 lines
22 KiB
Plaintext

@page "/einkauf"
@using System.Globalization
@using TrafagSalesExporter.Models
@inject TrafagSalesExporter.Services.IUiTextService UiText
@inject IJSRuntime JsRuntime
<PageTitle>@T("Einkauf", "Purchasing")</PageTitle>
<MudText Typo="Typo.h4" Class="mb-1">@T("Einkauf", "Purchasing")</MudText>
<MudText Typo="Typo.body2" Class="mb-4 purchasing-muted">
@T("SAP-Einkaufsdashboard nach Power-BI-Vorlage x.pbix, vorbereitet fuer Spend, offene Bestellungen, Kontrakte und Lieferantenperformance.",
"SAP purchasing dashboard based on the Power BI template x.pbix, prepared for spend, open orders, contracts and supplier performance.")
</MudText>
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Class="mb-4">
@T("Die PBIX-Vorlage liefert Struktur und Kennzahlenlogik. Live-Werte werden sichtbar, sobald die SAP-Einkaufsdatenquelle angebunden ist.",
"The PBIX template provides structure and KPI logic. Live values will appear once the SAP purchasing data source is connected.")
</MudAlert>
<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">
<MudIcon Icon="@card.Icon" Color="@card.Color" Size="Size.Large" />
<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="7">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.h6" Class="mb-2">@T("Analyseachsen", "Analysis axes")</MudText>
<MudTable Items="@AnalysisAxes" Dense="true" Hover="true">
<HeaderContent>
<MudTh>@T("Achse", "Axis")</MudTh>
<MudTh>@T("PBIX-Feld", "PBIX field")</MudTh>
<MudTh>@T("Verwendung", "Usage")</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@T(context.LabelDe, context.LabelEn)</MudTd>
<MudTd><code>@context.Field</code></MudTd>
<MudTd>@T(context.UsageDe, context.UsageEn)</MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
</MudItem>
<MudItem xs="12" lg="5">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.h6" Class="mb-2">@T("SAP-Quellen aus PBIX", "SAP sources from PBIX")</MudText>
@foreach (var source in SapSources)
{
<div class="purchasing-source-row">
<MudIcon Icon="@Icons.Material.Filled.TableChart" Size="Size.Small" />
<span><strong>@source.Name</strong> @source.Description</span>
</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."
DescriptionEn="Purchasing volume in CHF by year, supplier, material group and article."
Rows="@SpendRows" />
</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="Vorbereitet fuer offene Werte/Mengen pro Lieferant, Warengruppe und Artikel."
DescriptionEn="Prepared for open values/quantities by supplier, material group and article."
Rows="@OpenOrderRows" />
</MudTabPanel>
<MudTabPanel Text="@T("Kontrakte", "Contracts")" Icon="@Icons.Material.Filled.Assignment">
<PurchasingSection TitleDe="Offene Verpflichtungen"
TitleEn="Open commitments"
DescriptionDe="Vorbereitet fuer Mengenkontrakte und Restverpflichtungen pro Lieferant, Warengruppe und Artikel."
DescriptionEn="Prepared for quantity contracts and remaining commitments by supplier, material group and article."
Rows="@ContractRows" />
</MudTabPanel>
<MudTabPanel Text="@T("Lieferanten", "Suppliers")" Icon="@Icons.Material.Filled.Verified">
<PurchasingSection TitleDe="Lieferantenbewertung und Performance"
TitleEn="Supplier rating and performance"
DescriptionDe="Vorbereitet fuer Liefertermintreue, Qualitaet, Preisentwicklung und Reklamationen."
DescriptionEn="Prepared for delivery reliability, quality, price development and claims."
Rows="@SupplierPerformanceRows" />
</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 string _purchasing3dIndicator = "spend";
private string _purchasing3dChartType = "bar";
private double _purchasing3dFactor = 1d;
private double _purchasing3dLabelScale = 1.5d;
private readonly List<PurchasingKpiCard> KpiCards =
[
new("Spend total", "Total spend", "-", "Netwr CHF historisch", "Historic Netwr CHF", Icons.Material.Filled.Payments, Color.Primary),
new("Offene Bestellungen", "Open orders", "-", "Wert und Menge offen", "Open value and quantity", Icons.Material.Filled.PendingActions, Color.Warning),
new("Kontrakte", "Contracts", "-", "Restverpflichtungen", "Remaining commitments", Icons.Material.Filled.Assignment, Color.Info),
new("Lieferantenperformance", "Supplier performance", "-", "Bewertung / Liefertermintreue", "Rating / delivery reliability", 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 readonly List<PurchasingAnalysisRow> SpendRows =
[
new("Spend nach Jahr", "Spend by year", "Sum(EKPOSet.Netwr CHF)", "EKKOSet.Bedat Jahr", "PBIX"),
new("Spend nach Lieferant", "Spend by supplier", "Sum(EKPOSet.Netwr CHF)", "Data.Name", "PBIX"),
new("Spend nach Warengruppe", "Spend by material group", "Sum(EKPOSet.Netwr CHF)", "Data (2).Warengruppe", "PBIX"),
new("Spend nach Artikel", "Spend by article", "Sum(EKPOSet.Netwr CHF)", "EKPOSet.Matnr / Txz01", "PBIX")
];
private readonly List<PurchasingAnalysisRow> OpenOrderRows =
[
new("Offener Bestellwert", "Open order value", "Bestellwert offen CHF", "Lieferant / Warengruppe / Artikel", "Neu"),
new("Offene Bestellmenge", "Open order quantity", "Menge offen", "Lieferant / Warengruppe / Artikel", "Neu"),
new("Faelligkeiten", "Due dates", "Termin / Rueckstand", "Lieferant / Artikel", "Neu")
];
private readonly List<PurchasingAnalysisRow> ContractRows =
[
new("Mengenkontrakte", "Quantity contracts", "Kontraktmenge / Abrufmenge", "Lieferant / Warengruppe / Artikel", "Neu"),
new("Restverpflichtung", "Remaining commitment", "Restwert CHF", "Lieferant / Warengruppe / Artikel", "Neu"),
new("Kontrakt-Ausnutzung", "Contract consumption", "Abrufquote", "Lieferant / Artikel", "Neu")
];
private readonly List<PurchasingAnalysisRow> SupplierPerformanceRows =
[
new("Liefertermintreue", "Delivery reliability", "On-time %", "Lieferant / Artikel", "Neu"),
new("Preisentwicklung", "Price development", "Netwr CHF/Stk", "Lieferant / Artikel / Jahr", "PBIX"),
new("Qualitaet / Reklamation", "Quality / claims", "Fehlerquote", "Lieferant / Warengruppe", "Neu"),
new("Lieferantenbewertung", "Supplier rating", "Score", "Lieferant", "Neu")
];
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)
];
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
await RenderPurchasing3dAsync();
}
private string T(string german, string english) => UiText.Text(german, english);
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 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-kpi {
min-height: 104px;
}
.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;
}
</style>