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 { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { Chart, registerables } from 'chart.js'; import { Chart, registerables } from 'chart.js';
import { BaseChartDirective } from 'ng2-charts'; import { BaseChartDirective } from 'ng2-charts';
import L from 'leaflet'; import L from 'leaflet';
import { firstValueFrom } from 'rxjs';
import { StatsService } from '../services/stats.service'; import { StatsService } from '../services/stats.service';
import { import {
@@ -27,6 +28,7 @@ Chart.register(...registerables);
}) })
export class AdminStatsDashboardComponent implements OnInit, AfterViewChecked, OnDestroy { export class AdminStatsDashboardComponent implements OnInit, AfterViewChecked, OnDestroy {
private readonly statsService: StatsService = inject(StatsService); private readonly statsService: StatsService = inject(StatsService);
private readonly cd = inject(ChangeDetectorRef);
private readonly ngZone = inject(NgZone); private readonly ngZone = inject(NgZone);
@ViewChild('mapContainer', { static: false }) mapContainerRef!: ElementRef; @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. // doesn't exist yet. ngAfterViewChecked detects when it appears.
if (this.mapInitialized) return; if (this.mapInitialized) return;
if (!this.mapContainerRef) 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.mapInitialized = true;
this.ngZone.runOutsideAngular(() => { 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(() => {
requestAnimationFrame(() => this._initMap()); requestAnimationFrame(() => this._initMap());
}); });
@@ -187,22 +192,34 @@ export class AdminStatsDashboardComponent implements OnInit, AfterViewChecked, O
const { start, end } = this.getDates(); const { start, end } = this.getDates();
try { try {
const [summary, timeSeries, geoStats] = await Promise.all([ const [summary, timeSeries, geoStats] = await Promise.all([
this._firstValue(this.statsService.getSummary(start, end)), firstValueFrom(this.statsService.getSummary(start, end)),
this._firstValue(this.statsService.getTimeSeries(start, end)), firstValueFrom(this.statsService.getTimeSeries(start, end)),
this._firstValue(this.statsService.getGeoStats(start, end)), firstValueFrom(this.statsService.getGeoStats(start, end)),
]); ]);
// 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.summary = summary;
this.timeSeries = timeSeries; this.timeSeries = timeSeries;
this.geoStats = geoStats; this.geoStats = geoStats;
this._updateChart(); this._updateChart();
this._updateMapMarkers(); this._updateMapMarkers();
this.cd.markForCheck();
});
} catch (err) { } catch (err) {
console.error('Failed to load stats:', err); console.error('Failed to load stats:', err);
this.ngZone.run(() => {
this.error = 'Failed to load stats data. Please check your connection and try again.'; this.error = 'Failed to load stats data. Please check your connection and try again.';
this.cd.markForCheck();
});
} finally { } finally {
this.ngZone.run(() => {
this.loading = false; 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 ─────────────────────────────────────────────── // ── Chart ───────────────────────────────────────────────