@page "/meters" @using MeterVault.App.Energy @using MeterVault.App.Components.Shared.MeterLists @using MeterVault.Core.Analysis @using MeterVault.Infrastructure.Analysis @using Microsoft.EntityFrameworkCore @inject Microsoft.EntityFrameworkCore.IDbContextFactory DbFactory @inject ISnackbar Snackbar @inject IDialogService DialogService @inject NavigationManager Nav @inject InstanceClock Clock @inject AnalysisPeriods Periods @inject AnalysisReader Reader @inject MeterVault.Infrastructure.Analysis.VirtualMeterService VirtualMeters @inject NavState NavState @inject ILogger Logger @implements IDisposable @* Every meter (brief §7.3, §3.2): the shared meter list, grouped by energy type with a type filter and a search, each meter with what it measured in the chosen period — a calculated meter by its formula, a meter without data in words — and how it counts. Names open the meter's Analysis tab for the same period; the quick entry, edit and delete stay one click away. Deleting names the calculated meters that depend on the meter first (D-33). *@ @S.Meters_AddMeter @if (_query is not null) { } @S.Meters_EmptyBefore @S.Nav_Import @S.Meters_EmptyAfter @code { private readonly LoadSequencer _loads = new(); private readonly LoadState _state = new(); private AnalysisQuery? _query; private MeterEditor? _editor; /// What the list shows for one period: the resolved period and a row per meter. private sealed record MeterListState(ResolvedPeriod Period, IReadOnlyList Rows); protected override void OnInitialized() => Nav.LocationChanged += OnLocationChanged; protected override Task OnParametersSetAsync() => SyncAsync(); private void OnLocationChanged(object? sender, LocationChangedEventArgs e) => _ = InvokeAsync(async () => { // Only this page's own address: a link away fires this too, just before the page goes. var path = Nav.ToBaseRelativePath(e.Location); var end = path.IndexOfAny(['?', '#']); if (!string.Equals((end >= 0 ? path[..end] : path).TrimEnd('/'), "meters", StringComparison.OrdinalIgnoreCase)) { return; } await SyncAsync(); StateHasChanged(); }); private async Task SyncAsync() { var query = AnalysisQuery.Parse(Nav.Uri, AnalysisDefaults.History); if (query == _query) { return; } _query = query; await LoadAsync(query); } private Task Retry() => _query is null ? Task.CompletedTask : LoadAsync(_query); private Task LoadAsync(AnalysisQuery query) => _loads.RunAsync(_state, token => ReadAsync(query, token), Logger); /// /// The meters and their period totals: one portfolio read with every meter's own series, in year buckets — only the /// totals are shown, and they do not depend on the buckets — without a comparison. /// private async Task ReadAsync(AnalysisQuery query, CancellationToken token) { var period = await Periods.ResolveAsync(query.WithMetric(null).WithScope(QueryScope.Portfolio), Clock.Now, token); List meters; await using (var db = await DbFactory.CreateDbContextAsync(token)) { meters = await MeterFacts.LoadAsync(db, null, token); } AnalysisResult? result = null; if (meters.Count > 0) { var request = new AnalysisRequest(AnalysisScope.Portfolio, period) { Bucket = BucketSize.Year, IncludeMeterSeries = true }; result = await Reader.ReadAsync(request, token); } return new MeterListState(period, MeterListRows.Build(meters, result)); } private void OnQueryChanged(AnalysisQuery query) => AnalysisNavigation.Replace(Nav, query, AnalysisDefaults.History); /// A new meter goes straight to its own page, where adding readings or a source is the next step. private async Task OnSavedAsync((int MeterId, bool Created) saved) { if (saved.Created) { Nav.NavigateTo(MeterLinks.Detail(saved.MeterId)); return; } await Retry(); } private async Task DeleteAsync(MeterFacts meter) { await using var db = await DbFactory.CreateDbContextAsync(); var readings = await db.Readings.CountAsync(r => r.MeterId == meter.Id); var consumption = await db.Consumption.CountAsync(c => c.MeterId == meter.Id); var message = readings + consumption > 0 ? Loc.F(S.Meters_DeleteConfirmWithData, meter.Name, readings, consumption) : Loc.F(S.Meters_DeleteConfirm, meter.Name); // Virtual meters whose formula reads this one break with it (D-33): name them before anything is deleted. var dependents = await VirtualMeters.GetDependentsAsync(meter.Id); if (dependents.Count > 0) { message += " " + Loc.F(S.Meters_DeleteVirtualDependents, string.Join(", ", dependents.Select(d => d.Name))); } if (!await Confirm.DeleteAsync(DialogService, S.Meters_DeleteTitle, message)) { return; } // Readings and consumption first (restricted keys), and its meter-scoped prices, which no key ties to it; // events, sources, tank and members cascade. await MeterVault.Infrastructure.Persistence.EntityDeletion.DeleteMeterAsync(db, meter.Id); Snackbar.Add(S.Common_Deleted, Severity.Success); NavState.NotifyMetersChanged(); await Retry(); } public void Dispose() { Nav.LocationChanged -= OnLocationChanged; _loads.Dispose(); } }