feat(community): add metering data upload UI with preview and validation
- Add frontend upload page with file selection, drag-and-drop, and preview - Add preview endpoint (POST /api/community/metering-data/preview/xlsx) - Validate FEED_IN only for PRODUCER, CONSUMPTION only for CONSUMER metering points - Optimize deduplication with bulk query (fix N+1 problem) - Eliminate duplicate DB query in validateRecords - Add 4 new tests for data type validation - Add navigation entry for admin users
This commit is contained in:
parent
4b7b26231f
commit
a651ec946c
@ -61,6 +61,14 @@ public class MeteringDataController {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/preview/xlsx", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public ResponseEntity<List<MeteringDataRecordDto>> previewXlsx(
|
||||
@RequestParam("file") MultipartFile file) throws IOException {
|
||||
List<MeteringDataRecordDto> records = xlsxParser.parse(file.getInputStream());
|
||||
return ResponseEntity.ok(records.stream().limit(10).toList());
|
||||
}
|
||||
|
||||
@GetMapping("/{meteringPointId}")
|
||||
@PreAuthorize("hasRole('MEMBER')")
|
||||
public ResponseEntity<List<MeteringDataResponse>> getMeteringData(
|
||||
|
||||
@ -23,6 +23,13 @@ public interface MeteringDataRepository extends JpaRepository<MeteringData, UUID
|
||||
List<MeteringData> findByMeteringPointIdAndDataTypeAndIntervalStart(
|
||||
UUID meteringPointId, MeteringDataType dataType, LocalDateTime intervalStart);
|
||||
|
||||
@Query("SELECT m FROM MeteringData m WHERE m.meteringPoint.id = :meteringPointId " +
|
||||
"AND m.dataType = :dataType AND m.intervalStart IN :intervalStarts")
|
||||
List<MeteringData> findByMeteringPointIdAndDataTypeAndIntervalStartIn(
|
||||
@Param("meteringPointId") UUID meteringPointId,
|
||||
@Param("dataType") MeteringDataType dataType,
|
||||
@Param("intervalStarts") List<LocalDateTime> intervalStarts);
|
||||
|
||||
@Query("SELECT FUNCTION('DAYOFYEAR', m.intervalStart), FUNCTION('YEAR', m.intervalStart), SUM(m.kwh) " +
|
||||
"FROM MeteringData m WHERE m.meteringPoint.id = :meteringPointId AND m.dataType = 'CONSUMPTION' " +
|
||||
"AND m.intervalStart BETWEEN :from AND :to " +
|
||||
|
||||
@ -29,7 +29,8 @@ public class MeteringDataService {
|
||||
|
||||
@Transactional
|
||||
public MeteringDataUploadResponse uploadMeteringData(MeteringDataUploadRequest request, UUID uploadedBy) {
|
||||
List<String> validationErrors = validateRecords(request.records());
|
||||
Map<String, MeteringPoint> pointsByAtNumber = resolveMeteringPoints(request.records());
|
||||
List<String> validationErrors = validateRecords(request.records(), pointsByAtNumber);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return MeteringDataUploadResponse.validationFailed(validationErrors);
|
||||
@ -43,8 +44,6 @@ public class MeteringDataService {
|
||||
upload.setUploadedBy(uploadedBy);
|
||||
MeteringDataUpload savedUpload = uploadRepository.save(upload);
|
||||
|
||||
Map<String, MeteringPoint> pointsByAtNumber = resolveMeteringPoints(request.records());
|
||||
|
||||
List<MeteringData> entities = request.records().stream()
|
||||
.map(record -> toEntity(record, pointsByAtNumber.get(record.atNumber()),
|
||||
savedUpload.getId(), request.source()))
|
||||
@ -92,7 +91,7 @@ public class MeteringDataService {
|
||||
"Keine Berechtigung zum Zugriff auf diesen Zählpunkt"));
|
||||
}
|
||||
|
||||
private List<String> validateRecords(List<MeteringDataRecordDto> records) {
|
||||
private List<String> validateRecords(List<MeteringDataRecordDto> records, Map<String, MeteringPoint> pointsByAtNumber) {
|
||||
List<String> errors = new ArrayList<>();
|
||||
|
||||
if (records == null || records.isEmpty()) {
|
||||
@ -100,22 +99,17 @@ public class MeteringDataService {
|
||||
return errors;
|
||||
}
|
||||
|
||||
Set<String> atNumbers = records.stream()
|
||||
.map(MeteringDataRecordDto::atNumber)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Set<String> existingAtNumbers = meteringPointRepository.findByAtNumberIn(atNumbers).stream()
|
||||
.map(MeteringPoint::getAtNumber)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
for (int i = 0; i < records.size(); i++) {
|
||||
MeteringDataRecordDto record = records.get(i);
|
||||
String prefix = "Zeile " + (i + 1) + ": ";
|
||||
|
||||
if (!existingAtNumbers.contains(record.atNumber())) {
|
||||
if (!pointsByAtNumber.containsKey(record.atNumber())) {
|
||||
errors.add(prefix + "Zählpunkt " + record.atNumber() + " nicht im System vorhanden");
|
||||
continue;
|
||||
}
|
||||
|
||||
MeteringPoint point = pointsByAtNumber.get(record.atNumber());
|
||||
|
||||
if (record.intervalEnd().isBefore(record.intervalStart())) {
|
||||
errors.add(prefix + "Intervall-Ende liegt vor Intervall-Beginn");
|
||||
}
|
||||
@ -123,6 +117,13 @@ public class MeteringDataService {
|
||||
if (record.kwh() < 0) {
|
||||
errors.add(prefix + "kWh-Wert darf nicht negativ sein");
|
||||
}
|
||||
|
||||
if (record.dataType() == MeteringDataType.FEED_IN && point.getType() != PointType.PRODUCER) {
|
||||
errors.add(prefix + "Einspeisedaten (FEED_IN) nur für PRODUCER-Zählpunkte erlaubt");
|
||||
}
|
||||
if (record.dataType() == MeteringDataType.CONSUMPTION && point.getType() != PointType.CONSUMER) {
|
||||
errors.add(prefix + "Verbrauchsdaten (CONSUMPTION) nur für CONSUMER-Zählpunkte erlaubt");
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
@ -156,15 +157,25 @@ public class MeteringDataService {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<UUID, Map<MeteringDataType, List<LocalDateTime>>> grouped = entities.stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
e -> e.getMeteringPoint().getId(),
|
||||
Collectors.groupingBy(
|
||||
MeteringData::getDataType,
|
||||
Collectors.mapping(MeteringData::getIntervalStart, Collectors.toList())
|
||||
)
|
||||
));
|
||||
|
||||
Set<MeteringData> toDelete = new LinkedHashSet<>();
|
||||
|
||||
for (MeteringData entity : entities) {
|
||||
meteringDataRepository
|
||||
.findByMeteringPointIdAndDataTypeAndIntervalStart(
|
||||
entity.getMeteringPoint().getId(),
|
||||
entity.getDataType(),
|
||||
entity.getIntervalStart())
|
||||
.forEach(toDelete::add);
|
||||
for (var pointEntry : grouped.entrySet()) {
|
||||
UUID pointId = pointEntry.getKey();
|
||||
for (var typeEntry : pointEntry.getValue().entrySet()) {
|
||||
MeteringDataType dataType = typeEntry.getKey();
|
||||
List<LocalDateTime> starts = typeEntry.getValue();
|
||||
toDelete.addAll(meteringDataRepository.findByMeteringPointIdAndDataTypeAndIntervalStartIn(
|
||||
pointId, dataType, starts));
|
||||
}
|
||||
}
|
||||
|
||||
if (!toDelete.isEmpty()) {
|
||||
|
||||
@ -190,7 +190,15 @@ class MeteringDataServiceTest {
|
||||
|
||||
@Test
|
||||
void uploadMeteringData_multipleRecords() {
|
||||
when(meteringPointRepository.findByAtNumberIn(any())).thenReturn(List.of(testMeteringPoint));
|
||||
String producerAtNumber = "AT0010000000000000000000001234568";
|
||||
MeteringPoint producerPoint = new MeteringPoint();
|
||||
producerPoint.setId(UUID.randomUUID());
|
||||
producerPoint.setUserId(testUserId);
|
||||
producerPoint.setAtNumber(producerAtNumber);
|
||||
producerPoint.setType(PointType.PRODUCER);
|
||||
producerPoint.setMakoState(MakoState.ACTIVE);
|
||||
|
||||
when(meteringPointRepository.findByAtNumberIn(any())).thenReturn(List.of(testMeteringPoint, producerPoint));
|
||||
when(uploadRepository.save(any())).thenAnswer(invocation -> {
|
||||
MeteringDataUpload upload = invocation.getArgument(0);
|
||||
upload.setId(UUID.randomUUID());
|
||||
@ -203,7 +211,7 @@ class MeteringDataServiceTest {
|
||||
LocalDateTime.of(2026, 1, 1, 0, 0), LocalDateTime.of(2026, 1, 1, 0, 15), 1.25, null),
|
||||
new MeteringDataRecordDto(testAtNumber, MeteringDataType.CONSUMPTION,
|
||||
LocalDateTime.of(2026, 1, 1, 0, 15), LocalDateTime.of(2026, 1, 1, 0, 30), 0.80, null),
|
||||
new MeteringDataRecordDto(testAtNumber, MeteringDataType.FEED_IN,
|
||||
new MeteringDataRecordDto(producerAtNumber, MeteringDataType.FEED_IN,
|
||||
LocalDateTime.of(2026, 1, 1, 0, 0), LocalDateTime.of(2026, 1, 1, 0, 15), 2.50, null)
|
||||
);
|
||||
|
||||
@ -291,4 +299,95 @@ class MeteringDataServiceTest {
|
||||
MeteringDataType.CONSUMPTION,
|
||||
LocalDateTime.of(2026, 1, 1, 0, 0), LocalDateTime.of(2026, 1, 31, 23, 59)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadMeteringData_validationFailsForFeedInOnConsumerPoint() {
|
||||
when(meteringPointRepository.findByAtNumberIn(any())).thenReturn(List.of(testMeteringPoint));
|
||||
|
||||
MeteringDataRecordDto record = new MeteringDataRecordDto(
|
||||
testAtNumber,
|
||||
MeteringDataType.FEED_IN,
|
||||
LocalDateTime.of(2026, 1, 1, 0, 0),
|
||||
LocalDateTime.of(2026, 1, 1, 0, 15),
|
||||
2.50,
|
||||
null
|
||||
);
|
||||
|
||||
MeteringDataUploadRequest request = new MeteringDataUploadRequest(
|
||||
DataSource.EMAIL_XLSX,
|
||||
"test.xlsx",
|
||||
List.of(record)
|
||||
);
|
||||
|
||||
MeteringDataUploadResponse response = meteringDataService.uploadMeteringData(request, UUID.randomUUID());
|
||||
|
||||
assertEquals(UploadStatus.FAILED, response.status());
|
||||
assertFalse(response.validationErrors().isEmpty());
|
||||
assertTrue(response.validationErrors().get(0).contains("PRODUCER"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadMeteringData_validationFailsForConsumptionOnProducerPoint() {
|
||||
String producerAtNumber = "AT0010000000000000000000001234568";
|
||||
MeteringPoint producerPoint = new MeteringPoint();
|
||||
producerPoint.setId(UUID.randomUUID());
|
||||
producerPoint.setUserId(testUserId);
|
||||
producerPoint.setAtNumber(producerAtNumber);
|
||||
producerPoint.setType(PointType.PRODUCER);
|
||||
producerPoint.setMakoState(MakoState.ACTIVE);
|
||||
|
||||
when(meteringPointRepository.findByAtNumberIn(any())).thenReturn(List.of(producerPoint));
|
||||
|
||||
MeteringDataRecordDto record = new MeteringDataRecordDto(
|
||||
producerAtNumber,
|
||||
MeteringDataType.CONSUMPTION,
|
||||
LocalDateTime.of(2026, 1, 1, 0, 0),
|
||||
LocalDateTime.of(2026, 1, 1, 0, 15),
|
||||
1.25,
|
||||
null
|
||||
);
|
||||
|
||||
MeteringDataUploadRequest request = new MeteringDataUploadRequest(
|
||||
DataSource.EMAIL_XLSX,
|
||||
"test.xlsx",
|
||||
List.of(record)
|
||||
);
|
||||
|
||||
MeteringDataUploadResponse response = meteringDataService.uploadMeteringData(request, UUID.randomUUID());
|
||||
|
||||
assertEquals(UploadStatus.FAILED, response.status());
|
||||
assertFalse(response.validationErrors().isEmpty());
|
||||
assertTrue(response.validationErrors().get(0).contains("CONSUMER"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadMeteringData_allowsSelfConsumptionOnBothTypes() {
|
||||
when(meteringPointRepository.findByAtNumberIn(any())).thenReturn(List.of(testMeteringPoint));
|
||||
when(uploadRepository.save(any())).thenAnswer(invocation -> {
|
||||
MeteringDataUpload upload = invocation.getArgument(0);
|
||||
upload.setId(UUID.randomUUID());
|
||||
return upload;
|
||||
});
|
||||
when(meteringDataRepository.saveAll(any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
MeteringDataRecordDto record = new MeteringDataRecordDto(
|
||||
testAtNumber,
|
||||
MeteringDataType.SELF_CONSUMPTION,
|
||||
LocalDateTime.of(2026, 1, 1, 0, 0),
|
||||
LocalDateTime.of(2026, 1, 1, 0, 15),
|
||||
0.50,
|
||||
null
|
||||
);
|
||||
|
||||
MeteringDataUploadRequest request = new MeteringDataUploadRequest(
|
||||
DataSource.EMAIL_XLSX,
|
||||
"test.xlsx",
|
||||
List.of(record)
|
||||
);
|
||||
|
||||
MeteringDataUploadResponse response = meteringDataService.uploadMeteringData(request, UUID.randomUUID());
|
||||
|
||||
assertEquals(UploadStatus.COMPLETED, response.status());
|
||||
assertEquals(1, response.recordCount());
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ import {ResetPasswordComponent} from './pages/reset-password/reset-password';
|
||||
import {AdminTariffComponent} from './pages/admin-tariff/admin-tariff';
|
||||
import {UserTariffComponent} from './pages/user-tariff/user-tariff';
|
||||
import {ConsumptionDashboardComponent} from './pages/consumption-dashboard/consumption-dashboard';
|
||||
import {UploadMeteringDataComponent} from './pages/upload-metering-data/upload-metering-data';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: '', component: LandingPage },
|
||||
@ -68,6 +69,11 @@ export const routes: Routes = [
|
||||
component: AdminTariffComponent,
|
||||
canActivate: [roleGuard(['ADMIN'])]
|
||||
},
|
||||
{
|
||||
path: 'upload-metering-data',
|
||||
component: UploadMeteringDataComponent,
|
||||
canActivate: [roleGuard(['ADMIN'])]
|
||||
},
|
||||
{
|
||||
path: 'my-tariffs',
|
||||
component: UserTariffComponent,
|
||||
|
||||
@ -15,5 +15,6 @@ export const NAV_ITEMS: NavItem[] = [
|
||||
{label: 'Benutzerverwaltung', path: '/dashboard/approvals', roles: ['ADMIN']},
|
||||
{label: 'Energiegemeinschaften', path: '/dashboard/energy-communities', roles: ['ADMIN']},
|
||||
{label: 'Tarife verwalten', path: '/dashboard/tariffs', roles: ['ADMIN']},
|
||||
{label: 'Messdaten-Upload', path: '/dashboard/upload-metering-data', roles: ['ADMIN']},
|
||||
{label: 'Meine Tarife', path: '/dashboard/my-tariffs', roles: ['MEMBER']}
|
||||
];
|
||||
|
||||
@ -0,0 +1,113 @@
|
||||
<div class="max-w-2xl mx-auto p-6">
|
||||
<h1 class="text-2xl font-bold mb-6">Messdaten-Upload</h1>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<p class="text-gray-600 mb-4">
|
||||
Laden Sie eine Excel-Datei (XLSX) mit Messdaten hoch.
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-blue-400 transition-colors"
|
||||
(dragover)="onDragOver($event)"
|
||||
(drop)="onDrop($event)"
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
accept=".xlsx,.xls"
|
||||
(change)="onFileSelected($event)"
|
||||
class="hidden"
|
||||
#fileInput
|
||||
/>
|
||||
<button
|
||||
(click)="fileInput.click()"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
>
|
||||
Datei auswählen
|
||||
</button>
|
||||
<p class="mt-2 text-sm text-gray-500">
|
||||
oder Datei hierher ziehen
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if (selectedFile()) {
|
||||
<div class="mt-4 p-3 bg-gray-50 rounded flex items-center justify-between">
|
||||
<span class="text-sm text-gray-700">{{ selectedFile()!.name }}</span>
|
||||
<button
|
||||
(click)="upload()"
|
||||
[disabled]="isUploading()"
|
||||
class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50"
|
||||
>
|
||||
@if (isUploading()) {
|
||||
Wird hochgeladen...
|
||||
} @else {
|
||||
Upload starten
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isLoadingPreview()) {
|
||||
<div class="mt-4 text-center text-gray-500">
|
||||
Vorschau wird geladen...
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (previewData().length > 0) {
|
||||
<div class="mt-4">
|
||||
<h3 class="text-sm font-medium text-gray-700 mb-2">Vorschau (erste 10 Zeilen):</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
<th class="px-3 py-2 text-left">AT-Nummer</th>
|
||||
<th class="px-3 py-2 text-left">Typ</th>
|
||||
<th class="px-3 py-2 text-left">Von</th>
|
||||
<th class="px-3 py-2 text-left">Bis</th>
|
||||
<th class="px-3 py-2 text-right">kWh</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of previewData(); track $index) {
|
||||
<tr class="border-t">
|
||||
<td class="px-3 py-2">{{ row.atNumber }}</td>
|
||||
<td class="px-3 py-2">{{ row.dataType }}</td>
|
||||
<td class="px-3 py-2">{{ row.intervalStart }}</td>
|
||||
<td class="px-3 py-2">{{ row.intervalEnd }}</td>
|
||||
<td class="px-3 py-2 text-right">{{ row.kwh }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (uploadResult()) {
|
||||
<div class="mt-4 p-4 rounded" [class]="uploadResult()!.status === 'COMPLETED' ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'">
|
||||
<p class="font-semibold" [class]="uploadResult()!.status === 'COMPLETED' ? 'text-green-800' : 'text-red-800'">
|
||||
@if (uploadResult()!.status === 'COMPLETED') {
|
||||
Upload erfolgreich!
|
||||
} @else {
|
||||
Upload fehlgeschlagen
|
||||
}
|
||||
</p>
|
||||
<p class="text-sm text-gray-600 mt-1">
|
||||
{{ uploadResult()!.recordCount }} Datensätze verarbeitet
|
||||
</p>
|
||||
@if (uploadResult()!.validationErrors.length > 0) {
|
||||
<ul class="mt-2 text-sm text-red-600 list-disc list-inside">
|
||||
@for (error of uploadResult()!.validationErrors; track error) {
|
||||
<li>{{ error }}</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (errorMessage()) {
|
||||
<div class="mt-4 p-4 bg-red-50 border border-red-200 rounded">
|
||||
<p class="text-red-800">{{ errorMessage() }}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,113 @@
|
||||
import {Component, inject, signal} from '@angular/core';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {environment} from '../../../environments/environment';
|
||||
|
||||
interface PreviewRecord {
|
||||
atNumber: string;
|
||||
dataType: string;
|
||||
intervalStart: string;
|
||||
intervalEnd: string;
|
||||
kwh: number;
|
||||
readingValue: number | null;
|
||||
}
|
||||
|
||||
interface UploadResponse {
|
||||
uploadId: string;
|
||||
status: string;
|
||||
recordCount: number;
|
||||
validationErrors: string[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-upload-metering-data',
|
||||
standalone: true,
|
||||
templateUrl: './upload-metering-data.html',
|
||||
})
|
||||
export class UploadMeteringDataComponent {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
selectedFile = signal<File | null>(null);
|
||||
previewData = signal<PreviewRecord[]>([]);
|
||||
isLoadingPreview = signal(false);
|
||||
isUploading = signal(false);
|
||||
uploadResult = signal<UploadResponse | null>(null);
|
||||
errorMessage = signal<string | null>(null);
|
||||
|
||||
onFileSelected(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) {
|
||||
this.selectedFile.set(input.files[0]);
|
||||
this.uploadResult.set(null);
|
||||
this.errorMessage.set(null);
|
||||
this.loadPreview(input.files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
onDragOver(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
onDrop(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const files = event.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
this.selectedFile.set(files[0]);
|
||||
this.uploadResult.set(null);
|
||||
this.errorMessage.set(null);
|
||||
this.loadPreview(files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
private loadPreview(file: File) {
|
||||
this.isLoadingPreview.set(true);
|
||||
this.previewData.set([]);
|
||||
this.errorMessage.set(null);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
this.http.post<PreviewRecord[]>(
|
||||
`${environment.apiUrl}/api/community/metering-data/preview/xlsx`,
|
||||
formData
|
||||
).subscribe({
|
||||
next: (data) => {
|
||||
this.previewData.set(data);
|
||||
this.isLoadingPreview.set(false);
|
||||
},
|
||||
error: (error) => {
|
||||
this.errorMessage.set(error.error?.message || 'Fehler beim Laden der Vorschau');
|
||||
this.isLoadingPreview.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
upload() {
|
||||
const file = this.selectedFile();
|
||||
if (!file) return;
|
||||
|
||||
this.isUploading.set(true);
|
||||
this.uploadResult.set(null);
|
||||
this.errorMessage.set(null);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
this.http.post<UploadResponse>(
|
||||
`${environment.apiUrl}/api/community/metering-data/upload/xlsx`,
|
||||
formData
|
||||
).subscribe({
|
||||
next: (response) => {
|
||||
this.uploadResult.set(response);
|
||||
this.isUploading.set(false);
|
||||
this.selectedFile.set(null);
|
||||
this.previewData.set([]);
|
||||
},
|
||||
error: (error) => {
|
||||
this.errorMessage.set(error.error?.message || 'Fehler beim Upload');
|
||||
this.isUploading.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user