diff --git a/eeg_backend/eegportal.trace.db b/eeg_backend/eegportal.trace.db deleted file mode 100644 index a7239e6..0000000 --- a/eeg_backend/eegportal.trace.db +++ /dev/null @@ -1,38 +0,0 @@ -2026-06-16 18:27:44.502501+02:00 database: close -org.h2.message.DbException: Eingabe/Ausgabe Fehler: "Closing" -IO Exception: "Closing" [90028-240] - at org.h2.message.DbException.get(DbException.java:212) - at org.h2.mvstore.db.Store.close(Store.java:352) - at org.h2.engine.Database.closeOpenFilesAndUnlock(Database.java:1314) - at org.h2.engine.Database.closeImpl(Database.java:1276) - at org.h2.engine.Database.close(Database.java:1195) - at org.h2.engine.Database.onShutdown(Database.java:1180) - at org.h2.engine.OnExitDatabaseCloser.onShutdown(OnExitDatabaseCloser.java:85) - at org.h2.engine.OnExitDatabaseCloser.run(OnExitDatabaseCloser.java:114) -Caused by: org.h2.jdbc.JdbcSQLNonTransientException: Eingabe/Ausgabe Fehler: "Closing" -IO Exception: "Closing" [90028-240] - at org.h2.message.DbException.getJdbcSQLException(DbException.java:566) - at org.h2.message.DbException.getJdbcSQLException(DbException.java:489) - ... 8 more -Caused by: org.h2.mvstore.MVStoreException: java.lang.AssertionError: 8 [2.4.240/3] - at org.h2.mvstore.DataUtils.newMVStoreException(DataUtils.java:996) - at org.h2.mvstore.MVStore.executeFilestoreOperation(MVStore.java:1048) - at org.h2.mvstore.RandomAccessStore.compactMoveChunks(RandomAccessStore.java:463) - at org.h2.mvstore.RandomAccessStore.compactStore(RandomAccessStore.java:443) - at org.h2.mvstore.FileStore.compactStore(FileStore.java:891) - at org.h2.mvstore.FileStore.stop(FileStore.java:275) - at org.h2.mvstore.MVStore.closeStore(MVStore.java:803) - at org.h2.mvstore.MVStore.close(MVStore.java:763) - at org.h2.mvstore.db.Store.close(Store.java:348) - ... 6 more -Caused by: java.lang.AssertionError: 8 - at org.h2.mvstore.FreeSpaceBitSet.allocate(FreeSpaceBitSet.java:153) - at org.h2.mvstore.FreeSpaceBitSet.predictAllocation(FreeSpaceBitSet.java:133) - at org.h2.mvstore.RandomAccessStore.predictAllocation(RandomAccessStore.java:101) - at org.h2.mvstore.RandomAccessStore.moveChunkInside(RandomAccessStore.java:628) - at org.h2.mvstore.RandomAccessStore.compactMoveChunks(RandomAccessStore.java:569) - at org.h2.mvstore.RandomAccessStore.compactMoveChunks(RandomAccessStore.java:481) - at org.h2.mvstore.RandomAccessStore.lambda$compactMoveChunks$1(RandomAccessStore.java:468) - at org.h2.mvstore.FileStore.executeFileStoreOperation(FileStore.java:1894) - at org.h2.mvstore.MVStore.executeFilestoreOperation(MVStore.java:1044) - ... 13 more diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/MeteringPointApprovedEvent.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/MeteringPointApprovedEvent.java deleted file mode 100644 index ab89454..0000000 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/common/event/MeteringPointApprovedEvent.java +++ /dev/null @@ -1,9 +0,0 @@ -package at.mueller.eeg.backend.common.event; - -import java.util.UUID; - -public record MeteringPointApprovedEvent( - UUID userId, - String meteringPointNumber, - String type // "BEZUG" oder "EINSPEISUNG" -) {} \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/EnergyCommunityAdminController.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/EnergyCommunityAdminController.java new file mode 100644 index 0000000..99449c1 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/EnergyCommunityAdminController.java @@ -0,0 +1,68 @@ +package at.mueller.eeg.backend.community.api; + +import at.mueller.eeg.backend.community.domain.EnergyCommunity; +import at.mueller.eeg.backend.community.service.EnergyCommunityService; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/api/admin/energy-communities") +@RequiredArgsConstructor +public class EnergyCommunityAdminController { + + private final EnergyCommunityService service; + + // GET /api/admin/energy-communities + @GetMapping + @ApiResponse( + responseCode = "200", + description = "Liste der wartenden Benutzer erfolgreich geladen", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + array = @ArraySchema( + schema = @Schema(implementation = EnergyCommunity.class) + ) + ) + ) + public ResponseEntity> getAllEnergyCommunities() { + return ResponseEntity.ok(service.findAll()); + } + + // GET /api/admin/energy-communities/{id} + @GetMapping("/{id}") + public ResponseEntity getEnergyCommunityById(@PathVariable UUID id) { + return ResponseEntity.ok(service.findById(id)); + } + + // POST /api/admin/energy-communities + @PostMapping + public ResponseEntity createEnergyCommunity(@RequestBody EnergyCommunity community) { + EnergyCommunity createdCommunity = service.create(community); + return ResponseEntity.status(HttpStatus.CREATED).body(createdCommunity); + } + + // PUT /api/admin/energy-communities/{id} + @PutMapping("/{id}") + public ResponseEntity updateEnergyCommunity( + @PathVariable UUID id, + @RequestBody EnergyCommunity community) { + return ResponseEntity.ok(service.update(id, community)); + } + + // DELETE /api/admin/energy-communities/{id} + @DeleteMapping("/{id}") + public ResponseEntity deleteEnergyCommunity(@PathVariable UUID id) { + service.delete(id); + return ResponseEntity.noContent().build(); + } +} \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/domain/MeteringPoint.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/domain/MeteringPoint.java index c62f7fc..699776e 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/domain/MeteringPoint.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/domain/MeteringPoint.java @@ -6,6 +6,7 @@ import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; +import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -28,6 +29,9 @@ public class MeteringPoint { @Enumerated(EnumType.STRING) private PointType type; + @Column(name = "consent_requested_at") + private LocalDateTime consentRequestedAt; + @Column(name = "grid_operator_id", length = 50) private String gridOperatorId; @@ -50,8 +54,17 @@ public class MeteringPoint { public void fireTrigger(MakoTrigger trigger) { this.makoState = switch (this.makoState) { - case NEW -> - trigger == MakoTrigger.SEND_REQUEST ? MakoState.WAITING_FOR_CONSENT : invalid(trigger); + case NEW, ERROR -> switch (trigger) { + case SEND_REQUEST -> { + this.consentRequestedAt = LocalDateTime.now(); + yield MakoState.WAITING_FOR_CONSENT; + } + case TECHNICAL_FAILURE -> { + this.consentRequestedAt = null; + yield MakoState.ERROR; + } + default -> invalid(trigger); + }; case WAITING_FOR_CONSENT -> switch (trigger) { case CONSENT_APPROVED -> MakoState.CONSENT_GRANTED; @@ -67,10 +80,6 @@ public class MeteringPoint { default -> invalid(trigger); }; - // Aus einem Fehlerzustand heraus kann man den Prozess z.B. neu starten - case ERROR -> - trigger == MakoTrigger.SEND_REQUEST ? MakoState.WAITING_FOR_CONSENT : invalid(trigger); - // Endzustände (ACTIVE, REJECTED) erlauben standardmäßig keine weiteren Trigger default -> invalid(trigger); }; diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/repository/EnergyCommunityRepository.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/repository/EnergyCommunityRepository.java index 2e4e4a0..a28c619 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/repository/EnergyCommunityRepository.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/repository/EnergyCommunityRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.List; +import java.util.Optional; import java.util.UUID; public interface EnergyCommunityRepository extends JpaRepository { @@ -21,4 +22,6 @@ public interface EnergyCommunityRepository extends JpaRepository findByEdaEcId(String edaEcId); } diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/service/EnergyCommunityService.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/service/EnergyCommunityService.java new file mode 100644 index 0000000..4c5210c --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/service/EnergyCommunityService.java @@ -0,0 +1,53 @@ +package at.mueller.eeg.backend.community.service; + +import at.mueller.eeg.backend.community.domain.EnergyCommunity; +import at.mueller.eeg.backend.community.repository.EnergyCommunityRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class EnergyCommunityService { + + private final EnergyCommunityRepository repository; + + public List findAll() { + return repository.findAll(); + } + + public EnergyCommunity findById(UUID id) { + return repository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("Energy Community mit ID " + id + " nicht gefunden.")); + } + + @Transactional + public EnergyCommunity create(EnergyCommunity community) { + return repository.save(community); + } + + @Transactional + public EnergyCommunity update(UUID id, EnergyCommunity updatedData) { + EnergyCommunity existing = findById(id); + + existing.setEdaEcId(updatedData.getEdaEcId()); + existing.setType(updatedData.getType()); + existing.setName(updatedData.getName()); + existing.setGridOperatorId(updatedData.getGridOperatorId()); + existing.setSubstationId(updatedData.getSubstationId()); + existing.setTransformerId(updatedData.getTransformerId()); + + return repository.save(existing); + } + + @Transactional + public void delete(UUID id) { + if (!repository.existsById(id)) { + throw new IllegalArgumentException("Energy Community mit ID " + id + " existiert nicht."); + } + repository.deleteById(id); + } +} \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/api/AdminIamController.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/api/AdminIamController.java index ce6197b..4901d71 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/api/AdminIamController.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/api/AdminIamController.java @@ -1,11 +1,9 @@ package at.mueller.eeg.backend.iam.api; -import at.mueller.eeg.backend.common.event.UserApprovedEvent; import at.mueller.eeg.backend.iam.api.dto.IamDtos.UserProfileResponse; -import at.mueller.eeg.backend.iam.domain.RegistrationStatus; import at.mueller.eeg.backend.iam.domain.User; import at.mueller.eeg.backend.iam.mapper.UserMapper; -import at.mueller.eeg.backend.iam.repository.UserRepository; +import at.mueller.eeg.backend.iam.service.AdminIamService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; @@ -13,7 +11,6 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -31,12 +28,9 @@ import java.util.UUID; @RequiredArgsConstructor public class AdminIamController { - private final UserRepository userRepository; - + private final AdminIamService adminIamService; private final UserMapper userMapper; - private final ApplicationEventPublisher eventPublisher; - @GetMapping("/pending") @Operation(summary = "Lädt alle wartenden User", description = "Zeigt User im Status PENDING inkl. der Ergebnisse des EDA-Netz-Checks.") @@ -51,7 +45,7 @@ public class AdminIamController { ) ) public ResponseEntity> getPendingUsers() { - List allByStatus = userRepository.findAllByStatus(RegistrationStatus.PENDING); + List allByStatus = adminIamService.getPendingUsers(); return ResponseEntity.ok(userMapper.mapToDto(allByStatus)); } @@ -59,23 +53,14 @@ public class AdminIamController { @Operation(summary = "User freigeben", description = "Setzt enabled=true, status=APPROVED und triggert die finale EDA-Anmeldung.") public ResponseEntity approveUser(@PathVariable UUID userId) { - User user = userRepository.findById(userId) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")); - - if (!RegistrationStatus.PENDING.equals(user.getStatus())) { - throw new ResponseStatusException(HttpStatus.CONFLICT, "User is not in PENDING status"); + try { + adminIamService.approveUser(userId); + return ResponseEntity.noContent().build(); + } catch (IllegalArgumentException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + } catch (IllegalStateException e) { + throw new ResponseStatusException(HttpStatus.CONFLICT, e.getMessage()); } - if (user.isEnabled()) { - throw new ResponseStatusException(HttpStatus.CONFLICT, "User is already enabled"); - } - - user.setStatus(RegistrationStatus.APPROVED); - user.setEnabled(true); - userRepository.save(user); - - eventPublisher.publishEvent(new UserApprovedEvent(user.getId())); - - return ResponseEntity.noContent().build(); } @PostMapping("/{userId}/reject") @@ -85,6 +70,7 @@ public class AdminIamController { // Logik: status auf REJECTED setzen oder User hard-löschen, je nach Datenschutz-Konzept // Event feuern: UserRejectedEvent + adminIamService.rejectUser(userId); return ResponseEntity.noContent().build(); } } \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/AdminIamService.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/AdminIamService.java new file mode 100644 index 0000000..ded21ef --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/iam/service/AdminIamService.java @@ -0,0 +1,49 @@ +package at.mueller.eeg.backend.iam.service; + +import at.mueller.eeg.backend.common.event.UserApprovedEvent; +import at.mueller.eeg.backend.iam.domain.RegistrationStatus; +import at.mueller.eeg.backend.iam.domain.User; +import at.mueller.eeg.backend.iam.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class AdminIamService { + + private final UserRepository userRepository; + private final ApplicationEventPublisher eventPublisher; + + public List getPendingUsers() { + return userRepository.findAllByStatus(RegistrationStatus.PENDING); + } + + @Transactional + public void approveUser(UUID userId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("User nicht gefunden")); + + if (!RegistrationStatus.PENDING.equals(user.getStatus())) { + throw new IllegalStateException("User ist nicht im Status PENDING"); + } + if (user.isEnabled()) { + throw new IllegalStateException("User ist bereits aktiviert"); + } + + user.setStatus(RegistrationStatus.APPROVED); + user.setEnabled(true); + userRepository.save(user); + + eventPublisher.publishEvent(new UserApprovedEvent(user.getId())); + } + + @Transactional + public void rejectUser(UUID userId) { + // Hier folgt später die Logik für das Ablehnen + } +} \ No newline at end of file diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/event/listener/EdaCommuncationEventListener.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/event/listener/EdaCommuncationEventListener.java index 5117913..e27d23c 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/event/listener/EdaCommuncationEventListener.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/event/listener/EdaCommuncationEventListener.java @@ -1,9 +1,8 @@ package at.mueller.eeg.backend.mako.event.listener; -import at.mueller.eeg.backend.common.event.EdaTopologyCheckEvent; -import at.mueller.eeg.backend.common.event.EdaTopologyCompletedEvent; -import at.mueller.eeg.backend.common.event.EdaTopologyFailedEvent; +import at.mueller.eeg.backend.common.event.*; import at.mueller.eeg.backend.mako.api.dto.EdaTopologyResponse; +import at.mueller.eeg.backend.mako.service.EdaConsentService; import at.mueller.eeg.backend.mako.service.EdaTopologyService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -19,6 +18,7 @@ import org.springframework.stereotype.Component; public class EdaCommuncationEventListener { private final EdaTopologyService edaTopologyService; + private final EdaConsentService edaConsentService; private final ApplicationEventPublisher eventPublisher; @EventListener @@ -51,4 +51,30 @@ public class EdaCommuncationEventListener { )); } } + + @EventListener + @Async + public void handleInitiateConsentEvent(InitiateMakoConsentEvent event) { + log.info("MaKo-Domain: Asynchroner Consent-Request gestartet für AT-Nummer: {} (Typ: {})", + event.atNumber(), event.pointType()); + + try { + edaConsentService.sendConsentRequest(event.atNumber(), event.pointType()); + + log.info("MaKo-Domain: Consent-Request erfolgreich an EDA übermittelt für {}", event.atNumber()); + + // Da der Netzbetreiber jetzt asynchron antwortet (entweder via Webhook oder Mailbox), + // endet der MaKo-Job hier vorerst. Die Community-Domain wartet im Status WAITING_FOR_CONSENT. + + } catch (Exception e) { + log.error("MaKo-Domain: Fehler beim Senden des Consent-Requests für AT-Nummer {}: {}", + event.atNumber(), e.getMessage()); + + eventPublisher.publishEvent(new MakoConsensTechnicalFailedEvent( + event.atNumber(), + e.getMessage() + )); + } + } + } diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/event/listener/MakoCommandListener.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/event/listener/MakoCommandListener.java deleted file mode 100644 index 588d684..0000000 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/mako/event/listener/MakoCommandListener.java +++ /dev/null @@ -1,4 +0,0 @@ -package at.mueller.eeg.backend.mako.event.listener; - -public class MakoCommandListener { -} diff --git a/eeg_frontend/openapi.yaml b/eeg_frontend/openapi.yaml index bd8af08..1e9ede1 100644 --- a/eeg_frontend/openapi.yaml +++ b/eeg_frontend/openapi.yaml @@ -41,6 +41,63 @@ paths: responses: "200": description: OK + /api/admin/energy-communities/{id}: + get: + tags: + - energy-community-admin-controller + operationId: getEnergyCommunityById + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + "200": + description: OK + content: + '*/*': + schema: + $ref: "#/components/schemas/EnergyCommunity" + put: + tags: + - energy-community-admin-controller + operationId: updateEnergyCommunity + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/EnergyCommunity" + required: true + responses: + "200": + description: OK + content: + '*/*': + schema: + $ref: "#/components/schemas/EnergyCommunity" + delete: + tags: + - energy-community-admin-controller + operationId: deleteEnergyCommunity + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + "200": + description: OK /api/community/memberships/request: post: tags: @@ -132,6 +189,37 @@ paths: responses: "200": description: OK + /api/admin/energy-communities: + get: + tags: + - energy-community-admin-controller + operationId: getAllEnergyCommunities + responses: + "200": + description: Liste der wartenden Benutzer erfolgreich geladen + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/EnergyCommunity" + post: + tags: + - energy-community-admin-controller + operationId: createEnergyCommunity + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/EnergyCommunity" + required: true + responses: + "200": + description: OK + content: + '*/*': + schema: + $ref: "#/components/schemas/EnergyCommunity" /api/community/metering-points/{id}/eligible-communities: get: tags: @@ -171,6 +259,28 @@ paths: $ref: "#/components/schemas/UserProfileResponse" components: schemas: + EnergyCommunity: + type: object + properties: + id: + type: string + format: uuid + edaEcId: + type: string + type: + type: string + enum: + - EEG_LOKAL + - EEG_REGIONAL + - BEG + name: + type: string + gridOperatorId: + type: string + substationId: + type: string + transformerId: + type: string MembershipRequest: type: object properties: diff --git a/eeg_frontend/src/app/app.html b/eeg_frontend/src/app/app.html index 26be869..c385ddd 100644 --- a/eeg_frontend/src/app/app.html +++ b/eeg_frontend/src/app/app.html @@ -1,6 +1,7 @@ - - +@if (!isAdminRoute) { + +}
diff --git a/eeg_frontend/src/app/app.routes.ts b/eeg_frontend/src/app/app.routes.ts index 88e63b3..a951c8a 100644 --- a/eeg_frontend/src/app/app.routes.ts +++ b/eeg_frontend/src/app/app.routes.ts @@ -4,14 +4,34 @@ import {Register} from './pages/register/register'; import {LoginComponent} from './pages/login/login'; import {AdminUserApprovalComponent} from './pages/admin-user-approval/admin-user-approval'; import {adminAuthGuard} from './guards/admin-auth-guard'; +import {AppLayout} from './layout/app-layout/app-layout'; +import {AdminEnergyCommunity} from './pages/admin-energy-community/admin-energy-community'; export const routes: Routes = [ { path: '', component: LandingPage }, { path: 'register', component: Register }, { path: 'login', component: LoginComponent}, { - path: 'admin/approvals', - component: AdminUserApprovalComponent, - canActivate: [adminAuthGuard] - } + path: 'admin', + component: AppLayout, + canActivate: [adminAuthGuard], + children: [ + { + path: '', + redirectTo: 'approvals', + pathMatch: 'full' + }, + { + path: 'approvals', + component: AdminUserApprovalComponent + }, + { + path: 'energy-communities', + component: AdminEnergyCommunity + } + ] + }, + + // Wildcard-Fallback für unbekannte URLs + { path: '**', redirectTo: '' } ]; diff --git a/eeg_frontend/src/app/app.ts b/eeg_frontend/src/app/app.ts index 3991aba..044bc87 100644 --- a/eeg_frontend/src/app/app.ts +++ b/eeg_frontend/src/app/app.ts @@ -1,5 +1,5 @@ import {Component, signal} from '@angular/core'; -import {RouterOutlet} from '@angular/router'; +import {NavigationEnd, Router, RouterOutlet} from '@angular/router'; import {HeaderComponent} from './layout/header/header'; @Component({ @@ -10,4 +10,14 @@ import {HeaderComponent} from './layout/header/header'; }) export class App { protected readonly title = signal('eeg_frontend'); + + isAdminRoute = false; + + constructor(private router: Router) { + this.router.events.subscribe(event => { + if (event instanceof NavigationEnd) { + this.isAdminRoute = event.urlAfterRedirects.startsWith('/admin'); + } + }); + } } diff --git a/eeg_frontend/src/app/guards/admin-auth-guard.ts b/eeg_frontend/src/app/guards/admin-auth-guard.ts index f76c225..51da2c3 100644 --- a/eeg_frontend/src/app/guards/admin-auth-guard.ts +++ b/eeg_frontend/src/app/guards/admin-auth-guard.ts @@ -1,6 +1,6 @@ import {inject} from '@angular/core'; import {CanActivateFn, Router} from '@angular/router'; -import {AuthService} from '../services/auth'; // Adjust path if necessary +import {AuthService} from '../services/auth'; export const adminAuthGuard: CanActivateFn = (route, state) => { const authService = inject(AuthService); @@ -8,7 +8,7 @@ export const adminAuthGuard: CanActivateFn = (route, state) => { // Replace these with your actual authentication and role-checking methods const isAuthenticated = authService.isLoggedIn(); - const isAdmin = authService.getUserRole() === 'ADMIN'; + const isAdmin = authService.currentUserRole() === 'ADMIN'; if (isAuthenticated && isAdmin) { return true; } diff --git a/eeg_frontend/src/app/layout/app-layout/app-layout.html b/eeg_frontend/src/app/layout/app-layout/app-layout.html new file mode 100644 index 0000000..19617de --- /dev/null +++ b/eeg_frontend/src/app/layout/app-layout/app-layout.html @@ -0,0 +1,47 @@ +
+ + + +
+ +
+

Verwaltung

+ +
+ + + + + Admin +
+
+ +
+ +
+ +
+ +
diff --git a/eeg_frontend/src/app/layout/app-layout/app-layout.scss b/eeg_frontend/src/app/layout/app-layout/app-layout.scss new file mode 100644 index 0000000..e69de29 diff --git a/eeg_frontend/src/app/layout/app-layout/app-layout.spec.ts b/eeg_frontend/src/app/layout/app-layout/app-layout.spec.ts new file mode 100644 index 0000000..9ee1bd0 --- /dev/null +++ b/eeg_frontend/src/app/layout/app-layout/app-layout.spec.ts @@ -0,0 +1,22 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {AppLayout} from './app-layout'; + +describe('AdminLayout', () => { + let component: AppLayout; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppLayout], + }).compileComponents(); + + fixture = TestBed.createComponent(AppLayout); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/eeg_frontend/src/app/layout/app-layout/app-layout.ts b/eeg_frontend/src/app/layout/app-layout/app-layout.ts new file mode 100644 index 0000000..3cadcc2 --- /dev/null +++ b/eeg_frontend/src/app/layout/app-layout/app-layout.ts @@ -0,0 +1,11 @@ +import {Component} from '@angular/core'; +import {RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router'; + +@Component({ + selector: 'app-admin-layout', + standalone: true, + imports: [RouterOutlet, RouterLink, RouterLinkActive], + templateUrl: './app-layout.html', + styleUrl: './app-layout.scss' +}) +export class AppLayout {} diff --git a/eeg_frontend/src/app/layout/header/header.html b/eeg_frontend/src/app/layout/header/header.html index 864abcc..7aa62f7 100644 --- a/eeg_frontend/src/app/layout/header/header.html +++ b/eeg_frontend/src/app/layout/header/header.html @@ -25,7 +25,7 @@ @if (authService.isLoggedIn$ | async) { + + +
+ + + + + + + + + + + + @for (ec of communities; track ec.id) { + + + + + + + + } @empty { + + + + } + +
NameEDA IDTypNetzbetreiberAktionen
{{ ec.name }}{{ ec.edaEcId }} + + {{ ec.type }} + + {{ ec.gridOperatorId || '-' }} + + +
+
+ Keine Daten gefunden + Es wurden noch keine Energiegemeinschaften angelegt. +
+
+
+ + @if (isModalOpen) { +
+ +
+ +
+

Neue Energiegemeinschaft anlegen

+ +
+ +
+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+

Netz-Topologie (Optional)

+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ +
+
+ +
+ + +
+ +
+
+ } + + diff --git a/eeg_frontend/src/app/pages/admin-energy-community/admin-energy-community.scss b/eeg_frontend/src/app/pages/admin-energy-community/admin-energy-community.scss new file mode 100644 index 0000000..e69de29 diff --git a/eeg_frontend/src/app/pages/admin-energy-community/admin-energy-community.spec.ts b/eeg_frontend/src/app/pages/admin-energy-community/admin-energy-community.spec.ts new file mode 100644 index 0000000..c215eb5 --- /dev/null +++ b/eeg_frontend/src/app/pages/admin-energy-community/admin-energy-community.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminEnergyCommunity } from './admin-energy-community'; + +describe('AdminEnergyCommunity', () => { + let component: AdminEnergyCommunity; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AdminEnergyCommunity], + }).compileComponents(); + + fixture = TestBed.createComponent(AdminEnergyCommunity); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/eeg_frontend/src/app/pages/admin-energy-community/admin-energy-community.ts b/eeg_frontend/src/app/pages/admin-energy-community/admin-energy-community.ts new file mode 100644 index 0000000..9a95254 --- /dev/null +++ b/eeg_frontend/src/app/pages/admin-energy-community/admin-energy-community.ts @@ -0,0 +1,116 @@ +import {Component, inject, OnInit} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {FormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; +import {EnergyCommunity, EnergyCommunityAdminControllerService} from '../../api'; + +@Component({ + selector: 'app-admin-energy-community', + imports: [CommonModule, ReactiveFormsModule], + templateUrl: './admin-energy-community.html', + styleUrl: './admin-energy-community.scss', +}) +export class AdminEnergyCommunity implements OnInit { + private ecService = inject(EnergyCommunityAdminControllerService); + private fb = inject(FormBuilder); + + communities: EnergyCommunity[] = []; + + isModalOpen = false; + isEditMode = false; + editingId: string | null = null; + + createForm = this.fb.group({ + name: ['', Validators.required], + edaEcId: ['', Validators.required], + type: ['', Validators.required], + gridOperatorId: [''], + substationId: [''], + transformerId: [''] + }); + + ngOnInit() { + this.loadCommunities(); + } + + loadCommunities() { + this.ecService.getAllEnergyCommunities().subscribe({ + next: (data) => { + this.communities = data; + }, + error: (err) => { + console.error('Fehler beim Laden der Energiegemeinschaften', err); + // Hier würde später ein globaler Error-Toast getriggert werden + } + }); + } + + openCreateModal(): void { + this.isEditMode = false; + this.editingId = null; + this.createForm.reset(); + this.createForm.get('edaEcId')?.enable(); + this.createForm.get('type')?.enable(); + + this.isModalOpen = true; + } + + openEditModal(ec: EnergyCommunity): void { + this.isEditMode = true; + this.editingId = ec.id ?? null; + this.createForm.patchValue(ec as any); + this.createForm.get('edaEcId')?.disable(); + this.createForm.get('type')?.disable(); + + this.isModalOpen = true; + } + + closeModal(): void { + this.isModalOpen = false; + this.createForm.reset(); + } + + onSubmit(): void { + if (this.createForm.valid) { + const formData = this.createForm.getRawValue(); + + if (this.isEditMode && this.editingId) { + this.ecService.updateEnergyCommunity(this.editingId, formData as any).subscribe({ + next: (updatedEc) => { + const index = this.communities.findIndex(c => c.id === this.editingId); + if (index !== -1) this.communities[index] = updatedEc; + this.closeModal(); + }, + error: (err) => console.error('Fehler beim Updaten', err) + }); + } else { + this.ecService.createEnergyCommunity(formData as any).subscribe({ + next: (newCommunity) => { + this.communities.push(newCommunity); + this.closeModal(); + }, + error: (err) => console.error('Fehler beim Erstellen', err) + }); + } + } else { + this.createForm.markAllAsTouched(); + } + } + + canDelete(ec: EnergyCommunity): boolean { + // Wenn dein Backend z.B. die Anzahl der Mitglieder mitliefert: + // return !(ec as any).memberCount || (ec as any).memberCount === 0; + return true; // Vorerst true, bis die Relation im DTO steht + } + + onDelete(id: string | undefined): void { + if (!id) return; + if (confirm('Möchtest du diese Energiegemeinschaft wirklich löschen?')) { + this.ecService.deleteEnergyCommunity(id).subscribe({ + next: () => { + this.communities = this.communities.filter(c => c.id !== id); + }, + error: (err) => console.error('Löschen fehlgeschlagen', err) + }); + } + } +} diff --git a/eeg_frontend/src/app/services/auth.ts b/eeg_frontend/src/app/services/auth.ts index 2b62524..1e60b55 100644 --- a/eeg_frontend/src/app/services/auth.ts +++ b/eeg_frontend/src/app/services/auth.ts @@ -1,4 +1,4 @@ -import {inject, Injectable} from '@angular/core'; +import {inject, Injectable, signal} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {BehaviorSubject, Observable, tap} from 'rxjs'; import {AuthResponse} from '../api'; @@ -16,8 +16,9 @@ export class AuthService { isLoggedIn$ = this.isLoggedInSubject.asObservable(); + currentUserRole = signal(localStorage.getItem('user_role')); + register(userData: any): Observable { - // Sendet die Daten als JSON-Body an den Endpunkt /register return this.http.post(`${this.apiUrl}/register`, userData); } @@ -27,13 +28,12 @@ export class AuthService { // Token und Rolle im LocalStorage (oder SessionStorage) ablegen localStorage.setItem('token', response.token!); localStorage.setItem('user_role', response.role!); + this.isLoggedInSubject.next(true); + this.currentUserRole.set(response.role!); }) ); } - getUserRole(): string | null { - return localStorage.getItem('user_role'); - } isLoggedIn(): boolean { return !!localStorage.getItem('token');