feat(frontend): add metering data chart to dashboard overview with ECharts
This commit is contained in:
parent
a8686718db
commit
4fdc2d5f7a
@ -27,18 +27,52 @@
|
||||
</div>
|
||||
</div>
|
||||
} @else if (userStats(); as stats) {
|
||||
<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>
|
||||
<p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.totalMeteringPoints }}</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
|
||||
<p class="text-sm text-slate-500">Aktive Mitgliedschaften</p>
|
||||
<p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.activeMemberships }}</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
|
||||
<p class="text-sm text-slate-500">Offene Anfragen</p>
|
||||
<p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.pendingMemberships }}</p>
|
||||
<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>
|
||||
<p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.totalMeteringPoints }}</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
|
||||
<p class="text-sm text-slate-500">Aktive Mitgliedschaften</p>
|
||||
<p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.activeMemberships }}</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
|
||||
<p class="text-sm text-slate-500">Offene Anfragen</p>
|
||||
<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>
|
||||
}
|
||||
|
||||
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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});
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user