diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/MembershipController.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/MembershipController.java index cc2f7ae..8b231a1 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/MembershipController.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/MembershipController.java @@ -1,7 +1,10 @@ package at.mueller.eeg.backend.community.api; +import at.mueller.eeg.backend.common.security.CurrentUserId; import at.mueller.eeg.backend.community.api.dto.EligibleCommunityResponse; import at.mueller.eeg.backend.community.api.dto.MembershipRequest; +import at.mueller.eeg.backend.community.api.dto.MembershipResponse; +import at.mueller.eeg.backend.community.api.dto.PendingMembershipResponse; import at.mueller.eeg.backend.community.service.MembershipService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -33,10 +36,22 @@ public class MembershipController { return ResponseEntity.status(HttpStatus.CREATED).body(membershipId); } + @GetMapping("/memberships/me") + @PreAuthorize("hasRole('MEMBER')") + public ResponseEntity> getOwnMemberships(@CurrentUserId String userId) { + return ResponseEntity.ok(membershipService.getOwnMemberships(UUID.fromString(userId))); + } + // --- ADMIN ENDPUNKTE --- + @GetMapping("/admin/memberships/pending") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity> getPendingMemberships() { + return ResponseEntity.ok(membershipService.getPendingMemberships()); + } + @PutMapping("/admin/memberships/{id}/approve") - @PreAuthorize("hasRole('ADMIN')") // Verknüpfung mit deiner UserRole.ADMIN aus dem iam-Modul + @PreAuthorize("hasRole('ADMIN')") public ResponseEntity approveMembership(@PathVariable UUID id) { membershipService.approveMembership(id); return ResponseEntity.ok().build(); diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/dto/MembershipResponse.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/dto/MembershipResponse.java new file mode 100644 index 0000000..d601603 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/dto/MembershipResponse.java @@ -0,0 +1,16 @@ +package at.mueller.eeg.backend.community.api.dto; + +import java.time.LocalDate; +import java.util.UUID; + +public record MembershipResponse( + UUID id, + UUID meteringPointId, + String atNumber, + UUID energyCommunityId, + String communityName, + Integer priorityLevel, + String status, + LocalDate validFrom, + LocalDate validTo +) {} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/dto/PendingMembershipResponse.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/dto/PendingMembershipResponse.java new file mode 100644 index 0000000..634095d --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/dto/PendingMembershipResponse.java @@ -0,0 +1,14 @@ +package at.mueller.eeg.backend.community.api.dto; + +import java.util.UUID; + +public record PendingMembershipResponse( + UUID id, + String userEmail, + String firstName, + String lastName, + String atNumber, + String communityName, + Integer priorityLevel, + String status +) {} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/repository/MembershipRepository.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/repository/MembershipRepository.java index ad92c73..4e38eba 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/repository/MembershipRepository.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/repository/MembershipRepository.java @@ -25,4 +25,10 @@ public interface MembershipRepository extends JpaRepository { @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 countActiveMembersForCommunities(@Param("communityIds") List communityIds); + + @Query("SELECT m FROM Membership m JOIN FETCH m.meteringPoint mp JOIN FETCH m.energyCommunity ec WHERE mp.userId = :userId") + List findByUserIdWithDetails(@Param("userId") UUID userId); + + @Query("SELECT m FROM Membership m JOIN FETCH m.meteringPoint mp JOIN FETCH m.energyCommunity ec JOIN FETCH mp.user u WHERE m.status = 'PENDING'") + List findPendingWithDetails(); } \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/service/MembershipService.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/service/MembershipService.java index c4a3c59..393059f 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/service/MembershipService.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/service/MembershipService.java @@ -2,6 +2,8 @@ package at.mueller.eeg.backend.community.service; import at.mueller.eeg.backend.community.api.dto.EligibleCommunityResponse; import at.mueller.eeg.backend.community.api.dto.MembershipRequest; +import at.mueller.eeg.backend.community.api.dto.MembershipResponse; +import at.mueller.eeg.backend.community.api.dto.PendingMembershipResponse; import at.mueller.eeg.backend.community.domain.EnergyCommunity; import at.mueller.eeg.backend.community.domain.Membership; import at.mueller.eeg.backend.community.domain.MembershipStatus; @@ -10,6 +12,8 @@ import at.mueller.eeg.backend.community.domain.state.MakoState; 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.iam.domain.User; +import at.mueller.eeg.backend.iam.repository.UserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -17,7 +21,9 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.util.List; +import java.util.Map; import java.util.UUID; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -27,6 +33,7 @@ public class MembershipService { private final EnergyCommunityRepository energyCommunityRepository; private final MembershipRepository membershipRepository; private final MeteringPointRepository meteringPointRepository; + private final UserRepository userRepository; @Transactional(readOnly = true) public List getEligibleCommunities(UUID meteringPointId) { @@ -72,13 +79,11 @@ public class MembershipService { throw new IllegalStateException("Die Priorität " + request.priorityLevel() + " ist bereits vergeben. Bitte wähle eine andere oder ändere zuerst deine bestehenden Mitgliedschaften."); } - // TODO: Hier könnte man noch prüfen, ob das priorityLevel bereits für diesen Zählpunkt vergeben ist. - Membership membership = new Membership(); membership.setMeteringPoint(point); membership.setEnergyCommunity(community); membership.setPriorityLevel(request.priorityLevel()); - membership.setStatus(MembershipStatus.PENDING); // Startet als PENDING + membership.setStatus(MembershipStatus.PENDING); Membership saved = membershipRepository.save(membership); log.info("Beitrittsantrag für Zählpunkt {} zu Community {} erstellt.", point.getId(), community.getId()); @@ -95,13 +100,10 @@ public class MembershipService { } membership.setStatus(MembershipStatus.ACTIVE); - membership.setValidFrom(LocalDate.now()); // Ab heute aktiv + membership.setValidFrom(LocalDate.now()); membershipRepository.save(membership); log.info("Mitgliedschaft {} wurde vom Admin freigeschaltet.", membershipId); - - // TODO: Hier später ein Integration Event feuern (z.B. "MembershipActivatedEvent"), - // damit die mako-Domäne die Marktkommunikation (Beginn der Belieferung/Zuteilung) via EDA anstößt! } @Transactional @@ -119,4 +121,52 @@ public class MembershipService { membershipRepository.save(membership); log.info("Mitgliedschaft {} wurde vom Admin abgelehnt.", membershipId); } + + @Transactional(readOnly = true) + public List getOwnMemberships(UUID userId) { + return membershipRepository.findByUserIdWithDetails(userId).stream() + .map(m -> new MembershipResponse( + m.getId(), + m.getMeteringPoint().getId(), + m.getMeteringPoint().getAtNumber(), + m.getEnergyCommunity().getId(), + m.getEnergyCommunity().getName(), + m.getPriorityLevel(), + m.getStatus().name(), + m.getValidFrom(), + m.getValidTo() + )) + .toList(); + } + + @Transactional(readOnly = true) + public List getPendingMemberships() { + List pendingMemberships = membershipRepository.findPendingWithDetails(); + + // Collect all user IDs from metering points + List userIds = pendingMemberships.stream() + .map(m -> m.getMeteringPoint().getUserId()) + .distinct() + .toList(); + + // Fetch users and create a lookup map + Map usersById = userRepository.findAllById(userIds).stream() + .collect(Collectors.toMap(User::getId, user -> user)); + + return pendingMemberships.stream() + .map(m -> { + User user = usersById.get(m.getMeteringPoint().getUserId()); + return new PendingMembershipResponse( + m.getId(), + user != null ? user.getEmail() : "unknown", + user != null ? user.getFirstName() : "unknown", + user != null ? user.getLastName() : "unknown", + m.getMeteringPoint().getAtNumber(), + m.getEnergyCommunity().getName(), + m.getPriorityLevel(), + m.getStatus().name() + ); + }) + .toList(); + } } \ No newline at end of file diff --git a/eeg_backend/src/test/java/at/mueller/eeg/backend/community/service/MembershipServiceTest.java b/eeg_backend/src/test/java/at/mueller/eeg/backend/community/service/MembershipServiceTest.java new file mode 100644 index 0000000..0dbb8ca --- /dev/null +++ b/eeg_backend/src/test/java/at/mueller/eeg/backend/community/service/MembershipServiceTest.java @@ -0,0 +1,249 @@ +package at.mueller.eeg.backend.community.service; + +import at.mueller.eeg.backend.community.api.dto.EligibleCommunityResponse; +import at.mueller.eeg.backend.community.api.dto.MembershipRequest; +import at.mueller.eeg.backend.community.api.dto.MembershipResponse; +import at.mueller.eeg.backend.community.api.dto.PendingMembershipResponse; +import at.mueller.eeg.backend.community.domain.CommunityType; +import at.mueller.eeg.backend.community.domain.EnergyCommunity; +import at.mueller.eeg.backend.community.domain.Membership; +import at.mueller.eeg.backend.community.domain.MembershipStatus; +import at.mueller.eeg.backend.community.domain.MeteringPoint; +import at.mueller.eeg.backend.community.domain.state.MakoState; +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.iam.domain.User; +import at.mueller.eeg.backend.iam.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +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.time.LocalDate; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class MembershipServiceTest { + + @Mock + private EnergyCommunityRepository energyCommunityRepository; + + @Mock + private MembershipRepository membershipRepository; + + @Mock + private MeteringPointRepository meteringPointRepository; + + @Mock + private UserRepository userRepository; + + @InjectMocks + private MembershipService membershipService; + + private MeteringPoint testMeteringPoint; + private EnergyCommunity testCommunity; + private Membership testMembership; + private User testUser; + private UUID testUserId; + private UUID testMeteringPointId; + private UUID testCommunityId; + private UUID testMembershipId; + + @BeforeEach + void setUp() { + testUserId = UUID.randomUUID(); + testMeteringPointId = UUID.randomUUID(); + testCommunityId = UUID.randomUUID(); + testMembershipId = UUID.randomUUID(); + + testUser = new User(); + testUser.setId(testUserId); + testUser.setEmail("test@example.com"); + testUser.setFirstName("Test"); + testUser.setLastName("Benutzer"); + + testMeteringPoint = new MeteringPoint(); + testMeteringPoint.setId(testMeteringPointId); + testMeteringPoint.setUserId(testUserId); + testMeteringPoint.setAtNumber("AT0010000000000000000000001234567"); + testMeteringPoint.setMakoState(MakoState.ACTIVE); + testMeteringPoint.setSubstationId("UW_WAP_21_STREBERSDORF"); + testMeteringPoint.setTransformerId("TRAFO_21_042_BISAMBERG"); + testMeteringPoint.setGridOperatorId("Wiener Netze GmbH"); + + testCommunity = new EnergyCommunity(); + testCommunity.setId(testCommunityId); + testCommunity.setName("EEG Wien Mitte"); + testCommunity.setType(CommunityType.EEG_REGIONAL); + testCommunity.setEdaEcId("EDA-EC-001"); + testCommunity.setSubstationId("UW_WAP_21_STREBERSDORF"); + testCommunity.setGridOperatorId("Wiener Netze GmbH"); + + testMembership = new Membership(); + testMembership.setId(testMembershipId); + testMembership.setMeteringPoint(testMeteringPoint); + testMembership.setEnergyCommunity(testCommunity); + testMembership.setPriorityLevel(1); + testMembership.setStatus(MembershipStatus.PENDING); + } + + @Test + void getEligibleCommunities_returnsMatchingCommunities() { + when(meteringPointRepository.findById(testMeteringPointId)).thenReturn(Optional.of(testMeteringPoint)); + when(energyCommunityRepository.findEligibleCommunities( + testMeteringPoint.getSubstationId(), + testMeteringPoint.getTransformerId(), + testMeteringPoint.getGridOperatorId() + )).thenReturn(List.of(testCommunity)); + + List result = membershipService.getEligibleCommunities(testMeteringPointId); + + assertEquals(1, result.size()); + assertEquals(testCommunityId, result.get(0).id()); + assertEquals("EEG Wien Mitte", result.get(0).name()); + } + + @Test + void getEligibleCommunities_returnsEmptyForInactivePoint() { + testMeteringPoint.setMakoState(MakoState.WAITING_FOR_CONSENT); + when(meteringPointRepository.findById(testMeteringPointId)).thenReturn(Optional.of(testMeteringPoint)); + + List result = membershipService.getEligibleCommunities(testMeteringPointId); + + assertTrue(result.isEmpty()); + } + + @Test + void getEligibleCommunities_throwsForUnknownPoint() { + when(meteringPointRepository.findById(any())).thenReturn(Optional.empty()); + + assertThrows(IllegalArgumentException.class, () -> membershipService.getEligibleCommunities(UUID.randomUUID())); + } + + @Test + void requestMembership_createsNewMembership() { + when(meteringPointRepository.findById(testMeteringPointId)).thenReturn(Optional.of(testMeteringPoint)); + when(energyCommunityRepository.findById(testCommunityId)).thenReturn(Optional.of(testCommunity)); + when(membershipRepository.existsByMeteringPointIdAndEnergyCommunityId(testMeteringPointId, testCommunityId)).thenReturn(false); + when(membershipRepository.existsByMeteringPointIdAndPriorityLevelAndStatusIn( + testMeteringPointId, 1, List.of(MembershipStatus.ACTIVE, MembershipStatus.PENDING) + )).thenReturn(false); + when(membershipRepository.save(any())).thenReturn(testMembership); + + MembershipRequest request = new MembershipRequest(testMeteringPointId, testCommunityId, 1); + UUID result = membershipService.requestMembership(request); + + assertEquals(testMembershipId, result); + verify(membershipRepository).save(any()); + } + + @Test + void requestMembership_throwsForInactivePoint() { + testMeteringPoint.setMakoState(MakoState.NEW); + when(meteringPointRepository.findById(testMeteringPointId)).thenReturn(Optional.of(testMeteringPoint)); + when(energyCommunityRepository.findById(testCommunityId)).thenReturn(Optional.of(testCommunity)); + + MembershipRequest request = new MembershipRequest(testMeteringPointId, testCommunityId, 1); + + assertThrows(IllegalStateException.class, () -> membershipService.requestMembership(request)); + } + + @Test + void requestMembership_throwsForDuplicateRequest() { + when(meteringPointRepository.findById(testMeteringPointId)).thenReturn(Optional.of(testMeteringPoint)); + when(energyCommunityRepository.findById(testCommunityId)).thenReturn(Optional.of(testCommunity)); + when(membershipRepository.existsByMeteringPointIdAndEnergyCommunityId(testMeteringPointId, testCommunityId)).thenReturn(true); + + MembershipRequest request = new MembershipRequest(testMeteringPointId, testCommunityId, 1); + + assertThrows(IllegalStateException.class, () -> membershipService.requestMembership(request)); + } + + @Test + void requestMembership_throwsForTakenPriority() { + when(meteringPointRepository.findById(testMeteringPointId)).thenReturn(Optional.of(testMeteringPoint)); + when(energyCommunityRepository.findById(testCommunityId)).thenReturn(Optional.of(testCommunity)); + when(membershipRepository.existsByMeteringPointIdAndEnergyCommunityId(testMeteringPointId, testCommunityId)).thenReturn(false); + when(membershipRepository.existsByMeteringPointIdAndPriorityLevelAndStatusIn( + testMeteringPointId, 1, List.of(MembershipStatus.ACTIVE, MembershipStatus.PENDING) + )).thenReturn(true); + + MembershipRequest request = new MembershipRequest(testMeteringPointId, testCommunityId, 1); + + assertThrows(IllegalStateException.class, () -> membershipService.requestMembership(request)); + } + + @Test + void approveMembership_setsActiveStatus() { + when(membershipRepository.findById(testMembershipId)).thenReturn(Optional.of(testMembership)); + when(membershipRepository.save(any())).thenReturn(testMembership); + + membershipService.approveMembership(testMembershipId); + + assertEquals(MembershipStatus.ACTIVE, testMembership.getStatus()); + assertEquals(LocalDate.now(), testMembership.getValidFrom()); + verify(membershipRepository).save(testMembership); + } + + @Test + void approveMembership_throwsForNonPendingStatus() { + testMembership.setStatus(MembershipStatus.ACTIVE); + when(membershipRepository.findById(testMembershipId)).thenReturn(Optional.of(testMembership)); + + assertThrows(IllegalStateException.class, () -> membershipService.approveMembership(testMembershipId)); + } + + @Test + void rejectMembership_setsInactiveStatus() { + when(membershipRepository.findById(testMembershipId)).thenReturn(Optional.of(testMembership)); + when(membershipRepository.save(any())).thenReturn(testMembership); + + membershipService.rejectMembership(testMembershipId); + + assertEquals(MembershipStatus.INACTIVE, testMembership.getStatus()); + assertEquals(LocalDate.now(), testMembership.getValidTo()); + verify(membershipRepository).save(testMembership); + } + + @Test + void rejectMembership_throwsForNonPendingStatus() { + testMembership.setStatus(MembershipStatus.ACTIVE); + when(membershipRepository.findById(testMembershipId)).thenReturn(Optional.of(testMembership)); + + assertThrows(IllegalStateException.class, () -> membershipService.rejectMembership(testMembershipId)); + } + + @Test + void getOwnMemberships_returnsUserMemberships() { + when(membershipRepository.findByUserIdWithDetails(testUserId)).thenReturn(List.of(testMembership)); + + List result = membershipService.getOwnMemberships(testUserId); + + assertEquals(1, result.size()); + assertEquals(testMembershipId, result.get(0).id()); + assertEquals("AT0010000000000000000000001234567", result.get(0).atNumber()); + assertEquals("EEG Wien Mitte", result.get(0).communityName()); + } + + @Test + void getPendingMemberships_returnsPendingRequests() { + when(membershipRepository.findPendingWithDetails()).thenReturn(List.of(testMembership)); + when(userRepository.findAllById(List.of(testUserId))).thenReturn(List.of(testUser)); + + List result = membershipService.getPendingMemberships(); + + assertEquals(1, result.size()); + assertEquals(testMembershipId, result.get(0).id()); + assertEquals("test@example.com", result.get(0).userEmail()); + assertEquals("Test", result.get(0).firstName()); + } +} diff --git a/eeg_frontend/src/app/app.routes.ts b/eeg_frontend/src/app/app.routes.ts index 0e2835d..328776d 100644 --- a/eeg_frontend/src/app/app.routes.ts +++ b/eeg_frontend/src/app/app.routes.ts @@ -11,6 +11,9 @@ import {AdminEnergyCommunity} from './pages/admin-energy-community/admin-energy- import {VerifyEmailComponent} from './pages/verify-email/verify-email'; import {MeteringPointsComponent} from './pages/metering-points/metering-points'; import {ProfileComponent} from './pages/profile/profile'; +import {JoinCommunityComponent} from './pages/join-community/join-community'; +import {MyMembershipsComponent} from './pages/my-memberships/my-memberships'; +import {AdminMembershipsComponent} from './pages/admin-memberships/admin-memberships'; export const routes: Routes = [ { path: '', component: LandingPage }, @@ -28,6 +31,21 @@ export const routes: Routes = [ component: MeteringPointsComponent, canActivate: [roleGuard(['ADMIN', 'MEMBER'])] }, + { + path: 'join-community', + component: JoinCommunityComponent, + canActivate: [roleGuard(['MEMBER'])] + }, + { + path: 'my-memberships', + component: MyMembershipsComponent, + canActivate: [roleGuard(['MEMBER'])] + }, + { + path: 'admin-memberships', + component: AdminMembershipsComponent, + canActivate: [roleGuard(['ADMIN'])] + }, { path: 'approvals', component: AdminUserApprovalComponent, diff --git a/eeg_frontend/src/app/layout/dashboard-layout/dashboard-nav-items.ts b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-nav-items.ts index b3f4dac..c9f1636 100644 --- a/eeg_frontend/src/app/layout/dashboard-layout/dashboard-nav-items.ts +++ b/eeg_frontend/src/app/layout/dashboard-layout/dashboard-nav-items.ts @@ -7,6 +7,9 @@ export interface NavItem { export const NAV_ITEMS: NavItem[] = [ {label: 'Übersicht', path: '/dashboard', roles: ['ADMIN', 'MEMBER']}, {label: 'Zählpunkte', path: '/dashboard/metering-points', roles: ['ADMIN', 'MEMBER']}, + {label: 'Community beitreten', path: '/dashboard/join-community', roles: ['MEMBER']}, + {label: 'Meine Mitgliedschaften', path: '/dashboard/my-memberships', roles: ['MEMBER']}, + {label: 'Mitgliedschaften', path: '/dashboard/admin-memberships', roles: ['ADMIN']}, {label: 'Mein Profil', path: '/dashboard/profile', roles: ['ADMIN', 'MEMBER']}, {label: 'Benutzerverwaltung', path: '/dashboard/approvals', roles: ['ADMIN']}, {label: 'Energiegemeinschaften', path: '/dashboard/energy-communities', roles: ['ADMIN']} diff --git a/eeg_frontend/src/app/pages/admin-memberships/admin-memberships.html b/eeg_frontend/src/app/pages/admin-memberships/admin-memberships.html new file mode 100644 index 0000000..b40211f --- /dev/null +++ b/eeg_frontend/src/app/pages/admin-memberships/admin-memberships.html @@ -0,0 +1,46 @@ +
+

Mitgliedschaftsanträge

+ + @if (isLoading()) { +
+
+
Lade Anträge...
+
+ } @else if (pendingMemberships().length === 0) { +
+

Keine wartenden Mitgliedschaftsanträge vorhanden.

+
+ } @else { +
+ @for (membership of pendingMemberships(); track membership.id) { +
+
+
+
+ {{ membership.firstName }} {{ membership.lastName }} + ({{ membership.userEmail }}) +
+
+ Community: {{ membership.communityName }} | + Zählpunkt: {{ membership.atNumber }} | + Priorität: {{ membership.priorityLevel }} +
+
+
+ + +
+
+
+ } +
+ } +
diff --git a/eeg_frontend/src/app/pages/admin-memberships/admin-memberships.ts b/eeg_frontend/src/app/pages/admin-memberships/admin-memberships.ts new file mode 100644 index 0000000..20866e1 --- /dev/null +++ b/eeg_frontend/src/app/pages/admin-memberships/admin-memberships.ts @@ -0,0 +1,68 @@ +import {Component, inject, OnInit, signal} from '@angular/core'; +import {MembershipService, PendingMembershipResponse} from '../../services/membership'; +import {ToastService} from '../../services/toast'; + +@Component({ + selector: 'app-admin-memberships', + standalone: true, + templateUrl: './admin-memberships.html' +}) +export class AdminMembershipsComponent implements OnInit { + private membershipService = inject(MembershipService); + private toastService = inject(ToastService); + + pendingMemberships = signal([]); + isLoading = signal(true); + isProcessing = signal(null); + + ngOnInit() { + this.loadPendingMemberships(); + } + + loadPendingMemberships() { + this.membershipService.getPendingMemberships().subscribe({ + next: memberships => { + this.pendingMemberships.set(memberships); + this.isLoading.set(false); + }, + error: () => { + this.isLoading.set(false); + this.toastService.error('Fehler beim Laden der wartenden Anträge.'); + } + }); + } + + approveMembership(id: string) { + if (this.isProcessing()) return; + this.isProcessing.set(id); + + this.membershipService.approveMembership(id).subscribe({ + next: () => { + this.toastService.success('Mitgliedschaft genehmigt.'); + this.loadPendingMemberships(); + this.isProcessing.set(null); + }, + error: (err) => { + this.isProcessing.set(null); + this.toastService.error(err.error?.message || 'Fehler bei der Genehmigung.'); + } + }); + } + + rejectMembership(id: string) { + if (this.isProcessing()) return; + this.isProcessing.set(id); + + this.membershipService.rejectMembership(id).subscribe({ + next: () => { + this.toastService.success('Mitgliedschaft abgelehnt.'); + this.loadPendingMemberships(); + this.isProcessing.set(null); + }, + error: (err) => { + this.isProcessing.set(null); + this.toastService.error(err.error?.message || 'Fehler bei der Ablehnung.'); + } + }); + } +} diff --git a/eeg_frontend/src/app/pages/join-community/join-community.html b/eeg_frontend/src/app/pages/join-community/join-community.html new file mode 100644 index 0000000..3926068 --- /dev/null +++ b/eeg_frontend/src/app/pages/join-community/join-community.html @@ -0,0 +1,75 @@ +
+

Energiegemeinschaft beitreten

+ + @if (isLoading()) { +
+
+
Lade Zählpunkte...
+
+ } @else if (meteringPoints().length === 0) { +
+

Keine aktiven Zählpunkte vorhanden.

+

Bitte füge zuerst einen Zählpunkt hinzu und warte auf die Freigabe.

+
+ } @else { + +
+

1. Zählpunkt auswählen

+ +
+ + + @if (selectedMeteringPoint()) { +
+

2. Passende Energiegemeinschaften

+ + @if (isLoadingCommunities()) { +
+
+
Suche passende Communities...
+
+ } @else if (eligibleCommunities().length === 0) { +
+

Keine passenden Energiegemeinschaften für diesen Zählpunkt gefunden.

+
+ } @else { +
+ @for (community of eligibleCommunities(); track community.id) { +
+
+
+

{{ community.name }}

+

{{ community.type }} | EDA-ID: {{ community.edaEcId }}

+
+
+
+ + +
+ +
+
+
+ } +
+ } +
+ } + } +
diff --git a/eeg_frontend/src/app/pages/join-community/join-community.ts b/eeg_frontend/src/app/pages/join-community/join-community.ts new file mode 100644 index 0000000..2fdb741 --- /dev/null +++ b/eeg_frontend/src/app/pages/join-community/join-community.ts @@ -0,0 +1,97 @@ +import {Component, inject, OnInit, signal} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {MeteringPointService, MeteringPoint} from '../../services/metering-point'; +import {MembershipService, EligibleCommunity} from '../../services/membership'; +import {ToastService} from '../../services/toast'; + +@Component({ + selector: 'app-join-community', + standalone: true, + imports: [FormsModule], + templateUrl: './join-community.html' +}) +export class JoinCommunityComponent implements OnInit { + private meteringPointService = inject(MeteringPointService); + private membershipService = inject(MembershipService); + private toastService = inject(ToastService); + + meteringPoints = signal([]); + selectedMeteringPoint = signal(null); + eligibleCommunities = signal([]); + isLoading = signal(true); + isLoadingCommunities = signal(false); + isSubmitting = signal(false); + selectedPriority = signal(1); + + ngOnInit() { + this.loadMeteringPoints(); + } + + loadMeteringPoints() { + this.meteringPointService.getMyMeteringPoints().subscribe({ + next: (points: MeteringPoint[]) => { + // Only show ACTIVE metering points + this.meteringPoints.set(points.filter((p: MeteringPoint) => p.makoState === 'ACTIVE')); + this.isLoading.set(false); + }, + error: () => this.isLoading.set(false) + }); + } + + onMeteringPointChange(event: Event) { + const target = event.target as HTMLSelectElement; + const pointId = target.value; + + if (!pointId) { + this.selectedMeteringPoint.set(null); + this.eligibleCommunities.set([]); + return; + } + + const point = this.meteringPoints().find(p => p.id === pointId); + this.selectedMeteringPoint.set(point || null); + + if (point) { + this.loadEligibleCommunities(point.id); + } + } + + loadEligibleCommunities(meteringPointId: string) { + this.isLoadingCommunities.set(true); + this.eligibleCommunities.set([]); + + this.membershipService.getEligibleCommunities(meteringPointId).subscribe({ + next: communities => { + this.eligibleCommunities.set(communities); + this.isLoadingCommunities.set(false); + }, + error: () => { + this.isLoadingCommunities.set(false); + this.toastService.error('Fehler beim Laden der passenden Communities.'); + } + }); + } + + requestMembership(community: EligibleCommunity) { + if (this.isSubmitting() || !this.selectedMeteringPoint()) return; + + this.isSubmitting.set(true); + + this.membershipService.requestMembership({ + meteringPointId: this.selectedMeteringPoint()!.id, + energyCommunityId: community.id, + priorityLevel: this.selectedPriority() + }).subscribe({ + next: () => { + this.isSubmitting.set(false); + this.toastService.success(`Beitrittsantrag an "${community.name}" erfolgreich gestellt.`); + this.eligibleCommunities.set([]); + this.selectedMeteringPoint.set(null); + }, + error: (err) => { + this.isSubmitting.set(false); + this.toastService.error(err.error?.message || 'Fehler beim Stellen des Beitrittsantrags.'); + } + }); + } +} diff --git a/eeg_frontend/src/app/pages/my-memberships/my-memberships.html b/eeg_frontend/src/app/pages/my-memberships/my-memberships.html new file mode 100644 index 0000000..2beba2f --- /dev/null +++ b/eeg_frontend/src/app/pages/my-memberships/my-memberships.html @@ -0,0 +1,73 @@ +
+

Meine Mitgliedschaften

+ + @if (isLoading()) { +
+
+
Lade Mitgliedschaften...
+
+ } @else { + +
+ + +
+ + @if (filteredMemberships.length === 0) { +
+

Keine Mitgliedschaften gefunden.

+ + Energiegemeinschaft beitreten → + +
+ } @else { + +
+
+ + + + + + + + + + + + @for (membership of filteredMemberships; track membership.id) { + + + + + + + + } + +
CommunityZählpunktPrioritätStatusGültig ab
+
{{ membership.communityName }}
+
+
{{ membership.atNumber }}
+
+
{{ membership.priorityLevel }}
+
+ + {{ getStatusLabel(membership.status) }} + + + {{ membership.validFrom || '-' }} +
+
+
+ } + } +
diff --git a/eeg_frontend/src/app/pages/my-memberships/my-memberships.ts b/eeg_frontend/src/app/pages/my-memberships/my-memberships.ts new file mode 100644 index 0000000..a11d572 --- /dev/null +++ b/eeg_frontend/src/app/pages/my-memberships/my-memberships.ts @@ -0,0 +1,61 @@ +import {Component, inject, OnInit, signal} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {MembershipService, MembershipResponse} from '../../services/membership'; +import {ToastService} from '../../services/toast'; + +@Component({ + selector: 'app-my-memberships', + standalone: true, + imports: [FormsModule], + templateUrl: './my-memberships.html' +}) +export class MyMembershipsComponent implements OnInit { + private membershipService = inject(MembershipService); + private toastService = inject(ToastService); + + memberships = signal([]); + isLoading = signal(true); + filterStatus = signal('ALL'); + + ngOnInit() { + this.loadMemberships(); + } + + loadMemberships() { + this.membershipService.getOwnMemberships().subscribe({ + next: memberships => { + this.memberships.set(memberships); + this.isLoading.set(false); + }, + error: () => { + this.isLoading.set(false); + this.toastService.error('Fehler beim Laden der Mitgliedschaften.'); + } + }); + } + + get filteredMemberships(): MembershipResponse[] { + if (this.filterStatus() === 'ALL') { + return this.memberships(); + } + return this.memberships().filter(m => m.status === this.filterStatus()); + } + + getStatusClass(status: string): string { + switch (status) { + case 'ACTIVE': return 'bg-green-100 text-green-800'; + case 'PENDING': return 'bg-yellow-100 text-yellow-800'; + case 'INACTIVE': return 'bg-gray-100 text-gray-800'; + default: return 'bg-gray-100 text-gray-800'; + } + } + + getStatusLabel(status: string): string { + switch (status) { + case 'ACTIVE': return 'Aktiv'; + case 'PENDING': return 'Wartend'; + case 'INACTIVE': return 'Inaktiv'; + default: return status; + } + } +} diff --git a/eeg_frontend/src/app/services/membership.ts b/eeg_frontend/src/app/services/membership.ts new file mode 100644 index 0000000..d8bb472 --- /dev/null +++ b/eeg_frontend/src/app/services/membership.ts @@ -0,0 +1,69 @@ +import {inject, Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {Observable} from 'rxjs'; + +export interface EligibleCommunity { + id: string; + name: string; + type: string; + edaEcId: string; +} + +export interface MembershipRequest { + meteringPointId: string; + energyCommunityId: string; + priorityLevel: number; +} + +export interface MembershipResponse { + id: string; + meteringPointId: string; + atNumber: string; + energyCommunityId: string; + communityName: string; + priorityLevel: number; + status: string; + validFrom: string | null; + validTo: string | null; +} + +export interface PendingMembershipResponse { + id: string; + userEmail: string; + firstName: string; + lastName: string; + atNumber: string; + communityName: string; + priorityLevel: number; + status: string; +} + +@Injectable({providedIn: 'root'}) +export class MembershipService { + private http = inject(HttpClient); + private apiUrl = 'http://localhost:8080/api/community'; + + getEligibleCommunities(meteringPointId: string): Observable { + return this.http.get(`${this.apiUrl}/metering-points/${meteringPointId}/eligible-communities`); + } + + requestMembership(request: MembershipRequest): Observable { + return this.http.post(`${this.apiUrl}/memberships/request`, request); + } + + getOwnMemberships(): Observable { + return this.http.get(`${this.apiUrl}/memberships/me`); + } + + getPendingMemberships(): Observable { + return this.http.get(`${this.apiUrl}/admin/memberships/pending`); + } + + approveMembership(id: string): Observable { + return this.http.put(`${this.apiUrl}/admin/memberships/${id}/approve`, {}); + } + + rejectMembership(id: string): Observable { + return this.http.put(`${this.apiUrl}/admin/memberships/${id}/reject`, {}); + } +}