fix(frontend): add missing provideEcharts() to app config causing empty dashboard chart

Root cause: ngx-echarts v18+ requires provideEcharts() in the app
config. Without it, NgxEchartsDirective silently fails to render
charts, causing the dashboard overview chart to appear empty.

Added provideEcharts() to app.config.ts and added
DashboardServiceIntegrationTest for the metering data overview endpoint.
This commit is contained in:
Bernhard Müller 2026-07-24 15:57:35 +02:00
parent 0ef4f94933
commit 6cb4ff7a4a
2 changed files with 140 additions and 1 deletions

View File

@ -0,0 +1,137 @@
package at.mueller.eeg.backend.dashboard.service;
import at.mueller.eeg.backend.community.api.dto.MeteringDataRecordDto;
import at.mueller.eeg.backend.community.api.dto.MeteringDataUploadRequest;
import at.mueller.eeg.backend.community.api.dto.MeteringDataUploadResponse;
import at.mueller.eeg.backend.community.domain.*;
import at.mueller.eeg.backend.community.domain.state.MakoState;
import at.mueller.eeg.backend.community.repository.MeteringDataRepository;
import at.mueller.eeg.backend.community.repository.MeteringPointRepository;
import at.mueller.eeg.backend.community.service.MeteringDataService;
import at.mueller.eeg.backend.dashboard.api.dto.DashboardStatsDto;
import at.mueller.eeg.backend.iam.domain.ParticipantType;
import at.mueller.eeg.backend.iam.domain.RegistrationStatus;
import at.mueller.eeg.backend.iam.domain.User;
import at.mueller.eeg.backend.iam.domain.UserRole;
import at.mueller.eeg.backend.iam.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@ActiveProfiles("test")
@Transactional
class DashboardServiceIntegrationTest {
@Autowired
private DashboardService dashboardService;
@Autowired
private MeteringDataService meteringDataService;
@Autowired
private MeteringPointRepository meteringPointRepository;
@Autowired
private MeteringDataRepository meteringDataRepository;
@Autowired
private UserRepository userRepository;
private User testUser;
private MeteringPoint testMeteringPoint;
@BeforeEach
void setUp() {
testUser = new User();
testUser.setEmail("test-dashboard-" + UUID.randomUUID() + "@example.com");
testUser.setPasswordHash("encoded");
testUser.setFirstName("Test");
testUser.setLastName("Dashboard");
testUser.setRole(UserRole.MEMBER);
testUser.setStatus(RegistrationStatus.APPROVED);
testUser.setParticipantType(ParticipantType.PRIVATE);
testUser.setEnabled(true);
testUser = userRepository.save(testUser);
testMeteringPoint = new MeteringPoint();
testMeteringPoint.setUserId(testUser.getId());
testMeteringPoint.setAtNumber("AT0010000000000000000000001234567");
testMeteringPoint.setType(PointType.CONSUMER);
testMeteringPoint.setMakoState(MakoState.ACTIVE);
testMeteringPoint = meteringPointRepository.save(testMeteringPoint);
}
@Test
void getMeteringDataOverview_afterUpload_returnsNonEmptyTimeSeries() {
// Upload hourly data for 3 days
List<MeteringDataRecordDto> records = new ArrayList<>();
Instant base = Instant.parse("2026-06-01T00:00:00Z");
for (int i = 0; i < 72; i++) { // 3 days * 24 hours
Instant start = base.plusSeconds(i * 3600L);
Instant end = start.plusSeconds(3600);
records.add(new MeteringDataRecordDto(
testMeteringPoint.getAtNumber(), MeteringDataType.CONSUMPTION,
start, end, 0.5 + (i % 10) * 0.1, null));
}
MeteringDataUploadRequest request = new MeteringDataUploadRequest(
DataSource.EMAIL_XLSX, "test.xlsx", records);
MeteringDataUploadResponse uploadResponse = meteringDataService.uploadMeteringData(request, testUser.getId());
assertEquals(UploadStatus.COMPLETED, uploadResponse.status());
// Now query via dashboard service
Instant from = Instant.parse("2026-06-01T00:00:00Z");
Instant to = Instant.parse("2026-06-04T00:00:00Z");
DashboardStatsDto.MeteringDataOverview overview = dashboardService.getMeteringDataOverview(testUser.getId(), from, to);
assertNotNull(overview);
assertFalse(overview.meteringPoints().isEmpty(), "Should have metering points");
assertFalse(overview.timeSeries().isEmpty(), "Should have time series data");
// Data spans 3 full days, but timezone grouping may add 1 day
assertTrue(overview.timeSeries().size() >= 3, "Should have at least 3 days of data");
assertEquals(1, overview.meteringPoints().size());
assertTrue(overview.meteringPoints().get(0).totalKwh() > 0, "Total kWh should be > 0");
}
@Test
void getMeteringDataOverview_fullYearData_returnsTimeSeries() {
// Upload full year hourly data
List<MeteringDataRecordDto> records = new ArrayList<>();
Instant base = Instant.parse("2026-01-01T00:00:00Z");
for (int i = 0; i < 8760; i++) {
Instant start = base.plusSeconds(i * 3600L);
Instant end = start.plusSeconds(3600);
records.add(new MeteringDataRecordDto(
testMeteringPoint.getAtNumber(), MeteringDataType.CONSUMPTION,
start, end, 0.5 + (i % 10) * 0.1, null));
}
MeteringDataUploadRequest request = new MeteringDataUploadRequest(
DataSource.EMAIL_XLSX, "fullyear.xlsx", records);
MeteringDataUploadResponse uploadResponse = meteringDataService.uploadMeteringData(request, testUser.getId());
assertEquals(UploadStatus.COMPLETED, uploadResponse.status());
// Query last 30 days
Instant from = Instant.parse("2026-06-24T00:00:00Z");
Instant to = Instant.parse("2026-07-24T00:00:00Z");
DashboardStatsDto.MeteringDataOverview overview = dashboardService.getMeteringDataOverview(testUser.getId(), from, to);
assertNotNull(overview);
assertFalse(overview.meteringPoints().isEmpty(), "Should have metering points");
assertFalse(overview.timeSeries().isEmpty(), "Should have time series data for the queried range");
assertTrue(overview.timeSeries().size() >= 29, "Should have ~30 days of data");
}
}

View File

@ -6,6 +6,7 @@ import {BASE_PATH} from './api';
import {jwtInterceptor} from './interceptors/jwt.interceptor'; import {jwtInterceptor} from './interceptors/jwt.interceptor';
import {errorInterceptor} from './interceptors/error.interceptor'; import {errorInterceptor} from './interceptors/error.interceptor';
import {environment} from '../environments/environment'; import {environment} from '../environments/environment';
import {provideEcharts} from 'ngx-echarts';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
@ -13,6 +14,7 @@ export const appConfig: ApplicationConfig = {
provideRouter(routes), provideRouter(routes),
provideHttpClient(withFetch(), provideHttpClient(withFetch(),
withInterceptors([jwtInterceptor, errorInterceptor])), withInterceptors([jwtInterceptor, errorInterceptor])),
{ provide: BASE_PATH, useValue: environment.apiUrl } { provide: BASE_PATH, useValue: environment.apiUrl },
provideEcharts()
] ]
}; };