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>
|
||||||
</div>
|
</div>
|
||||||
} @else if (userStats(); as stats) {
|
} @else if (userStats(); as stats) {
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
<div class="space-y-6">
|
||||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
||||||
<p class="text-sm text-slate-500">Meine Zählpunkte</p>
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
|
||||||
<p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.totalMeteringPoints }}</p>
|
<p class="text-sm text-slate-500">Meine Zählpunkte</p>
|
||||||
</div>
|
<p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.totalMeteringPoints }}</p>
|
||||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
|
</div>
|
||||||
<p class="text-sm text-slate-500">Aktive Mitgliedschaften</p>
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
|
||||||
<p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.activeMemberships }}</p>
|
<p class="text-sm text-slate-500">Aktive Mitgliedschaften</p>
|
||||||
</div>
|
<p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.activeMemberships }}</p>
|
||||||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
|
</div>
|
||||||
<p class="text-sm text-slate-500">Offene Anfragen</p>
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
|
||||||
<p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.pendingMemberships }}</p>
|
<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>
|
</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>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import {Component, inject, OnInit, signal} from '@angular/core';
|
import {Component, inject, OnInit, signal} from '@angular/core';
|
||||||
|
import {NgxEchartsDirective} from 'ngx-echarts';
|
||||||
import {AuthService} from '../../services/auth';
|
import {AuthService} from '../../services/auth';
|
||||||
import {AdminStats, DashboardService, UserStats} from '../../services/dashboard';
|
import {AdminStats, DashboardService, MeteringDataOverview, UserStats} from '../../services/dashboard';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dashboard-overview',
|
selector: 'app-dashboard-overview',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
|
imports: [NgxEchartsDirective],
|
||||||
templateUrl: './dashboard-overview.html'
|
templateUrl: './dashboard-overview.html'
|
||||||
})
|
})
|
||||||
export class DashboardOverview implements OnInit {
|
export class DashboardOverview implements OnInit {
|
||||||
@ -17,6 +19,10 @@ export class DashboardOverview implements OnInit {
|
|||||||
isLoading = signal(true);
|
isLoading = signal(true);
|
||||||
error = signal<string | null>(null);
|
error = signal<string | null>(null);
|
||||||
|
|
||||||
|
meteringData = signal<MeteringDataOverview | null>(null);
|
||||||
|
chartOptions = signal<any>(null);
|
||||||
|
selectedDays = signal(30);
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
if (this.role === 'ADMIN') {
|
if (this.role === 'ADMIN') {
|
||||||
this.dashboardService.getAdminStats().subscribe({
|
this.dashboardService.getAdminStats().subscribe({
|
||||||
@ -28,7 +34,11 @@ export class DashboardOverview implements OnInit {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.dashboardService.getUserStats().subscribe({
|
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: () => {
|
error: () => {
|
||||||
this.isLoading.set(false);
|
this.isLoading.set(false);
|
||||||
this.error.set('Fehler beim Laden der Statistiken.');
|
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();
|
localStorage.clear();
|
||||||
this.isLoggedInSubject.next(false);
|
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 {inject, Injectable} from '@angular/core';
|
||||||
import {HttpClient} from '@angular/common/http';
|
import {HttpClient, HttpParams} from '@angular/common/http';
|
||||||
import {Observable} from 'rxjs';
|
import {Observable} from 'rxjs';
|
||||||
import {environment} from '../../environments/environment';
|
import {environment} from '../../environments/environment';
|
||||||
|
|
||||||
@ -16,6 +16,23 @@ export interface UserStats {
|
|||||||
pendingMemberships: number;
|
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'})
|
@Injectable({providedIn: 'root'})
|
||||||
export class DashboardService {
|
export class DashboardService {
|
||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
@ -28,4 +45,11 @@ export class DashboardService {
|
|||||||
getUserStats(): Observable<UserStats> {
|
getUserStats(): Observable<UserStats> {
|
||||||
return this.http.get<UserStats>(`${this.apiUrl}/user`);
|
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