From c5555b30031e1cad919a6ddf0ad7aee57ff04148 Mon Sep 17 00:00:00 2001 From: ThompsonNye Date: Thu, 19 Jun 2025 14:40:14 +0200 Subject: [PATCH] Finish implementing editing and displaying entries --- src/Vegasco-Web/package.json | 3 + src/Vegasco-Web/pnpm-lock.yaml | 37 +++++ .../src/app/api/models/consumption.ts | 9 ++ .../edit-entry/edit-entry.component.ts | 20 +-- .../entry-card/entry-card.component.html | 32 +++++ .../entry-card/entry-card.component.scss | 3 + .../entry-card/entry-card.component.ts | 133 ++++++++++++++++++ .../entries/entries/entries.component.html | 5 +- .../entries/entries/entries.component.ts | 29 ++-- .../services/entries-overview.service.ts | 49 +++++++ .../src/app/services/routing.service.ts | 21 +++ 11 files changed, 312 insertions(+), 29 deletions(-) create mode 100644 src/Vegasco-Web/src/app/api/models/consumption.ts create mode 100644 src/Vegasco-Web/src/app/modules/entries/entries/components/entry-card/entry-card.component.html create mode 100644 src/Vegasco-Web/src/app/modules/entries/entries/components/entry-card/entry-card.component.scss create mode 100644 src/Vegasco-Web/src/app/modules/entries/entries/components/entry-card/entry-card.component.ts create mode 100644 src/Vegasco-Web/src/app/modules/entries/entries/services/entries-overview.service.ts create mode 100644 src/Vegasco-Web/src/app/services/routing.service.ts diff --git a/src/Vegasco-Web/package.json b/src/Vegasco-Web/package.json index f7ad866..14d4723 100644 --- a/src/Vegasco-Web/package.json +++ b/src/Vegasco-Web/package.json @@ -18,6 +18,9 @@ "@angular/forms": "^19.2.14", "@angular/platform-browser": "^19.2.14", "@angular/router": "^19.2.14", + "@ng-icons/core": "^31.4.0", + "@ng-icons/material-file-icons": "^31.4.0", + "@ng-icons/material-icons": "^31.4.0", "@primeng/themes": "^19.1.3", "@tailwindcss/postcss": "^4.1.10", "keycloak-angular": "^19.0.2", diff --git a/src/Vegasco-Web/pnpm-lock.yaml b/src/Vegasco-Web/pnpm-lock.yaml index e107ca7..4d19323 100644 --- a/src/Vegasco-Web/pnpm-lock.yaml +++ b/src/Vegasco-Web/pnpm-lock.yaml @@ -26,6 +26,15 @@ importers: '@angular/router': specifier: ^19.2.14 version: 19.2.14(@angular/common@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@19.2.14(@angular/animations@19.2.14(@angular/common@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) + '@ng-icons/core': + specifier: ^31.4.0 + version: 31.4.0(@angular/common@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) + '@ng-icons/material-file-icons': + specifier: ^31.4.0 + version: 31.4.0 + '@ng-icons/material-icons': + specifier: ^31.4.0 + version: 31.4.0 '@primeng/themes': specifier: ^19.1.3 version: 19.1.3 @@ -1305,6 +1314,19 @@ packages: resolution: {integrity: sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ==} engines: {node: '>= 10'} + '@ng-icons/core@31.4.0': + resolution: {integrity: sha512-JfLiJGDX/ihWmawcnLGXtwyCqMi2qXz7gMJyXXWdUN5JA18EAnt3JnyuxDAGkoU/u7wRlcOI7irlXHU4spAKOg==} + peerDependencies: + '@angular/common': '>=18.0.0' + '@angular/core': '>=18.0.0' + rxjs: ^6.5.3 || ^7.4.0 + + '@ng-icons/material-file-icons@31.4.0': + resolution: {integrity: sha512-Ffh61ghuuDRxelfTe/rHQ5IFCqUget/JeZ/NLq6QWLBycxUC6PjiEIIAXQvnVmYwCHNgxjBIRExP1/+vdHriNQ==} + + '@ng-icons/material-icons@31.4.0': + resolution: {integrity: sha512-JCxwM0LXwOgT5LD99p5TwPM6dPQ5x1BGieNzAstz7vk5+aiASg3fqs3rjNx7CbN3c2QjJ8+KuKrCCBzT9DCkOQ==} + '@ngtools/webpack@19.2.15': resolution: {integrity: sha512-H37nop/wWMkSgoU2VvrMzanHePdLRRrX52nC5tT2ZhH3qP25+PrnMyw11PoLDLv3iWXC68uB1AiKNIT+jiQbuQ==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} @@ -5797,6 +5819,21 @@ snapshots: '@napi-rs/nice-win32-x64-msvc': 1.0.1 optional: true + '@ng-icons/core@31.4.0(@angular/common@19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)': + dependencies: + '@angular/common': 19.2.14(@angular/core@19.2.14(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) + '@angular/core': 19.2.14(rxjs@7.8.2)(zone.js@0.15.1) + rxjs: 7.8.2 + tslib: 2.8.1 + + '@ng-icons/material-file-icons@31.4.0': + dependencies: + tslib: 2.8.1 + + '@ng-icons/material-icons@31.4.0': + dependencies: + tslib: 2.8.1 + '@ngtools/webpack@19.2.15(@angular/compiler-cli@19.2.14(@angular/compiler@19.2.14)(typescript@5.8.3))(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.4))': dependencies: '@angular/compiler-cli': 19.2.14(@angular/compiler@19.2.14)(typescript@5.8.3) diff --git a/src/Vegasco-Web/src/app/api/models/consumption.ts b/src/Vegasco-Web/src/app/api/models/consumption.ts new file mode 100644 index 0000000..0462c83 --- /dev/null +++ b/src/Vegasco-Web/src/app/api/models/consumption.ts @@ -0,0 +1,9 @@ +export interface Consumption { + id: string; + dateTime: string; + distance: number; + amount: number; + ignoreInCalculation: boolean; + carId: string; + car: Car; +} \ No newline at end of file diff --git a/src/Vegasco-Web/src/app/modules/entries/edit-entry/edit-entry.component.ts b/src/Vegasco-Web/src/app/modules/entries/edit-entry/edit-entry.component.ts index a0e0b68..d957172 100644 --- a/src/Vegasco-Web/src/app/modules/entries/edit-entry/edit-entry.component.ts +++ b/src/Vegasco-Web/src/app/modules/entries/edit-entry/edit-entry.component.ts @@ -1,8 +1,11 @@ +import { HttpErrorResponse } from '@angular/common/http'; import { Component, computed, DestroyRef, inject, input, Signal } from '@angular/core'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; -import { Router } from '@angular/router'; import { CarClient } from '@vegasco-web/api/cars/car-client'; +import { ConsumptionClient } from '@vegasco-web/api/consumptions/consumption-client'; +import { RoutingService } from '@vegasco-web/services/routing.service'; +import { MessageService } from 'primeng/api'; import { ButtonModule } from 'primeng/button'; import { ChipModule } from 'primeng/chip'; import { DatePickerModule } from 'primeng/datepicker'; @@ -14,11 +17,8 @@ import { InputTextModule } from 'primeng/inputtext'; import { MultiSelectModule } from 'primeng/multiselect'; import { SelectModule } from 'primeng/select'; import { SkeletonModule } from 'primeng/skeleton'; -import { catchError, EMPTY, map, Observable, switchMap, tap, throwError } from 'rxjs'; +import { catchError, EMPTY, map, Observable, switchMap, throwError } from 'rxjs'; import { RequiredMarkerComponent } from './components/required-marker.component'; -import { ConsumptionClient } from '@vegasco-web/api/consumptions/consumption-client'; -import { HttpErrorResponse } from '@angular/common/http'; -import { MessageService } from 'primeng/api'; @Component({ selector: 'app-edit-entry', @@ -44,7 +44,7 @@ export class EditEntryComponent { private readonly formBuilder = inject(FormBuilder); private readonly carClient = inject(CarClient); private readonly consumptionClient = inject(ConsumptionClient); - private readonly router = inject(Router); + private readonly routingService = inject(RoutingService); private readonly destroyRef = inject(DestroyRef); private readonly messageService = inject(MessageService); @@ -88,7 +88,7 @@ export class EditEntryComponent { } async navigateToOverviewPage(): Promise { - await this.router.navigateByUrl(`/entries`); + await this.routingService.navigateToEntries(); } onSubmit(): void { @@ -119,7 +119,7 @@ export class EditEntryComponent { .pipe( takeUntilDestroyed(this.destroyRef), catchError((error) => this.handleError(error)), - switchMap(() => this.router.navigateByUrl('/entries')) + switchMap(() => this.routingService.navigateToEntries()) ) .subscribe(); } @@ -130,7 +130,7 @@ export class EditEntryComponent { .pipe( takeUntilDestroyed(this.destroyRef), catchError((error) => this.handleError(error)), - switchMap(() => this.router.navigateByUrl('/entries')) + switchMap(() => this.routingService.navigateToEntries()) ) .subscribe(); } @@ -149,7 +149,7 @@ export class EditEntryComponent { 'Beim Erstellen des Eintrags ist ein Fehler aufgetreten. Bitte versuche es erneut.', }); break; - case error.status == 400: + case error.status === 400: this.messageService.add({ severity: 'error', summary: 'Clientfehler', diff --git a/src/Vegasco-Web/src/app/modules/entries/entries/components/entry-card/entry-card.component.html b/src/Vegasco-Web/src/app/modules/entries/entries/components/entry-card/entry-card.component.html new file mode 100644 index 0000000..95f36be --- /dev/null +++ b/src/Vegasco-Web/src/app/modules/entries/entries/components/entry-card/entry-card.component.html @@ -0,0 +1,32 @@ + + +
+
+
+ +
+
{{ entry().dateTime | date:"dd.MM.yyyy" }}
+
+ +
+
{{ entry().car.name }}
+
+ +
+ {{entry().distance }} km +
+ +
+ {{entry().amount }} ℓ +
+
+
+
+ + +
+
\ No newline at end of file diff --git a/src/Vegasco-Web/src/app/modules/entries/entries/components/entry-card/entry-card.component.scss b/src/Vegasco-Web/src/app/modules/entries/entries/components/entry-card/entry-card.component.scss new file mode 100644 index 0000000..2006a40 --- /dev/null +++ b/src/Vegasco-Web/src/app/modules/entries/entries/components/entry-card/entry-card.component.scss @@ -0,0 +1,3 @@ +.edit-button { + cursor: pointer; +} \ No newline at end of file diff --git a/src/Vegasco-Web/src/app/modules/entries/entries/components/entry-card/entry-card.component.ts b/src/Vegasco-Web/src/app/modules/entries/entries/components/entry-card/entry-card.component.ts new file mode 100644 index 0000000..2e7988f --- /dev/null +++ b/src/Vegasco-Web/src/app/modules/entries/entries/components/entry-card/entry-card.component.ts @@ -0,0 +1,133 @@ +import { DatePipe } from '@angular/common'; +import { Component, DestroyRef, inject, input, output } from '@angular/core'; +import { ConsumptionClient } from '@vegasco-web/api/consumptions/consumption-client'; +import { Consumption } from '@vegasco-web/api/models/consumption'; +import { RoutingService } from '@vegasco-web/services/routing.service'; +import { ConfirmationService, MessageService } from 'primeng/api'; +import { ButtonModule } from 'primeng/button'; +import { CardModule } from 'primeng/card'; +import { ConfirmDialogModule } from 'primeng/confirmdialog'; +import { TooltipModule } from 'primeng/tooltip'; +import { + matCalendarMonthSharp, + matCommentSharp, + matDeleteSharp, + matMedicationSharp, + matPetsSharp, + matScaleSharp, +} from '@ng-icons/material-icons/sharp'; +import { NgIconComponent, provideIcons } from '@ng-icons/core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { catchError, EMPTY, Observable, takeUntil, tap, throwError } from 'rxjs'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + + +@Component({ + selector: 'app-entry-card', + imports: [ + DatePipe, + TooltipModule, + ButtonModule, + CardModule, + ConfirmDialogModule, + NgIconComponent, + ], + providers: [ + provideIcons({ + matDeleteSharp, + }), + ConfirmationService, + ], + templateUrl: './entry-card.component.html', + styleUrl: './entry-card.component.scss' +}) +export class EntryCardComponent { + readonly entry = input.required(); + + readonly entryDeleted = output(); + + private readonly routingService = inject(RoutingService); + private readonly consumptionClient = inject(ConsumptionClient); + private readonly messageService = inject(MessageService); + private readonly confirmationService = inject(ConfirmationService); + + private readonly destroyRef = inject(DestroyRef); + + async navigateToEdit(): Promise { + await this.routingService.navigateToEditEntry(this.entry().id); + } + + confirmDeleteEntry(): void { + const weighedAt = new Date( + Date.parse(this.entry().dateTime), + ).toLocaleString('de-DE', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }); + + this.confirmationService.confirm({ + closeOnEscape: true, + dismissableMask: true, + header: 'Bist du sicher?', + message: `Möchtest du diesen Eintrag (${weighedAt} für ${this.entry().car.name}) wirklich löschen?`, + acceptButtonProps: { + label: 'Löschen', + severity: 'danger', + }, + rejectButtonProps: { + label: 'Abbrechen', + outlined: true, + }, + accept: () => this.deleteEntry(), + }); + } + + deleteEntry(): void { + this.consumptionClient.delete(this.entry().id) + .pipe( + takeUntilDestroyed(this.destroyRef), + tap(() => this.entryDeleted.emit(this.entry())), + catchError((error) => this.handleError(error)), + ) + .subscribe(); + } + + private handleError(error: unknown): Observable { + if (!(error instanceof HttpErrorResponse)) { + return throwError(() => error); + } + + switch (true) { + case error.status >= 500 && error.status <= 599: + this.messageService.add({ + severity: 'error', + summary: 'Serverfehler', + detail: + 'Beim Erstellen des Eintrags ist ein Fehler aufgetreten. Bitte versuche es erneut.', + }); + break; + case error.status === 400: + this.messageService.add({ + severity: 'error', + summary: 'Clientfehler', + detail: + 'Die Anwendung scheint falsche Daten an den Server zu senden.', + }); + break; + default: + console.error(error); + this.messageService.add({ + severity: 'error', + summary: 'Unerwarteter Fehler', + detail: + 'Beim Erstellen des Eintrags hat der Server eine unerwartete Antwort zurückgegeben.', + }); + break; + } + + return EMPTY; + } +} diff --git a/src/Vegasco-Web/src/app/modules/entries/entries/entries.component.html b/src/Vegasco-Web/src/app/modules/entries/entries/entries.component.html index 5dd85e0..ec43a1c 100644 --- a/src/Vegasco-Web/src/app/modules/entries/entries/entries.component.html +++ b/src/Vegasco-Web/src/app/modules/entries/entries/entries.component.html @@ -26,9 +26,8 @@
@for (entry of entries; track entry.id) { - {{ entry | json }} - + }
diff --git a/src/Vegasco-Web/src/app/modules/entries/entries/entries.component.ts b/src/Vegasco-Web/src/app/modules/entries/entries/entries.component.ts index bfbf62d..27b6d74 100644 --- a/src/Vegasco-Web/src/app/modules/entries/entries/entries.component.ts +++ b/src/Vegasco-Web/src/app/modules/entries/entries/entries.component.ts @@ -3,8 +3,6 @@ import { Component, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { RouterLink } from '@angular/router'; -import { CarClient } from '@vegasco-web/api/cars/car-client'; -import { ConsumptionClient } from '@vegasco-web/api/consumptions/consumption-client'; import { MessageService } from 'primeng/api'; import { ButtonModule } from 'primeng/button'; import { DataViewModule } from 'primeng/dataview'; @@ -16,9 +14,10 @@ import { combineLatest, map, Observable, - startWith, - tap + startWith } from 'rxjs'; +import { EntriesOverviewService } from './services/entries-overview.service'; +import { EntryCardComponent } from './components/entry-card/entry-card.component'; @Component({ selector: 'app-entries', @@ -27,18 +26,21 @@ import { ButtonModule, CommonModule, DataViewModule, + EntryCardComponent, SkeletonModule, SelectModule, ReactiveFormsModule, RouterLink, ScrollTopModule, ], + providers: [ + EntriesOverviewService, + ], templateUrl: './entries.component.html', styleUrl: './entries.component.scss' }) export class EntriesComponent { - private readonly consumptionClient = inject(ConsumptionClient); - private readonly carClient = inject(CarClient); + private readonly entriesOverviewService = inject(EntriesOverviewService); private readonly messageService = inject(MessageService); protected readonly consumptionEntries$: Observable; @@ -51,13 +53,11 @@ export class EntriesComponent { private readonly deletedEntries$ = new BehaviorSubject([]); constructor() { - const consumptionEntries = this.consumptionClient.getAll() - .pipe( - map(response => response.consumptions) - ); + const entries = this.entriesOverviewService.getEntries() + .pipe(takeUntilDestroyed()); this.consumptionEntries$ = combineLatest([ - consumptionEntries, + entries, this.selectedCar.valueChanges.pipe(startWith(null)), this.deletedEntries$, ]) @@ -77,11 +77,8 @@ export class EntriesComponent { }) ); - this.cars$ = this.carClient.getAll() - .pipe( - takeUntilDestroyed(), - map(response => response.cars) - ); + this.cars$ = this.entriesOverviewService.getCars() + .pipe(takeUntilDestroyed()); } onEntryDeleted(entry: ConsumptionEntry): void { diff --git a/src/Vegasco-Web/src/app/modules/entries/entries/services/entries-overview.service.ts b/src/Vegasco-Web/src/app/modules/entries/entries/services/entries-overview.service.ts new file mode 100644 index 0000000..1483dd4 --- /dev/null +++ b/src/Vegasco-Web/src/app/modules/entries/entries/services/entries-overview.service.ts @@ -0,0 +1,49 @@ +import { inject, Injectable } from "@angular/core"; +import { CarClient } from "@vegasco-web/api/cars/car-client"; +import { ConsumptionClient } from "@vegasco-web/api/consumptions/consumption-client"; +import { Consumption } from "@vegasco-web/api/models/consumption"; +import { RoutingService } from "@vegasco-web/services/routing.service"; +import { combineLatest, map, Observable, shareReplay } from "rxjs"; + +@Injectable() +export class EntriesOverviewService { + private readonly carClient = inject(CarClient); + private readonly consumptionClient = inject(ConsumptionClient); + private readonly routingService = inject(RoutingService); + + private cachedCars$: Observable | null = null; + + private ensureCarsAreCached(): void { + if (this.cachedCars$ !== null) { + return; + } + + this.cachedCars$ = this.carClient.getAll() + .pipe( + map(response => response.cars), + shareReplay(1) + ); + } + + getEntries(): Observable { + this.ensureCarsAreCached(); + + const entries$ = this.consumptionClient.getAll() + .pipe(map(response => response.consumptions)); + + return combineLatest([this.cachedCars$!, entries$]) + .pipe( + map(([cars, entries]) => { + return entries.map((entry): Consumption => ({ + ...entry, + car: cars.find(car => car.id === entry.carId)! + })); + }) + ) + } + + getCars(): Observable { + this.ensureCarsAreCached(); + return this.cachedCars$!; + } +} \ No newline at end of file diff --git a/src/Vegasco-Web/src/app/services/routing.service.ts b/src/Vegasco-Web/src/app/services/routing.service.ts new file mode 100644 index 0000000..2c4ae34 --- /dev/null +++ b/src/Vegasco-Web/src/app/services/routing.service.ts @@ -0,0 +1,21 @@ +import { inject, Injectable } from "@angular/core"; +import { Router } from "@angular/router"; + +@Injectable({ + providedIn: 'root' +}) +export class RoutingService { + private readonly router = inject(Router); + + async navigateToEntries(): Promise { + await this.router.navigateByUrl('/entries'); + } + + async navigateToEditEntry(entryId: string): Promise { + await this.router.navigate(['entries', 'edit', entryId]); + } + + async navigateToCreateEntry(): Promise { + await this.router.navigate(['entries', 'create']); + } +} \ No newline at end of file