fixes initial loading of stats page

This commit is contained in:
2026-06-28 11:23:34 +00:00
parent db2ebd23fb
commit f3ba0bf5ba
@@ -1,9 +1,10 @@
import { Component, OnInit, inject, AfterViewChecked, OnDestroy, ViewChild, ElementRef, NgZone } from '@angular/core';
import { ChangeDetectorRef, Component, inject, AfterViewChecked, OnDestroy, OnInit, ViewChild, ElementRef, NgZone } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Chart, registerables } from 'chart.js';
import { BaseChartDirective } from 'ng2-charts';
import L from 'leaflet';
import { firstValueFrom } from 'rxjs';
import { StatsService } from '../services/stats.service';
import {
@@ -27,6 +28,7 @@ Chart.register(...registerables);
})
export class AdminStatsDashboardComponent implements OnInit, AfterViewChecked, OnDestroy {
private readonly statsService: StatsService = inject(StatsService);
private readonly cd = inject(ChangeDetectorRef);
private readonly ngZone = inject(NgZone);
@ViewChild('mapContainer', { static: false }) mapContainerRef!: ElementRef;
@@ -82,10 +84,13 @@ export class AdminStatsDashboardComponent implements OnInit, AfterViewChecked, O
// doesn't exist yet. ngAfterViewChecked detects when it appears.
if (this.mapInitialized) return;
if (!this.mapContainerRef) return;
// Verify the native element actually exists in the DOM (not just that
// the query ref was created — the @if block must have rendered too)
if (!this.mapContainerRef.nativeElement?.isConnected) return;
this.mapInitialized = true;
this.ngZone.runOutsideAngular(() => {
// Defer one frame so the browser has laid out the container
// Defer two frames so the browser has laid out the container
requestAnimationFrame(() => {
requestAnimationFrame(() => this._initMap());
});
@@ -187,22 +192,34 @@ export class AdminStatsDashboardComponent implements OnInit, AfterViewChecked, O
const { start, end } = this.getDates();
try {
const [summary, timeSeries, geoStats] = await Promise.all([
this._firstValue(this.statsService.getSummary(start, end)),
this._firstValue(this.statsService.getTimeSeries(start, end)),
this._firstValue(this.statsService.getGeoStats(start, end)),
firstValueFrom(this.statsService.getSummary(start, end)),
firstValueFrom(this.statsService.getTimeSeries(start, end)),
firstValueFrom(this.statsService.getGeoStats(start, end)),
]);
this.summary = summary;
this.timeSeries = timeSeries;
this.geoStats = geoStats;
// Angular 17+ uses fetch-based HTTP whose promise resolutions run
// outside the zone. Re-enter the zone and markForCheck() to force
// change detection through the OnPush parent chain.
this.ngZone.run(() => {
this.summary = summary;
this.timeSeries = timeSeries;
this.geoStats = geoStats;
this._updateChart();
this._updateMapMarkers();
this._updateChart();
this._updateMapMarkers();
this.cd.markForCheck();
});
} catch (err) {
console.error('Failed to load stats:', err);
this.error = 'Failed to load stats data. Please check your connection and try again.';
this.ngZone.run(() => {
this.error = 'Failed to load stats data. Please check your connection and try again.';
this.cd.markForCheck();
});
} finally {
this.loading = false;
this.ngZone.run(() => {
this.loading = false;
this.cd.markForCheck();
});
}
}
@@ -252,11 +269,6 @@ export class AdminStatsDashboardComponent implements OnInit, AfterViewChecked, O
});
}
private _firstValue<T>(obs: import('rxjs').Observable<T>): Promise<T> {
return new Promise<T>((resolve, reject) => {
obs.subscribe({ next: (v) => resolve(v), error: reject });
});
}
// ── Chart ───────────────────────────────────────────────