diff --git a/TrafagSalesExporter/Components/Pages/PurchasingDashboard.razor b/TrafagSalesExporter/Components/Pages/PurchasingDashboard.razor index 37344b7..a548237 100644 --- a/TrafagSalesExporter/Components/Pages/PurchasingDashboard.razor +++ b/TrafagSalesExporter/Components/Pages/PurchasingDashboard.razor @@ -191,12 +191,12 @@ private double _purchasing3dFactor = 1d; private double _purchasing3dLabelScale = 1.5d; - private readonly List KpiCards = + private IReadOnlyList 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) + new("Spend total", "Total spend", FormatChf(Purchasing3dBaseRows.Sum(x => x.Spend)), "Simulationsbasis bis SAP-Liveimport", "Simulation basis until SAP live import", Icons.Material.Filled.Payments, Color.Primary), + new("Offene Bestellungen", "Open orders", FormatChf(Purchasing3dBaseRows.Sum(x => x.OpenValue)), "Wert offen aus Simulationsbasis", "Open value from simulation basis", Icons.Material.Filled.PendingActions, Color.Warning), + new("Kontrakte", "Contracts", FormatChf(Purchasing3dBaseRows.Sum(x => x.ContractValue)), "Restverpflichtungen aus Simulationsbasis", "Remaining commitments from simulation basis", Icons.Material.Filled.Assignment, Color.Info), + new("Lieferantenperformance", "Supplier performance", $"{Purchasing3dBaseRows.Average(x => x.SupplierScore):N1}%", "Durchschnitt Score aus Simulationsbasis", "Average score from simulation basis", Icons.Material.Filled.Verified, Color.Success) ]; private readonly List AnalysisAxes = @@ -290,6 +290,7 @@ } private string T(string german, string english) => UiText.Text(german, english); + private static string FormatChf(double value) => $"CHF {value:N0}"; private async Task SetPurchasing3dIndicator(string value) { diff --git a/TrafagSalesExporter/Services/DashboardPageService.cs b/TrafagSalesExporter/Services/DashboardPageService.cs index 7f62922..158cd63 100644 --- a/TrafagSalesExporter/Services/DashboardPageService.cs +++ b/TrafagSalesExporter/Services/DashboardPageService.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using System.Data; using TrafagSalesExporter.Data; using TrafagSalesExporter.Models; @@ -28,14 +29,7 @@ public sealed class DashboardPageService : IDashboardPageService .GroupBy(l => l.SiteId) .Select(g => g.OrderByDescending(l => l.Timestamp).First()) .ToListAsync(); - var appLogs = await db.AppEventLogs - .Where(l => l.SiteId != null) - .OrderByDescending(l => l.Timestamp) - .Take(1000) - .ToListAsync(); - var latestAppLogsBySite = appLogs - .GroupBy(l => l.SiteId!.Value) - .ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.Timestamp).First()); + var latestAppLogsBySite = await LoadLatestAppLogsBySiteAsync(db); var rows = sites.Select(s => { @@ -86,6 +80,75 @@ public sealed class DashboardPageService : IDashboardPageService }; } + private static async Task> LoadLatestAppLogsBySiteAsync(AppDbContext db) + { + var connection = db.Database.GetDbConnection(); + var shouldClose = connection.State != ConnectionState.Open; + if (shouldClose) + await connection.OpenAsync(); + + try + { + await using var command = connection.CreateCommand(); + command.CommandText = """ +SELECT Id, Timestamp, Level, Category, SiteId, Land, Message, Details +FROM AppEventLogs +WHERE SiteId IS NOT NULL +ORDER BY Id DESC +LIMIT 1000; +"""; + + var logs = new List(); + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + if (!TryReadInt(reader["SiteId"], out var siteId)) + continue; + + if (!DateTime.TryParse(Convert.ToString(reader["Timestamp"]), out var timestamp)) + continue; + + logs.Add(new AppEventLog + { + Id = TryReadInt(reader["Id"], out var id) ? id : 0, + Timestamp = timestamp, + Level = Convert.ToString(reader["Level"]) ?? string.Empty, + Category = Convert.ToString(reader["Category"]) ?? string.Empty, + SiteId = siteId, + Land = Convert.ToString(reader["Land"]) ?? string.Empty, + Message = Convert.ToString(reader["Message"]) ?? string.Empty, + Details = Convert.ToString(reader["Details"]) ?? string.Empty + }); + } + + return logs + .GroupBy(l => l.SiteId!.Value) + .ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.Timestamp).First()); + } + finally + { + if (shouldClose) + await connection.CloseAsync(); + } + } + + private static bool TryReadInt(object? value, out int number) + { + if (value is int intValue) + { + number = intValue; + return true; + } + + if (value is long longValue && longValue >= int.MinValue && longValue <= int.MaxValue) + { + number = (int)longValue; + return true; + } + + return int.TryParse(Convert.ToString(value), out number); + } + private static List BuildReadinessWarnings(List activeSites, List sourceSystems) { var warnings = new List(); diff --git a/TrafagSalesExporter/wwwroot/js/finance3d.js b/TrafagSalesExporter/wwwroot/js/finance3d.js index f6670dc..ab3a3be 100644 --- a/TrafagSalesExporter/wwwroot/js/finance3d.js +++ b/TrafagSalesExporter/wwwroot/js/finance3d.js @@ -515,7 +515,7 @@ window.trafagFinance3d = { render: function (canvas, rows, options) { - if (!canvas) return; + if (!canvas || typeof canvas.addEventListener !== "function") return; const normalizedRows = normalizeRows(rows); if (normalizedRows.length === 0) return; const existing = stateByCanvas.get(canvas);