Build out purchasing dashboard sections

This commit is contained in:
2026-06-05 09:11:55 +02:00
parent 146c7481e1
commit bf20b3a240
3 changed files with 293 additions and 37 deletions
@@ -74,33 +74,53 @@
<MudTabPanel Text="@T("Spend", "Spend")" Icon="@Icons.Material.Filled.Payments"> <MudTabPanel Text="@T("Spend", "Spend")" Icon="@Icons.Material.Filled.Payments">
<PurchasingSection TitleDe="Spend total vergangen" <PurchasingSection TitleDe="Spend total vergangen"
TitleEn="Historic total spend" TitleEn="Historic total spend"
DescriptionDe="Beschaffungsvolumen in CHF nach Jahr, Lieferant, Warengruppe und Artikel." 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." 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."
Rows="@SpendRows" /> ChartTitleDe="Spend-Verlauf nach Einkaufsdimension"
ChartTitleEn="Spend trend by purchasing dimension"
Kpis="@SpendKpis"
ChartRows="@SpendChartRows"
StatusRows="@SpendStatusRows"
DetailRows="@SpendDetailRows" />
</MudTabPanel> </MudTabPanel>
<MudTabPanel Text="@T("Offene Bestellungen", "Open orders")" Icon="@Icons.Material.Filled.PendingActions"> <MudTabPanel Text="@T("Offene Bestellungen", "Open orders")" Icon="@Icons.Material.Filled.PendingActions">
<PurchasingSection TitleDe="Offene Bestellwerte und Mengen" <PurchasingSection TitleDe="Offene Bestellwerte und Mengen"
TitleEn="Open order values and quantities" TitleEn="Open order values and quantities"
DescriptionDe="Vorbereitet fuer offene Werte/Mengen pro Lieferant, Warengruppe und Artikel." DescriptionDe="Live-Bestellkoepfe aus EKKO sind angebunden. Offene Werte und Mengen brauchen zusaetzlich EKPO/EKET."
DescriptionEn="Prepared for open values/quantities by supplier, material group and article." DescriptionEn="Live purchase-order headers from EKKO are connected. Open values and quantities additionally need EKPO/EKET."
Rows="@OpenOrderRows" /> ChartTitleDe="Bestellaktivitaet und offene Positionen"
ChartTitleEn="Order activity and open items"
Kpis="@OpenOrderKpis"
ChartRows="@OpenOrderChartRows"
StatusRows="@OpenOrderStatusRows"
DetailRows="@OpenOrderDetailRows" />
</MudTabPanel> </MudTabPanel>
<MudTabPanel Text="@T("Kontrakte", "Contracts")" Icon="@Icons.Material.Filled.Assignment"> <MudTabPanel Text="@T("Kontrakte", "Contracts")" Icon="@Icons.Material.Filled.Assignment">
<PurchasingSection TitleDe="Offene Verpflichtungen" <PurchasingSection TitleDe="Offene Verpflichtungen"
TitleEn="Open commitments" TitleEn="Open commitments"
DescriptionDe="Vorbereitet fuer Mengenkontrakte und Restverpflichtungen pro Lieferant, Warengruppe und Artikel." DescriptionDe="Kontrakte und Restverpflichtungen werden auf EKPO/EKET aufgebaut. Der Reiter zeigt bereits die Zielkennzahlen und den aktuellen Ladezustand."
DescriptionEn="Prepared for quantity contracts and remaining commitments by supplier, material group and article." DescriptionEn="Contracts and remaining commitments are built on EKPO/EKET. This tab already shows the target KPIs and current load status."
Rows="@ContractRows" /> ChartTitleDe="Kontrakt- und Verpflichtungsuebersicht"
ChartTitleEn="Contract and commitment overview"
Kpis="@ContractKpis"
ChartRows="@ContractChartRows"
StatusRows="@ContractStatusRows"
DetailRows="@ContractDetailRows" />
</MudTabPanel> </MudTabPanel>
<MudTabPanel Text="@T("Lieferanten", "Suppliers")" Icon="@Icons.Material.Filled.Verified"> <MudTabPanel Text="@T("Lieferanten", "Suppliers")" Icon="@Icons.Material.Filled.Verified">
<PurchasingSection TitleDe="Lieferantenbewertung und Performance" <PurchasingSection TitleDe="Lieferantenbewertung und Performance"
TitleEn="Supplier rating and performance" TitleEn="Supplier rating and performance"
DescriptionDe="Vorbereitet fuer Liefertermintreue, Qualitaet, Preisentwicklung und Reklamationen." DescriptionDe="Lieferantenbasis kommt live aus EKKO. Bewertung, Termintreue und Preisentwicklung brauchen spaeter EKPO/EKET und Reklamationsdaten."
DescriptionEn="Prepared for delivery reliability, quality, price development and claims." DescriptionEn="The supplier base comes live from EKKO. Rating, delivery reliability and price development later need EKPO/EKET and claim data."
Rows="@SupplierPerformanceRows" /> ChartTitleDe="Lieferantenbasis und Performance-Indikatoren"
ChartTitleEn="Supplier base and performance indicators"
Kpis="@SupplierKpis"
ChartRows="@SupplierChartRows"
StatusRows="@SupplierStatusRows"
DetailRows="@SupplierDetailRows" />
</MudTabPanel> </MudTabPanel>
<MudTabPanel Text="@T("PBIX Vorlage", "PBIX template")" Icon="@Icons.Material.Filled.InsertChart"> <MudTabPanel Text="@T("PBIX Vorlage", "PBIX template")" Icon="@Icons.Material.Filled.InsertChart">
@@ -220,34 +240,36 @@
new("Data (2)", "Warengruppen-Mapping und Warengruppentexte.") new("Data (2)", "Warengruppen-Mapping und Warengruppentexte.")
]; ];
private readonly List<PurchasingAnalysisRow> SpendRows = private IReadOnlyList<PurchasingSectionKpi> SpendKpis =>
[ [
new("Spend nach Jahr", "Spend by year", "Sum(EKPOSet.Netwr CHF)", "EKKOSet.Bedat Jahr", "PBIX"), 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("Spend nach Lieferant", "Spend by supplier", "Sum(EKPOSet.Netwr CHF)", "Data.Name", "PBIX"), new("Jahre", "Years", "2024-2026", "aus PBIX-Struktur", "from PBIX structure"),
new("Spend nach Warengruppe", "Spend by material group", "Sum(EKPOSet.Netwr CHF)", "Data (2).Warengruppe", "PBIX"), new("Dimensionen", "Dimensions", "4", "Jahr, Lieferant, Warengruppe, Artikel", "year, supplier, material group, article"),
new("Spend nach Artikel", "Spend by article", "Sum(EKPOSet.Netwr CHF)", "EKPOSet.Matnr / Txz01", "PBIX") new("SAP Status", "SAP status", _liveState.EkpoLoaded ? "live" : "wartet", "EKPOSet fuer Spend notwendig", "EKPOSet required for spend")
]; ];
private readonly List<PurchasingAnalysisRow> OpenOrderRows = private IReadOnlyList<PurchasingSectionKpi> OpenOrderKpis =>
[ [
new("Offener Bestellwert", "Open order value", "Bestellwert offen CHF", "Lieferant / Warengruppe / Artikel", "Neu"), new("Bestellungen", "Orders", _liveState.EkkoLoaded ? _liveState.PurchaseOrderCount.ToString("N0") : "-", "EKKO live seit Jahresbeginn", "EKKO live since start of year"),
new("Offene Bestellmenge", "Open order quantity", "Menge offen", "Lieferant / Warengruppe / Artikel", "Neu"), new("Lieferanten", "Suppliers", _liveState.EkkoLoaded ? _liveState.SupplierCount.ToString("N0") : "-", "aus EKKO-Liveprobe", "from EKKO live sample"),
new("Faelligkeiten", "Due dates", "Termin / Rueckstand", "Lieferant / Artikel", "Neu") 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 readonly List<PurchasingAnalysisRow> ContractRows = private IReadOnlyList<PurchasingSectionKpi> ContractKpis =>
[ [
new("Mengenkontrakte", "Quantity contracts", "Kontraktmenge / Abrufmenge", "Lieferant / Warengruppe / Artikel", "Neu"), 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("Restverpflichtung", "Remaining commitment", "Restwert CHF", "Lieferant / Warengruppe / Artikel", "Neu"), new("Einteilungen", "Schedules", _liveState.EketLoaded ? _liveState.ScheduleSampleCount.ToString("N0") : "-", "EKET-Probe", "EKET sample"),
new("Kontrakt-Ausnutzung", "Contract consumption", "Abrufquote", "Lieferant / Artikel", "Neu") 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 readonly List<PurchasingAnalysisRow> SupplierPerformanceRows = private IReadOnlyList<PurchasingSectionKpi> SupplierKpis =>
[ [
new("Liefertermintreue", "Delivery reliability", "On-time %", "Lieferant / Artikel", "Neu"), new("Aktive Lieferanten", "Active suppliers", _liveState.EkkoLoaded ? _liveState.SupplierCount.ToString("N0") : "-", "EKKO live seit Jahresbeginn", "EKKO live since start of year"),
new("Preisentwicklung", "Price development", "Netwr CHF/Stk", "Lieferant / Artikel / Jahr", "PBIX"), new("Performance Score", "Performance score", $"{Purchasing3dBaseRows.Average(x => x.SupplierScore):N1}%", "Simulation bis Bewertungsdaten kommen", "simulation until rating data arrives"),
new("Qualitaet / Reklamation", "Quality / claims", "Fehlerquote", "Lieferant / Warengruppe", "Neu"), new("Preisindikator", "Price indicator", _liveState.EkpoLoaded ? "EKPO live" : "wartet", "Netwr CHF/Stk braucht EKPO", "Netwr CHF/unit needs EKPO"),
new("Lieferantenbewertung", "Supplier rating", "Score", "Lieferant", "Neu") new("Qualitaet", "Quality", "offen", "Reklamationsquelle noch nicht angebunden", "claim source not connected yet")
]; ];
private readonly List<PowerBiPageInfo> PowerBiPages = private readonly List<PowerBiPageInfo> PowerBiPages =
@@ -286,6 +308,80 @@
new("Artikel Top 10", 2026, 680000d, 105000d, 3350d, 195000d, 92d) 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() protected override async Task OnInitializedAsync()
{ {
_liveState = await PurchasingDashboardService.LoadAsync(); _liveState = await PurchasingDashboardService.LoadAsync();
@@ -300,6 +396,9 @@
private string T(string german, string english) => UiText.Text(german, english); private string T(string german, string english) => UiText.Text(german, english);
private static string FormatChf(double value) => $"CHF {value:N0}"; 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 private string PurchasingStatusText
=> _liveLoading => _liveLoading
? T("SAP-Einkaufsdaten werden geladen...", "Loading SAP purchasing data...") ? T("SAP-Einkaufsdaten werden geladen...", "Loading SAP purchasing data...")
@@ -310,6 +409,43 @@
? $"{T("Letztes EKKO-Datum", "Latest EKKO date")}: {_liveState.LatestOrderDate.Value:yyyy-MM-dd}." ? $"{T("Letztes EKKO-Datum", "Latest EKKO date")}: {_liveState.LatestOrderDate.Value:yyyy-MM-dd}."
: string.Empty; : string.Empty;
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) private async Task SetPurchasing3dIndicator(string value)
{ {
_purchasing3dIndicator = value; _purchasing3dIndicator = value;
@@ -5,19 +5,65 @@
<MudText Typo="Typo.h6">@T(TitleDe, TitleEn)</MudText> <MudText Typo="Typo.h6">@T(TitleDe, TitleEn)</MudText>
<MudText Typo="Typo.body2" Class="mb-3 purchasing-section-muted">@T(DescriptionDe, DescriptionEn)</MudText> <MudText Typo="Typo.body2" Class="mb-3 purchasing-section-muted">@T(DescriptionDe, DescriptionEn)</MudText>
<MudTable Items="@Rows" Dense="true" Hover="true"> <MudGrid Spacing="2" Class="mb-3">
@foreach (var kpi in Kpis)
{
<MudItem xs="12" sm="6" lg="3">
<MudPaper Class="pa-3 purchasing-section-kpi" Outlined="true">
<MudText Typo="Typo.caption" Class="purchasing-section-muted">@T(kpi.LabelDe, kpi.LabelEn)</MudText>
<MudText Typo="Typo.h6">@kpi.Value</MudText>
<MudText Typo="Typo.caption">@T(kpi.DetailDe, kpi.DetailEn)</MudText>
</MudPaper>
</MudItem>
}
</MudGrid>
<MudGrid Spacing="2" Class="mb-3">
<MudItem xs="12" lg="7">
<MudPaper Class="pa-3 purchasing-section-panel" Outlined="true">
<MudText Typo="Typo.subtitle1" Class="mb-2">@T(ChartTitleDe, ChartTitleEn)</MudText>
<div class="purchasing-bars">
@foreach (var item in ChartRows)
{
<div class="purchasing-bar-row">
<div class="purchasing-bar-label">@item.Label</div>
<div class="purchasing-bar-track">
<div class="purchasing-bar-fill" style="@($"width:{BuildWidth(item.Percent)}%; background:{item.Color}")"></div>
</div>
<div class="purchasing-bar-value">@item.Value</div>
</div>
}
</div>
</MudPaper>
</MudItem>
<MudItem xs="12" lg="5">
<MudPaper Class="pa-3 purchasing-section-panel" Outlined="true">
<MudText Typo="Typo.subtitle1" Class="mb-2">@T("Datenstatus", "Data status")</MudText>
@foreach (var status in StatusRows)
{
<div class="purchasing-status-row">
<MudIcon Icon="@status.Icon" Color="@status.Color" Size="Size.Small" />
<span>@T(status.LabelDe, status.LabelEn)</span>
<strong>@status.Value</strong>
</div>
}
</MudPaper>
</MudItem>
</MudGrid>
<MudTable Items="@DetailRows" Dense="true" Hover="true" Striped="true">
<HeaderContent> <HeaderContent>
<MudTh>@T("Analyse", "Analysis")</MudTh> <MudTh>@T("Bereich", "Area")</MudTh>
<MudTh>@T("Kennzahl", "Measure")</MudTh> <MudTh>@T("Wert", "Value")</MudTh>
<MudTh>@T("Dimension", "Dimension")</MudTh> <MudTh>@T("Dimension", "Dimension")</MudTh>
<MudTh>@T("Quelle", "Source")</MudTh> <MudTh>@T("Quelle", "Source")</MudTh>
</HeaderContent> </HeaderContent>
<RowTemplate> <RowTemplate>
<MudTd>@T(context.TitleDe, context.TitleEn)</MudTd> <MudTd>@T(context.LabelDe, context.LabelEn)</MudTd>
<MudTd><code>@context.Measure</code></MudTd> <MudTd><strong>@context.Value</strong></MudTd>
<MudTd>@context.Dimension</MudTd> <MudTd>@context.Dimension</MudTd>
<MudTd> <MudTd>
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="@(context.Source == "PBIX" ? Color.Primary : Color.Secondary)"> <MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="@ResolveSourceColor(context.Source)">
@context.Source @context.Source
</MudChip> </MudChip>
</MudTd> </MudTd>
@@ -30,13 +76,76 @@
[Parameter, EditorRequired] public string TitleEn { get; set; } = string.Empty; [Parameter, EditorRequired] public string TitleEn { get; set; } = string.Empty;
[Parameter, EditorRequired] public string DescriptionDe { get; set; } = string.Empty; [Parameter, EditorRequired] public string DescriptionDe { get; set; } = string.Empty;
[Parameter, EditorRequired] public string DescriptionEn { get; set; } = string.Empty; [Parameter, EditorRequired] public string DescriptionEn { get; set; } = string.Empty;
[Parameter, EditorRequired] public IReadOnlyList<PurchasingAnalysisRow> Rows { get; set; } = []; [Parameter, EditorRequired] public string ChartTitleDe { get; set; } = string.Empty;
[Parameter, EditorRequired] public string ChartTitleEn { get; set; } = string.Empty;
[Parameter, EditorRequired] public IReadOnlyList<PurchasingSectionKpi> Kpis { get; set; } = [];
[Parameter, EditorRequired] public IReadOnlyList<PurchasingSectionChartRow> ChartRows { get; set; } = [];
[Parameter, EditorRequired] public IReadOnlyList<PurchasingSectionStatusRow> StatusRows { get; set; } = [];
[Parameter, EditorRequired] public IReadOnlyList<PurchasingSectionDetailRow> DetailRows { get; set; } = [];
private string T(string german, string english) => UiText.Text(german, english); private string T(string german, string english) => UiText.Text(german, english);
private static string BuildWidth(double percent)
=> Math.Clamp(percent, 3d, 100d).ToString("0.##", System.Globalization.CultureInfo.InvariantCulture);
private static Color ResolveSourceColor(string source)
=> source.Equals("SAP live", StringComparison.OrdinalIgnoreCase)
? Color.Success
: source.Equals("Wartet auf SAP", StringComparison.OrdinalIgnoreCase)
? Color.Warning
: Color.Primary;
} }
<style> <style>
.purchasing-section-muted { .purchasing-section-muted {
color: var(--mud-palette-text-secondary); color: var(--mud-palette-text-secondary);
} }
.purchasing-section-kpi {
min-height: 104px;
}
.purchasing-section-panel {
min-height: 240px;
}
.purchasing-bars {
display: grid;
gap: 10px;
}
.purchasing-bar-row,
.purchasing-status-row {
display: grid;
grid-template-columns: minmax(120px, 1fr) 2fr auto;
gap: 10px;
align-items: center;
}
.purchasing-status-row {
grid-template-columns: 28px minmax(120px, 1fr) auto;
padding: 9px 0;
border-bottom: 1px solid var(--mud-palette-lines-default);
}
.purchasing-status-row:last-child {
border-bottom: 0;
}
.purchasing-bar-track {
height: 24px;
background: rgba(0,0,0,.08);
border-radius: 4px;
overflow: hidden;
}
.purchasing-bar-fill {
height: 100%;
border-radius: 4px;
min-width: 26px;
}
.purchasing-bar-value {
font-weight: 700;
text-align: right;
}
</style> </style>
@@ -0,0 +1,11 @@
using MudBlazor;
namespace TrafagSalesExporter.Models;
public sealed record PurchasingSectionKpi(string LabelDe, string LabelEn, string Value, string DetailDe, string DetailEn);
public sealed record PurchasingSectionChartRow(string Label, string Value, double Percent, string Color);
public sealed record PurchasingSectionStatusRow(string LabelDe, string LabelEn, string Value, string Icon, Color Color);
public sealed record PurchasingSectionDetailRow(string LabelDe, string LabelEn, string Value, string Dimension, string Source);