Finish implementing editing and displaying entries
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -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",
|
||||
|
||||
37
src/Vegasco-Web/pnpm-lock.yaml
generated
37
src/Vegasco-Web/pnpm-lock.yaml
generated
@@ -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)
|
||||
|
||||
9
src/Vegasco-Web/src/app/api/models/consumption.ts
Normal file
9
src/Vegasco-Web/src/app/api/models/consumption.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface Consumption {
|
||||
id: string;
|
||||
dateTime: string;
|
||||
distance: number;
|
||||
amount: number;
|
||||
ignoreInCalculation: boolean;
|
||||
carId: string;
|
||||
car: Car;
|
||||
}
|
||||
@@ -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<void> {
|
||||
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',
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<p-confirmDialog></p-confirmDialog>
|
||||
|
||||
<div class="flex rounded-border shadow">
|
||||
<div class="grow p-4 pos-relative edit-button" (click)="navigateToEdit()" role="button"
|
||||
aria-roledescription="Bearbeite diesen Eintrag">
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
|
||||
<div class="col-span-4 sm:col-span-2 md:col-span-1 flex my-auto items-center justify-center">
|
||||
<div>{{ entry().dateTime | date:"dd.MM.yyyy" }}</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-4 sm:col-span-2 md:col-span-1 flex my-auto items-center justify-center">
|
||||
<div>{{ entry().car.name }}</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-4 sm:col-span-2 md:col-span-1 flex my-auto items-center justify-center">
|
||||
{{entry().distance }} km
|
||||
</div>
|
||||
|
||||
<div class="col-span-4 sm:col-span-2 md:col-span-1 flex my-auto items-center justify-center">
|
||||
{{entry().amount }} ℓ
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-red-500 text-white rounded-r text-center flex flex-col justify-center">
|
||||
|
||||
<button type="button" title="Löschen" class="reset cursor-pointer primary-color-text p-4 h-full rounded-r"
|
||||
(click)="confirmDeleteEntry()">
|
||||
<ng-icon name="matDeleteSharp"></ng-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
.edit-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -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<Consumption>();
|
||||
|
||||
readonly entryDeleted = output<Consumption>();
|
||||
|
||||
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<void> {
|
||||
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<never> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,8 @@
|
||||
<ng-template #list let-entries>
|
||||
<div class="flex flex-col gap-2">
|
||||
@for (entry of entries; track entry.id) {
|
||||
{{ entry | json }}
|
||||
<!-- <app-weight-entry-card [weightEntry]="weightEntry"
|
||||
(entryDeleted)="onEntryDeleted($event)"></app-weight-entry-card> -->
|
||||
<app-entry-card [entry]="entry"
|
||||
(entryDeleted)="onEntryDeleted($event)" />
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -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<ConsumptionEntry[]>;
|
||||
@@ -51,13 +53,11 @@ export class EntriesComponent {
|
||||
private readonly deletedEntries$ = new BehaviorSubject(<string[]>[]);
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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<Car[]> | null = null;
|
||||
|
||||
private ensureCarsAreCached(): void {
|
||||
if (this.cachedCars$ !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cachedCars$ = this.carClient.getAll()
|
||||
.pipe(
|
||||
map(response => response.cars),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
getEntries(): Observable<Consumption[]> {
|
||||
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<Car[]> {
|
||||
this.ensureCarsAreCached();
|
||||
return this.cachedCars$!;
|
||||
}
|
||||
}
|
||||
21
src/Vegasco-Web/src/app/services/routing.service.ts
Normal file
21
src/Vegasco-Web/src/app/services/routing.service.ts
Normal file
@@ -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<void> {
|
||||
await this.router.navigateByUrl('/entries');
|
||||
}
|
||||
|
||||
async navigateToEditEntry(entryId: string): Promise<void> {
|
||||
await this.router.navigate(['entries', 'edit', entryId]);
|
||||
}
|
||||
|
||||
async navigateToCreateEntry(): Promise<void> {
|
||||
await this.router.navigate(['entries', 'create']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user