feat(frontend): add metering data chart to dashboard overview with ECharts

This commit is contained in:
Bernhard Müller 2026-07-23 15:02:51 +02:00
parent a8686718db
commit 4fdc2d5f7a
4 changed files with 162 additions and 15 deletions

View File

@ -27,6 +27,7 @@
</div>
</div>
} @else if (userStats(); as stats) {
<div class="space-y-6">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6">
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
<p class="text-sm text-slate-500">Meine Zählpunkte</p>
@ -41,4 +42,37 @@
<p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.pendingMemberships }}</p>
</div>
</div>
@if (meteringData(); as data) {
@if (data.meteringPoints.length > 0) {
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold text-slate-800">Verbrauch & Erzeugung</h2>
<div class="flex gap-2">
<button
(click)="setRange(7)"
[class]="selectedDays() === 7 ? 'px-3 py-2 text-sm rounded-lg bg-emerald-600 text-white' : 'px-3 py-2 text-sm rounded-lg border border-slate-300 hover:bg-slate-50'">
7 Tage
</button>
<button
(click)="setRange(30)"
[class]="selectedDays() === 30 ? 'px-3 py-2 text-sm rounded-lg bg-emerald-600 text-white' : 'px-3 py-2 text-sm rounded-lg border border-slate-300 hover:bg-slate-50'">
30 Tage
</button>
<button
(click)="setRange(90)"
[class]="selectedDays() === 90 ? 'px-3 py-2 text-sm rounded-lg bg-emerald-600 text-white' : 'px-3 py-2 text-sm rounded-lg border border-slate-300 hover:bg-slate-50'">
90 Tage
</button>
</div>
</div>
@if (chartOptions()) {
<div echarts [options]="chartOptions()" class="h-[400px]"></div>
} @else {
<p class="text-slate-500 text-center py-8">Keine Daten im ausgewählten Zeitraum vorhanden.</p>
}
</div>
}
}
</div>
}

View File

@ -1,10 +1,12 @@
import {Component, inject, OnInit, signal} from '@angular/core';
import {NgxEchartsDirective} from 'ngx-echarts';
import {AuthService} from '../../services/auth';
import {AdminStats, DashboardService, UserStats} from '../../services/dashboard';
import {AdminStats, DashboardService, MeteringDataOverview, UserStats} from '../../services/dashboard';
@Component({
selector: 'app-dashboard-overview',
standalone: true,
imports: [NgxEchartsDirective],
templateUrl: './dashboard-overview.html'
})
export class DashboardOverview implements OnInit {
@ -17,6 +19,10 @@ export class DashboardOverview implements OnInit {
isLoading = signal(true);
error = signal<string | null>(null);
meteringData = signal<MeteringDataOverview | null>(null);
chartOptions = signal<any>(null);
selectedDays = signal(30);
ngOnInit() {
if (this.role === 'ADMIN') {
this.dashboardService.getAdminStats().subscribe({
@ -28,7 +34,11 @@ export class DashboardOverview implements OnInit {
});
} else {
this.dashboardService.getUserStats().subscribe({
next: stats => { this.userStats.set(stats); this.isLoading.set(false); },
next: stats => {
this.userStats.set(stats);
this.isLoading.set(false);
this.loadMeteringData();
},
error: () => {
this.isLoading.set(false);
this.error.set('Fehler beim Laden der Statistiken.');
@ -36,4 +46,72 @@ export class DashboardOverview implements OnInit {
});
}
}
setRange(days: number) {
this.selectedDays.set(days);
this.loadMeteringData();
}
private loadMeteringData() {
const to = new Date();
const from = new Date();
from.setDate(from.getDate() - this.selectedDays());
const fromStr = from.toISOString();
const toStr = to.toISOString();
this.dashboardService.getMeteringDataOverview(fromStr, toStr).subscribe({
next: (data) => {
this.meteringData.set(data);
this.buildChartOptions(data);
},
error: () => {}
});
}
private buildChartOptions(data: MeteringDataOverview) {
if (!data || data.timeSeries.length === 0) {
this.chartOptions.set(null);
return;
}
const dates = data.timeSeries.map(e => {
const d = new Date(e.timestamp);
return d.toLocaleDateString('de-AT', {day: '2-digit', month: '2-digit'});
});
const series = data.meteringPoints.map(mp => ({
name: `${mp.atNumber.slice(-8)} (${mp.type})`,
type: 'bar' as const,
stack: 'total',
data: data.timeSeries.map(e => e.values[mp.atNumber] || 0),
itemStyle: {color: mp.type === 'CONSUMER' ? '#3b82f6' : '#22c55e'}
}));
this.chartOptions.set({
tooltip: {
trigger: 'axis',
formatter: (params: any[]) => {
const label = params[0]?.axisValue || '';
let html = `<strong>${label}</strong><br/>`;
params.forEach((p: any) => {
html += `${p.marker} ${p.seriesName}: ${p.value?.toFixed(2)} kWh<br/>`;
});
return html;
}
},
legend: {
data: series.map(s => s.name),
top: 0
},
grid: {left: '3%', right: '4%', bottom: '3%', containLabel: true},
xAxis: {
type: 'category',
data: dates,
axisLabel: {rotate: dates.length > 15 ? 45 : 0}
},
yAxis: {type: 'value', name: 'kWh'},
series
});
}
}

View File

@ -42,4 +42,15 @@ export class AuthService {
localStorage.clear();
this.isLoggedInSubject.next(false);
}
getCurrentUserId(): string | null {
const token = localStorage.getItem('token');
if (!token) return null;
try {
const payload = JSON.parse(atob(token.split('.')[1]));
return payload.userId || null;
} catch {
return null;
}
}
}

View File

@ -1,5 +1,5 @@
import {inject, Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {HttpClient, HttpParams} from '@angular/common/http';
import {Observable} from 'rxjs';
import {environment} from '../../environments/environment';
@ -16,6 +16,23 @@ export interface UserStats {
pendingMemberships: number;
}
export interface MeteringPointSummary {
id: string;
atNumber: string;
type: string;
totalKwh: number;
}
export interface TimeSeriesEntry {
timestamp: string;
values: { [atNumber: string]: number };
}
export interface MeteringDataOverview {
meteringPoints: MeteringPointSummary[];
timeSeries: TimeSeriesEntry[];
}
@Injectable({providedIn: 'root'})
export class DashboardService {
private http = inject(HttpClient);
@ -28,4 +45,11 @@ export class DashboardService {
getUserStats(): Observable<UserStats> {
return this.http.get<UserStats>(`${this.apiUrl}/user`);
}
getMeteringDataOverview(from: string, to: string): Observable<MeteringDataOverview> {
const params = new HttpParams()
.set('from', from)
.set('to', to);
return this.http.get<MeteringDataOverview>(`${this.apiUrl}/user/metering-data/overview`, {params});
}
}