feat(community): add complete membership flow with unit tests
Backend: - Add MembershipResponse and PendingMembershipResponse DTOs - Add GET /memberships/me and GET /admin/memberships/pending endpoints - Add findByUserIdWithDetails and findPendingWithDetails queries - Add getOwnMemberships and getPendingMemberships service methods Frontend: - Add membership service for API calls - Add join-community page for users to browse and join communities - Add my-memberships page for users to view their memberships - Add admin-memberships page for admins to approve/reject requests - Add routes and navigation items Tests: - Add MembershipServiceTest with 13 unit tests - All 20 tests passing (UserServiceTest + MembershipServiceTest)
This commit is contained in:
parent
838fdb2826
commit
c66c5fddfa
@ -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<List<MembershipResponse>> getOwnMemberships(@CurrentUserId String userId) {
|
||||
return ResponseEntity.ok(membershipService.getOwnMemberships(UUID.fromString(userId)));
|
||||
}
|
||||
|
||||
// --- ADMIN ENDPUNKTE ---
|
||||
|
||||
@GetMapping("/admin/memberships/pending")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public ResponseEntity<List<PendingMembershipResponse>> 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<Void> approveMembership(@PathVariable UUID id) {
|
||||
membershipService.approveMembership(id);
|
||||
return ResponseEntity.ok().build();
|
||||
|
||||
@ -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
|
||||
) {}
|
||||
@ -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
|
||||
) {}
|
||||
@ -25,4 +25,10 @@ public interface MembershipRepository extends JpaRepository<Membership, UUID> {
|
||||
|
||||
@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);
|
||||
|
||||
@Query("SELECT m FROM Membership m JOIN FETCH m.meteringPoint mp JOIN FETCH m.energyCommunity ec WHERE mp.userId = :userId")
|
||||
List<Membership> 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<Membership> findPendingWithDetails();
|
||||
}
|
||||
@ -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<EligibleCommunityResponse> 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<MembershipResponse> 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<PendingMembershipResponse> getPendingMemberships() {
|
||||
List<Membership> pendingMemberships = membershipRepository.findPendingWithDetails();
|
||||
|
||||
// Collect all user IDs from metering points
|
||||
List<UUID> userIds = pendingMemberships.stream()
|
||||
.map(m -> m.getMeteringPoint().getUserId())
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
// Fetch users and create a lookup map
|
||||
Map<UUID, User> 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();
|
||||
}
|
||||
}
|
||||
@ -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<EligibleCommunityResponse> 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<EligibleCommunityResponse> 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<MembershipResponse> 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<PendingMembershipResponse> 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());
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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']}
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
<div class="max-w-4xl mx-auto p-4 md:p-6">
|
||||
<h1 class="text-2xl font-bold mb-6">Mitgliedschaftsanträge</h1>
|
||||
|
||||
@if (isLoading()) {
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mb-2"></div>
|
||||
<div>Lade Anträge...</div>
|
||||
</div>
|
||||
} @else if (pendingMemberships().length === 0) {
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-6 text-center">
|
||||
<p class="text-gray-600">Keine wartenden Mitgliedschaftsanträge vorhanden.</p>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="space-y-4">
|
||||
@for (membership of pendingMemberships(); track membership.id) {
|
||||
<div class="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="font-semibold text-gray-900">{{ membership.firstName }} {{ membership.lastName }}</span>
|
||||
<span class="text-sm text-gray-500">({{ membership.userEmail }})</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-600">
|
||||
<span class="font-medium">Community:</span> {{ membership.communityName }} |
|
||||
<span class="font-medium">Zählpunkt:</span> {{ membership.atNumber }} |
|
||||
<span class="font-medium">Priorität:</span> {{ membership.priorityLevel }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button (click)="approveMembership(membership.id)"
|
||||
[disabled]="isProcessing() === membership.id"
|
||||
class="bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 disabled:opacity-50 text-sm font-medium">
|
||||
{{ isProcessing() === membership.id ? '...' : 'Genehmigen' }}
|
||||
</button>
|
||||
<button (click)="rejectMembership(membership.id)"
|
||||
[disabled]="isProcessing() === membership.id"
|
||||
class="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700 disabled:opacity-50 text-sm font-medium">
|
||||
{{ isProcessing() === membership.id ? '...' : 'Ablehnen' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@ -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<PendingMembershipResponse[]>([]);
|
||||
isLoading = signal(true);
|
||||
isProcessing = signal<string | null>(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.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
<div class="max-w-4xl mx-auto p-4 md:p-6">
|
||||
<h1 class="text-2xl font-bold mb-6">Energiegemeinschaft beitreten</h1>
|
||||
|
||||
@if (isLoading()) {
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mb-2"></div>
|
||||
<div>Lade Zählpunkte...</div>
|
||||
</div>
|
||||
} @else if (meteringPoints().length === 0) {
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-6 text-center">
|
||||
<p class="text-yellow-800">Keine aktiven Zählpunkte vorhanden.</p>
|
||||
<p class="text-yellow-600 text-sm mt-2">Bitte füge zuerst einen Zählpunkt hinzu und warte auf die Freigabe.</p>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Zählpunkt auswählen -->
|
||||
<div class="bg-white shadow rounded-lg p-6 mb-6">
|
||||
<h2 class="text-lg font-semibold mb-4">1. Zählpunkt auswählen</h2>
|
||||
<select (change)="onMeteringPointChange($event)"
|
||||
class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">-- Zählpunkt wählen --</option>
|
||||
@for (point of meteringPoints(); track point.id) {
|
||||
<option [value]="point.id">{{ point.atNumber }} ({{ point.type }})</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Passende Communities -->
|
||||
@if (selectedMeteringPoint()) {
|
||||
<div class="bg-white shadow rounded-lg p-6 mb-6">
|
||||
<h2 class="text-lg font-semibold mb-4">2. Passende Energiegemeinschaften</h2>
|
||||
|
||||
@if (isLoadingCommunities()) {
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mb-2"></div>
|
||||
<div>Suche passende Communities...</div>
|
||||
</div>
|
||||
} @else if (eligibleCommunities().length === 0) {
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-6 text-center">
|
||||
<p class="text-gray-600">Keine passenden Energiegemeinschaften für diesen Zählpunkt gefunden.</p>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="space-y-4">
|
||||
@for (community of eligibleCommunities(); track community.id) {
|
||||
<div class="border border-gray-200 rounded-lg p-4 hover:border-blue-300 transition-colors">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900">{{ community.name }}</h3>
|
||||
<p class="text-sm text-gray-500">{{ community.type }} | EDA-ID: {{ community.edaEcId }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Priorität</label>
|
||||
<select [(ngModel)]="selectedPriority"
|
||||
class="border border-gray-300 rounded-md px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option [value]="1">1</option>
|
||||
<option [value]="2">2</option>
|
||||
<option [value]="3">3</option>
|
||||
<option [value]="4">4</option>
|
||||
<option [value]="5">5</option>
|
||||
</select>
|
||||
</div>
|
||||
<button (click)="requestMembership(community)" [disabled]="isSubmitting()"
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:opacity-50 text-sm">
|
||||
{{ isSubmitting() ? 'Beantrage...' : 'Beitreten' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
97
eeg_frontend/src/app/pages/join-community/join-community.ts
Normal file
97
eeg_frontend/src/app/pages/join-community/join-community.ts
Normal file
@ -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<MeteringPoint[]>([]);
|
||||
selectedMeteringPoint = signal<MeteringPoint | null>(null);
|
||||
eligibleCommunities = signal<EligibleCommunity[]>([]);
|
||||
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.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
<div class="max-w-4xl mx-auto p-4 md:p-6">
|
||||
<h1 class="text-2xl font-bold mb-6">Meine Mitgliedschaften</h1>
|
||||
|
||||
@if (isLoading()) {
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mb-2"></div>
|
||||
<div>Lade Mitgliedschaften...</div>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Filter -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Filter nach Status</label>
|
||||
<select [(ngModel)]="filterStatus"
|
||||
class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="ALL">Alle</option>
|
||||
<option value="ACTIVE">Aktiv</option>
|
||||
<option value="PENDING">Wartend</option>
|
||||
<option value="INACTIVE">Inaktiv</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@if (filteredMemberships.length === 0) {
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-6 text-center">
|
||||
<p class="text-gray-600">Keine Mitgliedschaften gefunden.</p>
|
||||
<a routerLink="/dashboard/join-community"
|
||||
class="inline-block mt-4 text-blue-600 hover:text-blue-800 font-medium">
|
||||
Energiegemeinschaft beitreten →
|
||||
</a>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Tabelle -->
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Community</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Zählpunkt</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Priorität</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Gültig ab</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@for (membership of filteredMemberships; track membership.id) {
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">{{ membership.communityName }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">{{ membership.atNumber }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">{{ membership.priorityLevel }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full"
|
||||
[class]="getStatusClass(membership.status)">
|
||||
{{ getStatusLabel(membership.status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ membership.validFrom || '-' }}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
61
eeg_frontend/src/app/pages/my-memberships/my-memberships.ts
Normal file
61
eeg_frontend/src/app/pages/my-memberships/my-memberships.ts
Normal file
@ -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<MembershipResponse[]>([]);
|
||||
isLoading = signal(true);
|
||||
filterStatus = signal<string>('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;
|
||||
}
|
||||
}
|
||||
}
|
||||
69
eeg_frontend/src/app/services/membership.ts
Normal file
69
eeg_frontend/src/app/services/membership.ts
Normal file
@ -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<EligibleCommunity[]> {
|
||||
return this.http.get<EligibleCommunity[]>(`${this.apiUrl}/metering-points/${meteringPointId}/eligible-communities`);
|
||||
}
|
||||
|
||||
requestMembership(request: MembershipRequest): Observable<string> {
|
||||
return this.http.post<string>(`${this.apiUrl}/memberships/request`, request);
|
||||
}
|
||||
|
||||
getOwnMemberships(): Observable<MembershipResponse[]> {
|
||||
return this.http.get<MembershipResponse[]>(`${this.apiUrl}/memberships/me`);
|
||||
}
|
||||
|
||||
getPendingMemberships(): Observable<PendingMembershipResponse[]> {
|
||||
return this.http.get<PendingMembershipResponse[]>(`${this.apiUrl}/admin/memberships/pending`);
|
||||
}
|
||||
|
||||
approveMembership(id: string): Observable<void> {
|
||||
return this.http.put<void>(`${this.apiUrl}/admin/memberships/${id}/approve`, {});
|
||||
}
|
||||
|
||||
rejectMembership(id: string): Observable<void> {
|
||||
return this.http.put<void>(`${this.apiUrl}/admin/memberships/${id}/reject`, {});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user