diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/MeteringPointController.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/MeteringPointController.java index 7d7885a..380b03a 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/MeteringPointController.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/MeteringPointController.java @@ -3,6 +3,7 @@ package at.mueller.eeg.backend.community.api; import at.mueller.eeg.backend.common.security.CurrentUserId; import at.mueller.eeg.backend.community.api.dto.CreateMeteringPointRequest; import at.mueller.eeg.backend.community.api.dto.MeteringPointResponse; +import at.mueller.eeg.backend.community.api.dto.UpdateMeteringPointRequest; import at.mueller.eeg.backend.community.service.MeteringPointService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -47,6 +48,16 @@ public class MeteringPointController { return ResponseEntity.noContent().build(); } + @PutMapping("/{id}") + @PreAuthorize("hasRole('MEMBER')") + public ResponseEntity updateOwnMeteringPoint( + @CurrentUserId String userId, + @PathVariable UUID id, + @Valid @RequestBody UpdateMeteringPointRequest request) { + MeteringPointResponse updated = meteringPointService.updateOwnMeteringPoint(UUID.fromString(userId), id, request); + return ResponseEntity.ok(updated); + } + // --- ADMIN: alle Zählpunkte im System --- @GetMapping diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/dto/UpdateMeteringPointRequest.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/dto/UpdateMeteringPointRequest.java new file mode 100644 index 0000000..f9f71c9 --- /dev/null +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/api/dto/UpdateMeteringPointRequest.java @@ -0,0 +1,10 @@ +package at.mueller.eeg.backend.community.api.dto; + +import at.mueller.eeg.backend.community.domain.PointType; +import jakarta.validation.constraints.NotNull; + +public record UpdateMeteringPointRequest( + @NotNull + PointType type +) { +} diff --git a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/service/MeteringPointService.java b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/service/MeteringPointService.java index b79660d..880139e 100644 --- a/eeg_backend/src/main/java/at/mueller/eeg/backend/community/service/MeteringPointService.java +++ b/eeg_backend/src/main/java/at/mueller/eeg/backend/community/service/MeteringPointService.java @@ -4,6 +4,7 @@ import at.mueller.eeg.backend.common.event.InitiateMakoConsentEvent; import at.mueller.eeg.backend.common.exception.AtNumberAlreadyExistsException; import at.mueller.eeg.backend.community.api.dto.CreateMeteringPointRequest; import at.mueller.eeg.backend.community.api.dto.MeteringPointResponse; +import at.mueller.eeg.backend.community.api.dto.UpdateMeteringPointRequest; import at.mueller.eeg.backend.community.domain.MeteringPoint; import at.mueller.eeg.backend.community.domain.state.MakoState; import at.mueller.eeg.backend.community.domain.state.MakoTrigger; @@ -69,6 +70,24 @@ public class MeteringPointService { .toList(); } + @Transactional + public MeteringPointResponse updateOwnMeteringPoint(UUID userId, UUID pointId, UpdateMeteringPointRequest request) { + MeteringPoint point = meteringPointRepository.findById(pointId) + .orElseThrow(() -> new IllegalArgumentException("Zählpunkt nicht gefunden")); + + if (!point.getUserId().equals(userId)) { + throw new org.springframework.security.access.AccessDeniedException("Keine Berechtigung zum Bearbeiten dieses Zählpunkts"); + } + + if (point.getMakoState() != MakoState.NEW && point.getMakoState() != MakoState.REJECTED && point.getMakoState() != MakoState.ERROR) { + throw new IllegalStateException("Zählpunkte im Consent-Flow oder aktive Zählpunkte können nicht bearbeitet werden"); + } + + point.setType(request.type()); + MeteringPoint saved = meteringPointRepository.save(point); + return toResponse(saved, null); + } + public List getAllMeteringPoints() { List allPoints = meteringPointRepository.findAll(); diff --git a/eeg_backend/src/test/java/at/mueller/eeg/backend/community/service/MeteringPointServiceTest.java b/eeg_backend/src/test/java/at/mueller/eeg/backend/community/service/MeteringPointServiceTest.java index 6afe155..7052088 100644 --- a/eeg_backend/src/test/java/at/mueller/eeg/backend/community/service/MeteringPointServiceTest.java +++ b/eeg_backend/src/test/java/at/mueller/eeg/backend/community/service/MeteringPointServiceTest.java @@ -4,6 +4,7 @@ import at.mueller.eeg.backend.common.event.InitiateMakoConsentEvent; import at.mueller.eeg.backend.common.exception.AtNumberAlreadyExistsException; import at.mueller.eeg.backend.community.api.dto.CreateMeteringPointRequest; import at.mueller.eeg.backend.community.api.dto.MeteringPointResponse; +import at.mueller.eeg.backend.community.api.dto.UpdateMeteringPointRequest; import at.mueller.eeg.backend.community.domain.MeteringPoint; import at.mueller.eeg.backend.community.domain.PointType; import at.mueller.eeg.backend.community.domain.state.MakoState; @@ -162,4 +163,81 @@ class MeteringPointServiceTest { assertThrows(IllegalArgumentException.class, () -> meteringPointService.deleteOwnMeteringPoint(testUserId, UUID.randomUUID())); } + + @Test + void updateOwnMeteringPoint_updatesType() { + when(meteringPointRepository.findById(testPointId)).thenReturn(Optional.of(testMeteringPoint)); + when(meteringPointRepository.save(any())).thenReturn(testMeteringPoint); + + UpdateMeteringPointRequest request = new UpdateMeteringPointRequest(PointType.PRODUCER); + MeteringPointResponse response = meteringPointService.updateOwnMeteringPoint(testUserId, testPointId, request); + + assertNotNull(response); + verify(meteringPointRepository).save(any()); + } + + @Test + void updateOwnMeteringPoint_throwsForWrongOwner() { + UUID otherUserId = UUID.randomUUID(); + when(meteringPointRepository.findById(testPointId)).thenReturn(Optional.of(testMeteringPoint)); + + UpdateMeteringPointRequest request = new UpdateMeteringPointRequest(PointType.PRODUCER); + assertThrows(org.springframework.security.access.AccessDeniedException.class, () -> + meteringPointService.updateOwnMeteringPoint(otherUserId, testPointId, request)); + } + + @Test + void updateOwnMeteringPoint_throwsForActivePoint() { + testMeteringPoint.setMakoState(MakoState.ACTIVE); + when(meteringPointRepository.findById(testPointId)).thenReturn(Optional.of(testMeteringPoint)); + + UpdateMeteringPointRequest request = new UpdateMeteringPointRequest(PointType.PRODUCER); + assertThrows(IllegalStateException.class, () -> + meteringPointService.updateOwnMeteringPoint(testUserId, testPointId, request)); + } + + @Test + void updateOwnMeteringPoint_throwsForWaitingPoint() { + testMeteringPoint.setMakoState(MakoState.WAITING_FOR_CONSENT); + when(meteringPointRepository.findById(testPointId)).thenReturn(Optional.of(testMeteringPoint)); + + UpdateMeteringPointRequest request = new UpdateMeteringPointRequest(PointType.PRODUCER); + assertThrows(IllegalStateException.class, () -> + meteringPointService.updateOwnMeteringPoint(testUserId, testPointId, request)); + } + + @Test + void updateOwnMeteringPoint_allowsUpdateForRejectedPoint() { + testMeteringPoint.setMakoState(MakoState.REJECTED); + when(meteringPointRepository.findById(testPointId)).thenReturn(Optional.of(testMeteringPoint)); + when(meteringPointRepository.save(any())).thenReturn(testMeteringPoint); + + UpdateMeteringPointRequest request = new UpdateMeteringPointRequest(PointType.PRODUCER); + MeteringPointResponse response = meteringPointService.updateOwnMeteringPoint(testUserId, testPointId, request); + + assertNotNull(response); + verify(meteringPointRepository).save(any()); + } + + @Test + void updateOwnMeteringPoint_allowsUpdateForErrorPoint() { + testMeteringPoint.setMakoState(MakoState.ERROR); + when(meteringPointRepository.findById(testPointId)).thenReturn(Optional.of(testMeteringPoint)); + when(meteringPointRepository.save(any())).thenReturn(testMeteringPoint); + + UpdateMeteringPointRequest request = new UpdateMeteringPointRequest(PointType.PRODUCER); + MeteringPointResponse response = meteringPointService.updateOwnMeteringPoint(testUserId, testPointId, request); + + assertNotNull(response); + verify(meteringPointRepository).save(any()); + } + + @Test + void updateOwnMeteringPoint_throwsForUnknownPoint() { + when(meteringPointRepository.findById(any())).thenReturn(Optional.empty()); + + UpdateMeteringPointRequest request = new UpdateMeteringPointRequest(PointType.PRODUCER); + assertThrows(IllegalArgumentException.class, () -> + meteringPointService.updateOwnMeteringPoint(testUserId, UUID.randomUUID(), request)); + } } diff --git a/eeg_frontend/src/app/pages/metering-points/metering-points.html b/eeg_frontend/src/app/pages/metering-points/metering-points.html index 52871a2..88d9598 100644 --- a/eeg_frontend/src/app/pages/metering-points/metering-points.html +++ b/eeg_frontend/src/app/pages/metering-points/metering-points.html @@ -64,18 +64,47 @@ @for (point of points(); track point.id) { {{ point.atNumber }} - {{ point.type }} + + @if (editingId() === point.id) { + + + Verbraucher + Erzeuger + Prosumer + + + } @else { + {{ point.type }} + } + {{ point.makoState }} @if (isAdmin) { {{ point.ownerEmail }} } - @if (canDelete(point)) { - + + } @else { + @if (canEdit(point)) { + + } + @if (canDelete(point)) { + + } } diff --git a/eeg_frontend/src/app/pages/metering-points/metering-points.ts b/eeg_frontend/src/app/pages/metering-points/metering-points.ts index 405467b..57a8255 100644 --- a/eeg_frontend/src/app/pages/metering-points/metering-points.ts +++ b/eeg_frontend/src/app/pages/metering-points/metering-points.ts @@ -26,12 +26,17 @@ export class MeteringPointsComponent implements OnInit { isLoading = signal(true); errorMessage = signal(''); deletingId = signal(null); + editingId = signal(null); addForm = this.fb.group({ atNumber: ['', [Validators.required, Validators.pattern(/^AT[0-9]{31}$/)]], type: this.fb.control('CONSUMER', {nonNullable: true, validators: Validators.required}) }); + editForm = this.fb.group({ + type: this.fb.control('CONSUMER', {nonNullable: true, validators: Validators.required}) + }); + ngOnInit() { this.loadPoints(); } @@ -77,6 +82,36 @@ export class MeteringPointsComponent implements OnInit { }); } + canEdit(point: MeteringPoint): boolean { + return !this.isAdmin && (point.makoState === 'NEW' || point.makoState === 'REJECTED' || point.makoState === 'ERROR'); + } + + startEdit(point: MeteringPoint) { + this.editingId.set(point.id); + this.editForm.patchValue({type: point.type}); + } + + cancelEdit() { + this.editingId.set(null); + } + + onEditSubmit(point: MeteringPoint) { + if (this.editForm.invalid) return; + + const payload = {type: this.editForm.value.type!}; + + this.meteringPointService.updateMeteringPoint(point.id, payload).subscribe({ + next: updated => { + this.points.update(list => list.map(p => p.id === updated.id ? updated : p)); + this.editingId.set(null); + this.toastService.success('Zählpunkt erfolgreich aktualisiert.'); + }, + error: err => { + this.toastService.error(err.error?.message || 'Zählpunkt konnte nicht aktualisiert werden.'); + } + }); + } + canDelete(point: MeteringPoint): boolean { return this.isAdmin || point.makoState === 'NEW' || point.makoState === 'REJECTED' || point.makoState === 'ERROR'; } diff --git a/eeg_frontend/src/app/services/metering-point.ts b/eeg_frontend/src/app/services/metering-point.ts index 7671229..7baa874 100644 --- a/eeg_frontend/src/app/services/metering-point.ts +++ b/eeg_frontend/src/app/services/metering-point.ts @@ -20,6 +20,10 @@ export interface CreateMeteringPointRequest { type: PointType; } +export interface UpdateMeteringPointRequest { + type: PointType; +} + @Injectable({providedIn: 'root'}) export class MeteringPointService { private http = inject(HttpClient); @@ -40,4 +44,8 @@ export class MeteringPointService { deleteMeteringPoint(id: string): Observable { return this.http.delete(`${this.apiUrl}/${id}`); } + + updateMeteringPoint(id: string, payload: UpdateMeteringPointRequest): Observable { + return this.http.put(`${this.apiUrl}/${id}`, payload); + } }