feat(dashboard): add pending memberships card to admin overview

- Add pendingMemberships field to AdminStats DTO
- Add 4th card 'Offene Mitgliedsanträge' to admin dashboard
- Rename 'Offene Freigaben' to 'Ausstehende Freischaltungen'
- Update DashboardService to count pending memberships
- Update tests to match new AdminStats structure
This commit is contained in:
Bernhard Müller 2026-07-22 10:59:01 +02:00
parent 5949359216
commit 949e1e6c34
6 changed files with 16 additions and 5 deletions

View File

@ -4,6 +4,7 @@ public interface DashboardStatsDto {
record AdminStats( record AdminStats(
long totalCommunities, long totalCommunities,
long pendingUsers, long pendingUsers,
long pendingMemberships,
long totalActiveMemberships long totalActiveMemberships
) {} ) {}

View File

@ -26,9 +26,10 @@ public class DashboardService {
public DashboardStatsDto.AdminStats getAdminStats() { public DashboardStatsDto.AdminStats getAdminStats() {
long communities = energyCommunityRepository.count(); long communities = energyCommunityRepository.count();
long pendingUsers = userRepository.countByStatusAndEmailVerified(RegistrationStatus.PENDING, true); long pendingUsers = userRepository.countByStatusAndEmailVerified(RegistrationStatus.PENDING, true);
long pendingMemberships = membershipRepository.countByStatus(MembershipStatus.PENDING);
long activeMemberships = membershipRepository.countByStatus(MembershipStatus.ACTIVE); long activeMemberships = membershipRepository.countByStatus(MembershipStatus.ACTIVE);
return new DashboardStatsDto.AdminStats(communities, pendingUsers, activeMemberships); return new DashboardStatsDto.AdminStats(communities, pendingUsers, pendingMemberships, activeMemberships);
} }
public DashboardStatsDto.UserStats getUserStats(UUID userId) { public DashboardStatsDto.UserStats getUserStats(UUID userId) {

View File

@ -25,7 +25,7 @@ class DashboardControllerTest {
@Test @Test
void getAdminDashboard() { void getAdminDashboard() {
DashboardStatsDto.AdminStats stats = new DashboardStatsDto.AdminStats(5, 3, 12); DashboardStatsDto.AdminStats stats = new DashboardStatsDto.AdminStats(5, 3, 2, 12);
when(dashboardService.getAdminStats()).thenReturn(stats); when(dashboardService.getAdminStats()).thenReturn(stats);
ResponseEntity<DashboardStatsDto.AdminStats> response = dashboardController.getAdminDashboard(); ResponseEntity<DashboardStatsDto.AdminStats> response = dashboardController.getAdminDashboard();
@ -33,18 +33,20 @@ class DashboardControllerTest {
assertEquals(200, response.getStatusCode().value()); assertEquals(200, response.getStatusCode().value());
assertEquals(5, response.getBody().totalCommunities()); assertEquals(5, response.getBody().totalCommunities());
assertEquals(3, response.getBody().pendingUsers()); assertEquals(3, response.getBody().pendingUsers());
assertEquals(2, response.getBody().pendingMemberships());
assertEquals(12, response.getBody().totalActiveMemberships()); assertEquals(12, response.getBody().totalActiveMemberships());
} }
@Test @Test
void getAdminDashboard_empty() { void getAdminDashboard_empty() {
DashboardStatsDto.AdminStats stats = new DashboardStatsDto.AdminStats(0, 0, 0); DashboardStatsDto.AdminStats stats = new DashboardStatsDto.AdminStats(0, 0, 0, 0);
when(dashboardService.getAdminStats()).thenReturn(stats); when(dashboardService.getAdminStats()).thenReturn(stats);
ResponseEntity<DashboardStatsDto.AdminStats> response = dashboardController.getAdminDashboard(); ResponseEntity<DashboardStatsDto.AdminStats> response = dashboardController.getAdminDashboard();
assertEquals(0, response.getBody().totalCommunities()); assertEquals(0, response.getBody().totalCommunities());
assertEquals(0, response.getBody().pendingUsers()); assertEquals(0, response.getBody().pendingUsers());
assertEquals(0, response.getBody().pendingMemberships());
assertEquals(0, response.getBody().totalActiveMemberships()); assertEquals(0, response.getBody().totalActiveMemberships());
} }

View File

@ -40,12 +40,14 @@ class DashboardServiceTest {
void getAdminStats_returnsCorrectCounts() { void getAdminStats_returnsCorrectCounts() {
when(energyCommunityRepository.count()).thenReturn(5L); when(energyCommunityRepository.count()).thenReturn(5L);
when(userRepository.countByStatusAndEmailVerified(RegistrationStatus.PENDING, true)).thenReturn(3L); when(userRepository.countByStatusAndEmailVerified(RegistrationStatus.PENDING, true)).thenReturn(3L);
when(membershipRepository.countByStatus(MembershipStatus.PENDING)).thenReturn(2L);
when(membershipRepository.countByStatus(MembershipStatus.ACTIVE)).thenReturn(10L); when(membershipRepository.countByStatus(MembershipStatus.ACTIVE)).thenReturn(10L);
DashboardStatsDto.AdminStats stats = dashboardService.getAdminStats(); DashboardStatsDto.AdminStats stats = dashboardService.getAdminStats();
assertEquals(5, stats.totalCommunities()); assertEquals(5, stats.totalCommunities());
assertEquals(3, stats.pendingUsers()); assertEquals(3, stats.pendingUsers());
assertEquals(2, stats.pendingMemberships());
assertEquals(10, stats.totalActiveMemberships()); assertEquals(10, stats.totalActiveMemberships());
} }

View File

@ -8,15 +8,19 @@
<p class="text-red-600">{{ error() }}</p> <p class="text-red-600">{{ error() }}</p>
</div> </div>
} @else if (role === 'ADMIN' && adminStats(); as stats) { } @else if (role === 'ADMIN' && adminStats(); as stats) {
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6"> <div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
<p class="text-sm text-slate-500">Energiegemeinschaften</p> <p class="text-sm text-slate-500">Energiegemeinschaften</p>
<p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.totalCommunities }}</p> <p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.totalCommunities }}</p>
</div> </div>
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6"> <div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
<p class="text-sm text-slate-500">Offene Freigaben</p> <p class="text-sm text-slate-500">Ausstehende Freischaltungen</p>
<p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.pendingUsers }}</p> <p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.pendingUsers }}</p>
</div> </div>
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
<p class="text-sm text-slate-500">Offene Mitgliedsanträge</p>
<p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.pendingMemberships }}</p>
</div>
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6"> <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-sm text-slate-500">Aktive Mitgliedschaften</p>
<p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.totalActiveMemberships }}</p> <p class="text-3xl font-bold text-slate-800 mt-2">{{ stats.totalActiveMemberships }}</p>

View File

@ -6,6 +6,7 @@ import {environment} from '../../environments/environment';
export interface AdminStats { export interface AdminStats {
totalCommunities: number; totalCommunities: number;
pendingUsers: number; pendingUsers: number;
pendingMemberships: number;
totalActiveMemberships: number; totalActiveMemberships: number;
} }