diff --git a/src/app/admin/admin-stats-dashboard.component.ts b/src/app/admin/admin-stats-dashboard.component.ts index daaebaf..e2f9633 100644 --- a/src/app/admin/admin-stats-dashboard.component.ts +++ b/src/app/admin/admin-stats-dashboard.component.ts @@ -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(obs: import('rxjs').Observable): Promise { - return new Promise((resolve, reject) => { - obs.subscribe({ next: (v) => resolve(v), error: reject }); - }); - } // ── Chart ───────────────────────────────────────────────