@page "/einkauf" @using System.Globalization @using TrafagSalesExporter.Models @using TrafagSalesExporter.Services @inject TrafagSalesExporter.Services.IUiTextService UiText @inject IJSRuntime JsRuntime @inject IPurchasingDashboardService PurchasingDashboardService @T("Einkauf", "Purchasing") @T("Einkauf", "Purchasing") @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.") @PurchasingStatusText @foreach (var card in KpiCards) { @T(card.TitleDe, card.TitleEn) @card.Value @T(card.DetailDe, card.DetailEn) } @T("Analyseachsen", "Analysis axes") @T("Achse", "Axis") @T("PBIX-Feld", "PBIX field") @T("Verwendung", "Usage") @T(context.LabelDe, context.LabelEn) @context.Field @T(context.UsageDe, context.UsageEn) @T("SAP-Quellen aus PBIX", "SAP sources from PBIX") @foreach (var source in SapSources) { @source.Name @source.Description } @T("Aus x.pbix uebernommene Seiten", "Pages derived from x.pbix") @T("Power-BI-Seite", "Power BI page") @T("Visuals", "Visuals") @T("Kennzahl", "Measure") @T("Dimensionen", "Dimensions") @context.Page @context.Visuals @context.Measure @context.Dimensions @foreach (var option in Purchasing3dIndicators) { @T(option.TitleDe, option.TitleEn) } @T("Balken", "Bars") @T("Linie", "Line") @T("Flaeche", "Surface") @T("Kreis", "Pie") @T("Preis-/Wechselkurs-Szenario", "Price/exchange-rate scenario") -10% @_purchasing3dFactor.ToString("0.00", CultureInfo.InvariantCulture)x @T("Delta", "Delta"): @FormatScenarioDelta() @T("Beschriftung", "Labels") @_purchasing3dLabelScale.ToString("0.0", CultureInfo.InvariantCulture)x @T("Neu zeichnen", "Redraw") @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 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 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 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 IReadOnlyList 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 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 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 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 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 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 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 SpendChartRows => BuildPurchasingChartRows(x => x.Spend, FormatChf); private IReadOnlyList OpenOrderChartRows => _liveState.EkkoLoaded ? BuildOpenOrderLiveChartRows() : BuildPurchasingChartRows(x => x.OpenValue, FormatChf); private IReadOnlyList ContractChartRows => BuildPurchasingChartRows(x => x.ContractValue, FormatChf); private IReadOnlyList SupplierChartRows => BuildPurchasingChartRows(x => x.SupplierScore, value => $"{value:N1}%"); private IReadOnlyList 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 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 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 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 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 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 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 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 IReadOnlyList BuildPurchasingChartRows(Func selector, Func 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 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 selector, Func 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 BuildPurchasing3dRows() => Purchasing3dBaseRows .Select(row => new { country = row.Axis, year = row.Year, value = ResolvePurchasing3dValue(row) }) .Cast() .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); }
@context.Field
@context.Measure