From 6cb4ff7a4a745dc0b824306440c038dd3e580f2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernhard=20M=C3=BCller?= Date: Fri, 24 Jul 2026 15:57:35 +0200 Subject: [PATCH] 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. --- .../DashboardServiceIntegrationTest.java | 137 ++++++++++++++++++ eeg_frontend/src/app/app.config.ts | 4 +- 2 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 eeg_backend/src/test/java/at/mueller/eeg/backend/dashboard/service/DashboardServiceIntegrationTest.java diff --git a/eeg_backend/src/test/java/at/mueller/eeg/backend/dashboard/service/DashboardServiceIntegrationTest.java b/eeg_backend/src/test/java/at/mueller/eeg/backend/dashboard/service/DashboardServiceIntegrationTest.java new file mode 100644 index 0000000..ee65c42 --- /dev/null +++ b/eeg_backend/src/test/java/at/mueller/eeg/backend/dashboard/service/DashboardServiceIntegrationTest.java @@ -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 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 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"); + } +} diff --git a/eeg_frontend/src/app/app.config.ts b/eeg_frontend/src/app/app.config.ts index 3d9c586..1f2917b 100644 --- a/eeg_frontend/src/app/app.config.ts +++ b/eeg_frontend/src/app/app.config.ts @@ -6,6 +6,7 @@ import {BASE_PATH} from './api'; import {jwtInterceptor} from './interceptors/jwt.interceptor'; import {errorInterceptor} from './interceptors/error.interceptor'; import {environment} from '../environments/environment'; +import {provideEcharts} from 'ngx-echarts'; export const appConfig: ApplicationConfig = { providers: [ @@ -13,6 +14,7 @@ export const appConfig: ApplicationConfig = { provideRouter(routes), provideHttpClient(withFetch(), withInterceptors([jwtInterceptor, errorInterceptor])), - { provide: BASE_PATH, useValue: environment.apiUrl } + { provide: BASE_PATH, useValue: environment.apiUrl }, + provideEcharts() ] };