@inject NavigationManager Nav @* The meter's Analysis tab (brief §7.2): the shared period toolbar with CSV export, the period's quantity (normalized unit, D-20) and cost (with its rule named, or why there is none), the projection kept apart from the actual (D-09), the full-size chart with the comparison overlay, the accessible table, a click on a bucket drilling down (D-51), and what qualifies the figures: coverage, resolution, opening balance, rows after now, freshness, and the events and tariff changes inside the range. A virtual meter gets the same from its formula, with its sources' contributions. *@ @if (r.MeterId == Detail.Id) { var s = r.Series; @if (s is null) { @Loc.F(S.MeterDetail_NotFound, Detail.Id) } else if (r.Quantities.Refusal != AnalysisRefusal.None) { @* The toolbar above says why (too many points) and offers the coarser bucket. *@ } else { @if (s.IsPending) { } else if (s.Total.Status == BucketStatus.Invalid) { @Loc.F(S.MeterDetail_CalculationNotEvaluable, (s.Virtual?.Status ?? VirtualMeterStatus.Invalid).Display())
@S.MeterDetail_EditCalculation @S.MeterDetail_ShowCalculation
} else if (r.Quantities.NotYetOccurred || s.Total.Status == BucketStatus.Missing) { @if (s.Availability is null) { @NextStepText
@if (MeterEventRules.TakesReadings(Detail.Mode)) { @S.MeterDetail_AddFirstReading @S.MeterDetail_ConnectSource } else if (Detail.Mode == MeterMode.ConsumableBalance) { @S.MeterDetail_RecordTankLevel } else if (Detail.IsVirtual) { @S.MeterDetail_ShowCalculation }
}
@if (Detail.IsVirtual) { } } else { @if (r.Projection is { } projection) { } @if (r.Cost is { } cost) { @if (r.IsCosted) { } else { @S.MeterDetail_CostTitle
@MeterCostRule.None.Display()
@((cost.Meter?.NotCosted ?? MeterNotCostedReason.None).Display())
}
}
@* A click leads somewhere or is not offered (D-51): the chart is clickable, the table has its drill column and the hint shows only when some bucket opens something. *@ var drills = r.Quantities.Plan.Buckets.Any(b => DrillHref(b, r, s) is not null); @if (drills) { @DrillHint(s) } else { } @if (Detail.IsVirtual) { @S.MeterDetail_SourcesOfCalculation } } } }
@code { /// The meter. [Parameter, EditorRequired] public MeterDetailView Detail { get; set; } = null!; /// The page's analysis state. [Parameter, EditorRequired] public AnalysisQuery Query { get; set; } = null!; /// The page's analysis load (owned by the page, so switching tabs never reloads it, D-46). [Parameter, EditorRequired] public LoadState State { get; set; } = null!; /// A toolbar choice: the page writes it into its address (replace). [Parameter] public EventCallback OnQueryChanged { get; set; } [Parameter] public EventCallback OnRetry { get; set; } [Parameter] public EventCallback OnAddReading { get; set; } [Parameter] public EventCallback OnRecordEvent { get; set; } /// Opens the shared meter editor (install date, calculation). [Parameter] public EventCallback OnEdit { get; set; } private AnalysisDefaults Defaults => MeterAnalysisLoader.DefaultsFor(Detail.Id); /// The committed value when it is this meter's. private MeterAnalysisView? Current => State.Value is { } value && value.MeterId == Detail.Id ? value : null; private string ExportHref => AnalysisLinks.Export(MeterAnalysisLoader.ForMeter(Query, Detail.Id)); private string? ComparisonCaption => Query.Comparison.Kind == ComparisonKind.None ? null : Query.Comparison.Display(); private string NextStepText => Detail.Mode switch { MeterMode.ConsumableBalance => S.MeterDetail_NextStepTank, MeterMode.Virtual => S.MeterDetail_NextStepVirtual, _ => S.MeterDetail_NextStepCounter, }; /// /// The readers' problems for the attention list. Rows recorded after now are explained with their dates and amount in /// the coverage section below, so they are not listed twice. /// private static IEnumerable ProblemsOf(MeterAnalysisView view) => view.Quantities.Problems.Concat(view.Cost?.QuantityProblems ?? []) .Where(p => p.Kind != AnalysisProblemKind.RecordedAfterNow); private string CostRuleText(Infrastructure.Costing.CostAnalysis cost) => Loc.F(S.MeterDetail_CostRule, (cost.Meter?.Rule ?? MeterCostRule.None).Display()); private static string ChartTitle(MeterAnalysisView view, AnalysisSeries series) => view.ShowsCost ? Loc.F(S.MeterDetail_CostOf, AnalysisChartSeries.NameOf(series)) : Loc.F(S.MeterDetail_ChartTitle, series.Kind.Display(), AnalysisChartSeries.NameOf(series)); private string DrillHint(AnalysisSeries series) => Detail.IsVirtual ? S.MeterDetail_DrillHintVirtual : S.MeterDetail_DrillHint; private string? LatestHref(AnalysisSeries series) => AnalysisNavigation.LatestData(Query, series.Availability) is { } latest ? MeterLinks.Analysis(Detail.Id, latest) : null; /// /// Where a bucket leads (D-51, ): the next finer size the data resolves; otherwise a physical /// meter's records of the bucket, or a virtual meter's own analysis over just that bucket, whose source contributions /// link on to each source's records — never a dead end. /// private string? DrillHref(AnalysisBucket bucket, MeterAnalysisView view, AnalysisSeries series) => MeterDrill.Href(Detail.Id, Detail.IsVirtual, Query, view.Period, bucket, series.Resolution); /// A chart click drills down, pushing a history entry so Back returns to the range it came from (D-46). private void Drill(AnalysisBucket bucket, MeterAnalysisView view, AnalysisSeries series) { if (DrillHref(bucket, view, series) is { } href) { Nav.NavigateTo(href); } } /// The buckets are finer than the data: apply the interval that shows it (the page replaces its address). private Task UseBucketAsync(BucketSize size) => OnQueryChanged.InvokeAsync(Query.WithBucket(size)); }