Compare commits
4 Commits
92e4da4b93
...
1c8e02b3fa
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c8e02b3fa | |||
| feadab4dff | |||
| 41c342bb0f | |||
| 2e3000c3fc |
2
src/Vegasco-Web/.vscode/launch.json
vendored
2
src/Vegasco-Web/.vscode/launch.json
vendored
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Launch (Chrome)",
|
"name": "Launch Web (Chrome)",
|
||||||
"type": "chrome",
|
"type": "chrome",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "npm: start",
|
"preLaunchTask": "npm: start",
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
@if (isLoading()) {
|
@if (isLoading()) {
|
||||||
<p-skeleton height="4rem" styleClass="mb-2" />
|
<div class="flex flex-col gap-6">
|
||||||
|
<p-skeleton height="3.5rem" />
|
||||||
|
<p-skeleton height="3.5rem" />
|
||||||
|
<p-skeleton height="3.5rem" />
|
||||||
|
<p-skeleton height="3.5rem" />
|
||||||
|
<div class="flex flex-row gap-4">
|
||||||
|
<p-skeleton height="3.5rem" width="10rem" />
|
||||||
|
<p-skeleton height="3.5rem" width="10rem" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<form [formGroup]="formGroup" class="flex flex-col gap-4" (ngSubmit)="onSubmit()">
|
<form [formGroup]="formGroup" class="flex flex-col gap-4" (ngSubmit)="onSubmit()">
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { HttpErrorResponse } from '@angular/common/http';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
import { Component, computed, DestroyRef, inject, input, Signal } from '@angular/core';
|
import { Component, computed, DestroyRef, inject, input, OnInit, signal, Signal } from '@angular/core';
|
||||||
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
||||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { CarClient } from '@vegasco-web/api/cars/car-client';
|
import { CarClient } from '@vegasco-web/api/cars/car-client';
|
||||||
import { ConsumptionClient } from '@vegasco-web/api/consumptions/consumption-client';
|
import { ConsumptionClient } from '@vegasco-web/api/consumptions/consumption-client';
|
||||||
import { RoutingService } from '@vegasco-web/services/routing.service';
|
import { RoutingService } from '@vegasco-web/services/routing.service';
|
||||||
@@ -17,7 +17,7 @@ import { InputTextModule } from 'primeng/inputtext';
|
|||||||
import { MultiSelectModule } from 'primeng/multiselect';
|
import { MultiSelectModule } from 'primeng/multiselect';
|
||||||
import { SelectModule } from 'primeng/select';
|
import { SelectModule } from 'primeng/select';
|
||||||
import { SkeletonModule } from 'primeng/skeleton';
|
import { SkeletonModule } from 'primeng/skeleton';
|
||||||
import { catchError, EMPTY, map, Observable, switchMap, throwError } from 'rxjs';
|
import { catchError, combineLatest, EMPTY, filter, map, Observable, switchMap, tap, throwError } from 'rxjs';
|
||||||
import { RequiredMarkerComponent } from './components/required-marker.component';
|
import { RequiredMarkerComponent } from './components/required-marker.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -40,15 +40,14 @@ import { RequiredMarkerComponent } from './components/required-marker.component'
|
|||||||
templateUrl: './edit-entry.component.html',
|
templateUrl: './edit-entry.component.html',
|
||||||
styleUrl: './edit-entry.component.scss'
|
styleUrl: './edit-entry.component.scss'
|
||||||
})
|
})
|
||||||
export class EditEntryComponent {
|
export class EditEntryComponent implements OnInit {
|
||||||
private readonly formBuilder = inject(FormBuilder);
|
|
||||||
private readonly carClient = inject(CarClient);
|
private readonly carClient = inject(CarClient);
|
||||||
private readonly consumptionClient = inject(ConsumptionClient);
|
private readonly consumptionClient = inject(ConsumptionClient);
|
||||||
private readonly routingService = inject(RoutingService);
|
private readonly routingService = inject(RoutingService);
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
private readonly messageService = inject(MessageService);
|
private readonly messageService = inject(MessageService);
|
||||||
|
|
||||||
protected readonly entryId = input<string | undefined>(undefined);
|
protected readonly id = input<string | undefined>(undefined);
|
||||||
|
|
||||||
protected readonly formFieldNames = {
|
protected readonly formFieldNames = {
|
||||||
car: 'car',
|
car: 'car',
|
||||||
@@ -57,34 +56,82 @@ export class EditEntryComponent {
|
|||||||
amount: 'amount',
|
amount: 'amount',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
protected readonly formGroup = this.formBuilder.group({
|
protected readonly formGroup = new FormGroup({
|
||||||
[this.formFieldNames.car]: [<Car | null>null, Validators.required],
|
[this.formFieldNames.car]: new FormControl<Car | null>({ value: null, disabled: true }, [Validators.required]),
|
||||||
[this.formFieldNames.date]: [<Date>new Date(), Validators.required],
|
[this.formFieldNames.date]: new FormControl<Date>({ value: new Date(), disabled: true }, [Validators.required]),
|
||||||
[this.formFieldNames.mileage]: [
|
[this.formFieldNames.mileage]: new FormControl<number | null>({ value: null, disabled: true }, [Validators.required, Validators.min(1)]),
|
||||||
<number | null>null,
|
[this.formFieldNames.amount]: new FormControl<number | null>({ value: null, disabled: true }, [Validators.required, Validators.min(1)]),
|
||||||
[Validators.required, Validators.min(1)],
|
|
||||||
],
|
|
||||||
[this.formFieldNames.amount]: [
|
|
||||||
<number | null>null,
|
|
||||||
[Validators.required, Validators.min(1)],
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
private readonly cars$: Observable<Car[]>;
|
||||||
protected readonly cars: Signal<Car[] | undefined>;
|
protected readonly cars: Signal<Car[] | undefined>;
|
||||||
|
|
||||||
|
private readonly isEntryDataLoaded = signal(false);
|
||||||
|
|
||||||
protected readonly isLoading = computed(() => {
|
protected readonly isLoading = computed(() => {
|
||||||
return this.cars() === undefined;
|
var cars = this.cars();
|
||||||
})
|
var isEntryDataLoaded = this.isEntryDataLoaded();
|
||||||
|
return cars === undefined || !isEntryDataLoaded;
|
||||||
|
});
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.cars = toSignal(
|
this.cars$ = this.carClient
|
||||||
this.carClient
|
|
||||||
.getAll()
|
.getAll()
|
||||||
.pipe(
|
.pipe(
|
||||||
takeUntilDestroyed(),
|
takeUntilDestroyed(),
|
||||||
map(response => response.cars)
|
map(response => response.cars)
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
this.cars = toSignal(this.cars$);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.loadEntryDetailsAndEnableControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadEntryDetailsAndEnableControls() {
|
||||||
|
const entryId = this.id();
|
||||||
|
|
||||||
|
if (entryId === undefined || entryId === null) {
|
||||||
|
this.enableFormControls();
|
||||||
|
this.isEntryDataLoaded.set(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const consumption$ = this.consumptionClient
|
||||||
|
.getSingle(entryId);
|
||||||
|
|
||||||
|
combineLatest([
|
||||||
|
consumption$, this.cars$
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
filter(([_, cars]) => cars !== undefined),
|
||||||
|
takeUntilDestroyed(this.destroyRef),
|
||||||
|
catchError((error) => this.handleGetError(error)),
|
||||||
|
tap(([consumption, cars]) => {
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
[this.formFieldNames.car]: cars!.find(c => c.id === consumption.carId) ?? null,
|
||||||
|
[this.formFieldNames.date]: new Date(consumption.dateTime),
|
||||||
|
[this.formFieldNames.mileage]: consumption.distance,
|
||||||
|
[this.formFieldNames.amount]: consumption.amount,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
tap(() => {
|
||||||
|
this.enableFormControls();
|
||||||
|
this.isEntryDataLoaded.set(true);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private enableFormControls(): void {
|
||||||
|
for (const controlName of Object.values(this.formFieldNames)) {
|
||||||
|
const control = this.formGroup.get(controlName);
|
||||||
|
if (control) {
|
||||||
|
control.enable();
|
||||||
|
} else {
|
||||||
|
console.warn(`Form control '${controlName}' not found.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async navigateToOverviewPage(): Promise<void> {
|
async navigateToOverviewPage(): Promise<void> {
|
||||||
@@ -92,7 +139,7 @@ export class EditEntryComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSubmit(): void {
|
onSubmit(): void {
|
||||||
var entryId = this.entryId();
|
var entryId = this.id();
|
||||||
if (entryId === undefined || entryId === null) {
|
if (entryId === undefined || entryId === null) {
|
||||||
this.createEntry();
|
this.createEntry();
|
||||||
return;
|
return;
|
||||||
@@ -118,7 +165,7 @@ export class EditEntryComponent {
|
|||||||
this.consumptionClient.create(request)
|
this.consumptionClient.create(request)
|
||||||
.pipe(
|
.pipe(
|
||||||
takeUntilDestroyed(this.destroyRef),
|
takeUntilDestroyed(this.destroyRef),
|
||||||
catchError((error) => this.handleError(error)),
|
catchError((error) => this.handleCreateOrUpdateError(error)),
|
||||||
switchMap(() => this.routingService.navigateToEntries())
|
switchMap(() => this.routingService.navigateToEntries())
|
||||||
)
|
)
|
||||||
.subscribe();
|
.subscribe();
|
||||||
@@ -129,13 +176,41 @@ export class EditEntryComponent {
|
|||||||
this.consumptionClient.update(id, request)
|
this.consumptionClient.update(id, request)
|
||||||
.pipe(
|
.pipe(
|
||||||
takeUntilDestroyed(this.destroyRef),
|
takeUntilDestroyed(this.destroyRef),
|
||||||
catchError((error) => this.handleError(error)),
|
catchError((error) => this.handleCreateOrUpdateError(error)),
|
||||||
switchMap(() => this.routingService.navigateToEntries())
|
switchMap(() => this.routingService.navigateToEntries())
|
||||||
)
|
)
|
||||||
.subscribe();
|
.subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleError(error: unknown): Observable<never> {
|
private handleGetError(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;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleCreateOrUpdateError(error: unknown): Observable<never> {
|
||||||
if (!(error instanceof HttpErrorResponse)) {
|
if (!(error instanceof HttpErrorResponse)) {
|
||||||
return throwError(() => error);
|
return throwError(() => error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,13 @@ import { SelectModule } from 'primeng/select';
|
|||||||
import { SkeletonModule } from 'primeng/skeleton';
|
import { SkeletonModule } from 'primeng/skeleton';
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
|
catchError,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
|
EMPTY,
|
||||||
map,
|
map,
|
||||||
Observable,
|
Observable,
|
||||||
startWith
|
startWith,
|
||||||
|
throwError
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { EntriesOverviewService } from './services/entries-overview.service';
|
import { EntriesOverviewService } from './services/entries-overview.service';
|
||||||
import { EntryCardComponent } from './components/entry-card/entry-card.component';
|
import { EntryCardComponent } from './components/entry-card/entry-card.component';
|
||||||
@@ -22,6 +25,7 @@ import {
|
|||||||
matAddSharp,
|
matAddSharp,
|
||||||
} from '@ng-icons/material-icons/sharp';
|
} from '@ng-icons/material-icons/sharp';
|
||||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-entries',
|
selector: 'app-entries',
|
||||||
@@ -62,7 +66,10 @@ export class EntriesComponent {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const entries = this.entriesOverviewService.getEntries()
|
const entries = this.entriesOverviewService.getEntries()
|
||||||
.pipe(takeUntilDestroyed());
|
.pipe(
|
||||||
|
takeUntilDestroyed(),
|
||||||
|
catchError((error) => this.handleGetEntriesError(error))
|
||||||
|
);
|
||||||
|
|
||||||
this.consumptionEntries$ = combineLatest([
|
this.consumptionEntries$ = combineLatest([
|
||||||
entries,
|
entries,
|
||||||
@@ -97,4 +104,32 @@ export class EntriesComponent {
|
|||||||
detail: 'Der Eintrag wurde erfolgreich gelöscht.',
|
detail: 'Der Eintrag wurde erfolgreich gelöscht.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleGetEntriesError(error: unknown): Observable<never> {
|
||||||
|
if (!(error instanceof HttpErrorResponse)) {
|
||||||
|
return throwError(() => new Error('An unexpected error occurred'));
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case error.status >= 500 && error.status <= 599:
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Serverfehler',
|
||||||
|
detail:
|
||||||
|
'Beim Abrufen der Einträge ist ein Fehler aufgetreten. Bitte versuche es erneut.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(error);
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Unerwarteter Fehler',
|
||||||
|
detail:
|
||||||
|
'Beim Abrufen der Einträge hat der Server eine unerwartete Antwort zurückgegeben.',
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ export class EntriesOverviewService {
|
|||||||
return combineLatest([this.cachedCars$!, entries$])
|
return combineLatest([this.cachedCars$!, entries$])
|
||||||
.pipe(
|
.pipe(
|
||||||
map(([cars, entries]) => {
|
map(([cars, entries]) => {
|
||||||
return entries.map((entry): Consumption => ({
|
return entries
|
||||||
|
.sort((a, b) => b.dateTime.localeCompare(a.dateTime))
|
||||||
|
.map((entry): Consumption => ({
|
||||||
...entry,
|
...entry,
|
||||||
car: cars.find(car => car.id === entry.carId)!
|
car: cars.find(car => car.id === entry.carId)!
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ public static class GetConsumptions
|
|||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
List<ResponseDto> consumptions = await dbContext.Consumptions
|
List<ResponseDto> consumptions = await dbContext.Consumptions
|
||||||
|
.OrderByDescending(x => x.DateTime)
|
||||||
.Select(x =>
|
.Select(x =>
|
||||||
new ResponseDto(x.Id.Value, x.DateTime, x.Distance, x.Amount, x.IgnoreInCalculation, x.CarId.Value))
|
new ResponseDto(x.Id.Value, x.DateTime, x.Distance, x.Amount, x.IgnoreInCalculation, x.CarId.Value))
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|||||||
Reference in New Issue
Block a user