fix(dashboard): optimize stats queries and add error handling
Backend: - Add countByStatus() to MembershipRepository - Add countByUserIdAndStatus() to MembershipRepository - Add countByStatusAndEmailVerified() to UserRepository - Add countByUserId() to MeteringPointRepository - Optimize DashboardService to use count queries instead of findAll Frontend: - Add error signal and error message display - Add loading spinner animation Tests: - Add DashboardServiceTest with 2 unit tests - All 22 tests passing
This commit is contained in:
parent
c66c5fddfa
commit
a2025a55e4
@ -23,6 +23,11 @@ public interface MembershipRepository extends JpaRepository<Membership, UUID> {
|
||||
|
||||
boolean existsByEnergyCommunityId(UUID energyCommunityId);
|
||||
|
||||
long countByStatus(MembershipStatus status);
|
||||
|
||||
@Query("SELECT COUNT(m) FROM Membership m JOIN m.meteringPoint mp WHERE mp.userId = :userId AND m.status = :status")
|
||||
long countByUserIdAndStatus(@Param("userId") UUID userId, @Param("status") MembershipStatus status);
|
||||
|
||||
@Query("SELECT m.energyCommunity.id, COUNT(m) FROM Membership m WHERE m.energyCommunity.id IN :communityIds AND m.status != 'INACTIVE' GROUP BY m.energyCommunity.id")
|
||||
List<Object[]> countActiveMembersForCommunities(@Param("communityIds") List<UUID> communityIds);
|
||||
|
||||
|
||||
@ -11,6 +11,8 @@ public interface MeteringPointRepository extends JpaRepository<MeteringPoint, UU
|
||||
|
||||
List<MeteringPoint> findAllByUserId(UUID userId);
|
||||
|
||||
long countByUserId(UUID userId);
|
||||
|
||||
boolean existsByAtNumber(String atNumber);
|
||||
|
||||
Optional<MeteringPoint> findByAtNumber(String atNumber);
|
||||
|
||||
@ -25,23 +25,17 @@ public class DashboardService {
|
||||
|
||||
public DashboardStatsDto.AdminStats getAdminStats() {
|
||||
long communities = energyCommunityRepository.count();
|
||||
long pendingUsers = userRepository.findAllByStatusAndEmailVerified(RegistrationStatus.PENDING, true).size();
|
||||
|
||||
// Zählt alle aktiven Mitgliedschaften
|
||||
long activeMemberships = membershipRepository.findAll().stream()
|
||||
.filter(m -> m.getStatus() == MembershipStatus.ACTIVE)
|
||||
.count();
|
||||
long pendingUsers = userRepository.countByStatusAndEmailVerified(RegistrationStatus.PENDING, true);
|
||||
long activeMemberships = membershipRepository.countByStatus(MembershipStatus.ACTIVE);
|
||||
|
||||
return new DashboardStatsDto.AdminStats(communities, pendingUsers, activeMemberships);
|
||||
}
|
||||
|
||||
public DashboardStatsDto.UserStats getUserStats(UUID userId) {
|
||||
long meteringPoints = meteringPointRepository.findAllByUserId(userId).size();
|
||||
long meteringPoints = meteringPointRepository.countByUserId(userId);
|
||||
long activeMemberships = membershipRepository.countByUserIdAndStatus(userId, MembershipStatus.ACTIVE);
|
||||
long pendingMemberships = membershipRepository.countByUserIdAndStatus(userId, MembershipStatus.PENDING);
|
||||
|
||||
// Zählt die Mitgliedschaften der Zählpunkte des Users
|
||||
long activeMembers = 0; // Hier idealerweise eine angepasste Query im Repository nutzen
|
||||
long pendingMembers = 0;
|
||||
|
||||
return new DashboardStatsDto.UserStats(meteringPoints, activeMembers, pendingMembers);
|
||||
return new DashboardStatsDto.UserStats(meteringPoints, activeMemberships, pendingMemberships);
|
||||
}
|
||||
}
|
||||
@ -14,5 +14,7 @@ public interface UserRepository extends JpaRepository<User, UUID> {
|
||||
|
||||
List<User> findAllByStatusAndEmailVerified(RegistrationStatus status, Boolean verified);
|
||||
|
||||
long countByStatusAndEmailVerified(RegistrationStatus status, Boolean verified);
|
||||
|
||||
Optional<User> findByVerificationToken(String token);
|
||||
}
|
||||
|
||||
@ -0,0 +1,65 @@
|
||||
package at.mueller.eeg.backend.dashboard.service;
|
||||
|
||||
import at.mueller.eeg.backend.community.domain.MembershipStatus;
|
||||
import at.mueller.eeg.backend.community.repository.EnergyCommunityRepository;
|
||||
import at.mueller.eeg.backend.community.repository.MembershipRepository;
|
||||
import at.mueller.eeg.backend.community.repository.MeteringPointRepository;
|
||||
import at.mueller.eeg.backend.dashboard.api.dto.DashboardStatsDto;
|
||||
import at.mueller.eeg.backend.iam.domain.RegistrationStatus;
|
||||
import at.mueller.eeg.backend.iam.repository.UserRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DashboardServiceTest {
|
||||
|
||||
@Mock
|
||||
private EnergyCommunityRepository energyCommunityRepository;
|
||||
|
||||
@Mock
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Mock
|
||||
private MembershipRepository membershipRepository;
|
||||
|
||||
@Mock
|
||||
private MeteringPointRepository meteringPointRepository;
|
||||
|
||||
@InjectMocks
|
||||
private DashboardService dashboardService;
|
||||
|
||||
@Test
|
||||
void getAdminStats_returnsCorrectCounts() {
|
||||
when(energyCommunityRepository.count()).thenReturn(5L);
|
||||
when(userRepository.countByStatusAndEmailVerified(RegistrationStatus.PENDING, true)).thenReturn(3L);
|
||||
when(membershipRepository.countByStatus(MembershipStatus.ACTIVE)).thenReturn(10L);
|
||||
|
||||
DashboardStatsDto.AdminStats stats = dashboardService.getAdminStats();
|
||||
|
||||
assertEquals(5, stats.totalCommunities());
|
||||
assertEquals(3, stats.pendingUsers());
|
||||
assertEquals(10, stats.totalActiveMemberships());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getUserStats_returnsCorrectCounts() {
|
||||
UUID userId = UUID.randomUUID();
|
||||
when(meteringPointRepository.countByUserId(userId)).thenReturn(2L);
|
||||
when(membershipRepository.countByUserIdAndStatus(userId, MembershipStatus.ACTIVE)).thenReturn(1L);
|
||||
when(membershipRepository.countByUserIdAndStatus(userId, MembershipStatus.PENDING)).thenReturn(1L);
|
||||
|
||||
DashboardStatsDto.UserStats stats = dashboardService.getUserStats(userId);
|
||||
|
||||
assertEquals(2, stats.totalMeteringPoints());
|
||||
assertEquals(1, stats.activeMemberships());
|
||||
assertEquals(1, stats.pendingMemberships());
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,12 @@
|
||||
@if (isLoading()) {
|
||||
<div class="text-slate-500">Lade Daten...</div>
|
||||
<div class="flex justify-center items-center py-12">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-emerald-600"></div>
|
||||
<span class="ml-3 text-slate-500">Lade Daten...</span>
|
||||
</div>
|
||||
} @else if (error()) {
|
||||
<div class="bg-red-50 border border-red-200 rounded-lg p-6 text-center">
|
||||
<p class="text-red-600">{{ error() }}</p>
|
||||
</div>
|
||||
} @else if (role === 'ADMIN' && adminStats(); 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">
|
||||
|
||||
@ -15,17 +15,24 @@ export class DashboardOverview implements OnInit {
|
||||
adminStats = signal<AdminStats | null>(null);
|
||||
userStats = signal<UserStats | null>(null);
|
||||
isLoading = signal(true);
|
||||
error = signal<string | null>(null);
|
||||
|
||||
ngOnInit() {
|
||||
if (this.role === 'ADMIN') {
|
||||
this.dashboardService.getAdminStats().subscribe({
|
||||
next: stats => { this.adminStats.set(stats); this.isLoading.set(false); },
|
||||
error: () => this.isLoading.set(false)
|
||||
error: () => {
|
||||
this.isLoading.set(false);
|
||||
this.error.set('Fehler beim Laden der Statistiken.');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.dashboardService.getUserStats().subscribe({
|
||||
next: stats => { this.userStats.set(stats); this.isLoading.set(false); },
|
||||
error: () => this.isLoading.set(false)
|
||||
error: () => {
|
||||
this.isLoading.set(false);
|
||||
this.error.set('Fehler beim Laden der Statistiken.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user