feat(kpi): K14 — Site Calls trend charts

This commit is contained in:
Joseph Doherty
2026-06-17 20:30:36 -04:00
parent 0dc819f191
commit 4a88355098
3 changed files with 262 additions and 0 deletions
@@ -5,9 +5,12 @@
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit
@using ZB.MOM.WW.ScadaBridge.Communication
@using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
@inject CommunicationService CommunicationService
@inject ISiteRepository SiteRepository
@inject IDialogService Dialog
@inject IKpiHistoryQueryService KpiHistory
@inject ILogger<SiteCallsReport> Logger
<div class="container-fluid mt-3">
@@ -230,6 +233,59 @@
</div>
</div>
}
@* ── Trends (K14: collapsible KPI-history charts) ──
A best-effort section — each chart's GetSeriesAsync is wrapped so a KPI
backend hiccup renders the chart unavailable rather than breaking the
grid above. The series load lazily on first expand (and re-load on the
24h/7d window toggle), so the page's primary job (the call list) is never
blocked on the history query. *@
<div class="mt-4" data-test="site-calls-trends">
<div class="d-flex justify-content-between align-items-center mb-2">
<button type="button" class="btn btn-link text-decoration-none px-0"
@onclick="ToggleTrendsAsync"
aria-expanded="@(_trendsExpanded ? "true" : "false")"
data-test="site-calls-trends-toggle">
<span class="me-1">@(_trendsExpanded ? "▾" : "▸")</span>
<span class="h5 mb-0">Trends</span>
</button>
@if (_trendsExpanded)
{
<div class="btn-group btn-group-sm" role="group" aria-label="Trend window">
<button type="button"
class="btn @(_windowHours == 24 ? "btn-primary" : "btn-outline-secondary")"
@onclick="() => SetWindowAsync(24)" disabled="@_trendsLoading">24h</button>
<button type="button"
class="btn @(_windowHours == 168 ? "btn-primary" : "btn-outline-secondary")"
@onclick="() => SetWindowAsync(168)" disabled="@_trendsLoading">7d</button>
</div>
}
</div>
@if (_trendsExpanded)
{
<div class="row g-3">
<div class="col-lg-4 col-md-6">
<KpiTrendChart Title="Buffered"
Points="@_bufferedSeries"
IsAvailable="@_bufferedAvailable"
ErrorMessage="@_bufferedError" />
</div>
<div class="col-lg-4 col-md-6">
<KpiTrendChart Title="Parked"
Points="@_parkedSeries"
IsAvailable="@_parkedAvailable"
ErrorMessage="@_parkedError" />
</div>
<div class="col-lg-4 col-md-6">
<KpiTrendChart Title="Failed / interval"
Points="@_failedSeries"
IsAvailable="@_failedAvailable"
ErrorMessage="@_failedError" />
</div>
</div>
}
</div>
</div>
@* ── Row detail modal ── *@
@@ -3,8 +3,10 @@ using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.SiteCalls;
@@ -94,6 +96,29 @@ public partial class SiteCallsReport
private DateTime? _fromFilter;
private DateTime? _toFilter;
// ── Trends (K14) ──────────────────────────────────────────────────────────
// Collapsible KPI-history charts for the SiteCallAudit / Global series. The
// section is collapsed on init and the series load lazily on first expand
// (and on each 24h/7d window toggle), so the page's primary job — the call
// list — never waits on the history query. Each metric carries its own
// series + availability + error so one metric's failure leaves the other two
// charts intact; a GetSeriesAsync throw never breaks the grid above.
private bool _trendsExpanded;
private bool _trendsLoading;
private int _windowHours = 24;
private IReadOnlyList<KpiSeriesPoint>? _bufferedSeries;
private bool _bufferedAvailable = true;
private string? _bufferedError;
private IReadOnlyList<KpiSeriesPoint>? _parkedSeries;
private bool _parkedAvailable = true;
private string? _parkedError;
private IReadOnlyList<KpiSeriesPoint>? _failedSeries;
private bool _failedAvailable = true;
private string? _failedError;
private bool HasNextPage => _nextCursor is not null;
/// <inheritdoc />
@@ -521,4 +546,79 @@ public partial class SiteCallsReport
return Task.FromResult(_permittedSiteIds.Contains(resolved.Id));
}
// ── Trends (K14) ──────────────────────────────────────────────────────────
/// <summary>
/// Expand/collapse the Trends section. The series load on first expand (and
/// stay loaded across collapse/expand cycles); collapsing leaves the loaded
/// series in place so re-expanding is instant. A re-fetch only happens on the
/// window toggle.
/// </summary>
private async Task ToggleTrendsAsync()
{
_trendsExpanded = !_trendsExpanded;
if (_trendsExpanded && _bufferedSeries is null && _parkedSeries is null && _failedSeries is null)
{
await LoadTrendsAsync();
}
}
/// <summary>Switch the trend window (24h / 7d) and re-load the series.</summary>
private async Task SetWindowAsync(int windowHours)
{
if (_windowHours == windowHours)
{
return;
}
_windowHours = windowHours;
await LoadTrendsAsync();
}
/// <summary>
/// Load all three SiteCallAudit / Global trend series for the current window.
/// Best-effort: each <see cref="IKpiHistoryQueryService.GetSeriesAsync"/> is
/// wrapped independently so one metric's failure renders only that chart
/// unavailable, and a throw never propagates to break the Site Calls grid.
/// </summary>
private async Task LoadTrendsAsync()
{
_trendsLoading = true;
var toUtc = DateTime.UtcNow;
var fromUtc = toUtc - TimeSpan.FromHours(_windowHours);
(_bufferedSeries, _bufferedAvailable, _bufferedError) =
await LoadSeriesAsync("buffered", fromUtc, toUtc);
(_parkedSeries, _parkedAvailable, _parkedError) =
await LoadSeriesAsync("parked", fromUtc, toUtc);
(_failedSeries, _failedAvailable, _failedError) =
await LoadSeriesAsync("failedLastInterval", fromUtc, toUtc);
_trendsLoading = false;
}
/// <summary>
/// Fetch one SiteCallAudit / Global series, swallowing any failure into the
/// chart's unavailable state. Returns the points (null on failure), the
/// availability flag, and a short error message for the chart placeholder.
/// </summary>
private async Task<(IReadOnlyList<KpiSeriesPoint>? Points, bool Available, string? Error)>
LoadSeriesAsync(string metric, DateTime fromUtc, DateTime toUtc)
{
try
{
var points = await KpiHistory.GetSeriesAsync(
KpiSources.SiteCallAudit, metric, KpiScopes.Global, scopeKey: null, fromUtc, toUtc);
return (points, true, null);
}
catch (Exception ex)
{
// A KPI-history hiccup must never break the Site Calls page — the
// chart degrades to its unavailable placeholder instead.
Logger.LogWarning(ex, "Failed to load Site Calls trend series for metric {Metric}.", metric);
return (null, false, "Trend data unavailable.");
}
}
}