All checks were successful
continuous-integration/drone/push Build is passing
294 lines
9.7 KiB
TypeScript
294 lines
9.7 KiB
TypeScript
import dayjs from 'dayjs';
|
|
import { HttpErrorResponse } from '@angular/common/http';
|
|
import { Component, computed, DestroyRef, inject, input, OnInit, signal, Signal } from '@angular/core';
|
|
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
import { FormControl, FormGroup, ReactiveFormsModule, ValidationErrors, Validators } from '@angular/forms';
|
|
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';
|
|
import { FloatLabelModule } from 'primeng/floatlabel';
|
|
import { InputGroupModule } from 'primeng/inputgroup';
|
|
import { InputGroupAddonModule } from 'primeng/inputgroupaddon';
|
|
import { InputNumberModule } from 'primeng/inputnumber';
|
|
import { InputTextModule } from 'primeng/inputtext';
|
|
import { MultiSelectModule } from 'primeng/multiselect';
|
|
import { SelectModule } from 'primeng/select';
|
|
import { SkeletonModule } from 'primeng/skeleton';
|
|
import { catchError, combineLatest, EMPTY, filter, map, Observable, switchMap, tap, throwError } from 'rxjs';
|
|
import { RequiredMarkerComponent } from './components/required-marker.component';
|
|
import { SelectedCarService } from '../services/selected-car.service';
|
|
|
|
@Component({
|
|
selector: 'app-edit-entry',
|
|
imports: [
|
|
ButtonModule,
|
|
ChipModule,
|
|
DatePickerModule,
|
|
FloatLabelModule,
|
|
InputGroupAddonModule,
|
|
InputGroupModule,
|
|
InputNumberModule,
|
|
InputTextModule,
|
|
MultiSelectModule,
|
|
ReactiveFormsModule,
|
|
RequiredMarkerComponent,
|
|
SelectModule,
|
|
SkeletonModule,
|
|
],
|
|
templateUrl: './edit-entry.component.html',
|
|
styleUrl: './edit-entry.component.scss'
|
|
})
|
|
export class EditEntryComponent implements OnInit {
|
|
private readonly carClient = inject(CarClient);
|
|
private readonly consumptionClient = inject(ConsumptionClient);
|
|
private readonly routingService = inject(RoutingService);
|
|
private readonly destroyRef = inject(DestroyRef);
|
|
private readonly messageService = inject(MessageService);
|
|
private readonly selectedCarService = inject(SelectedCarService);
|
|
|
|
protected readonly id = input<string | undefined>(undefined);
|
|
|
|
protected readonly today = new Date();
|
|
|
|
protected readonly formFieldNames = {
|
|
car: 'car',
|
|
date: 'date',
|
|
mileage: 'mileage',
|
|
amount: 'amount',
|
|
} as const;
|
|
|
|
protected readonly formGroup = new FormGroup({
|
|
[this.formFieldNames.car]: new FormControl<Car | null>({ value: null, disabled: true }, [Validators.required]),
|
|
[this.formFieldNames.date]: new FormControl<Date>({ value: new Date(), disabled: true }, [Validators.required, this.dateTimeGreaterThanOrEqualToTodayValidator]),
|
|
[this.formFieldNames.mileage]: new FormControl<number | null>({ value: null, disabled: true }, [Validators.required, Validators.min(1)]),
|
|
[this.formFieldNames.amount]: new FormControl<number | null>({ value: null, disabled: true }, [Validators.required, Validators.min(1)]),
|
|
});
|
|
|
|
private readonly cars$: Observable<Car[]>;
|
|
protected readonly cars: Signal<Car[] | undefined>;
|
|
|
|
private readonly isEntryDataLoaded = signal(false);
|
|
|
|
protected readonly isLoading = computed(() => {
|
|
var cars = this.cars();
|
|
var isEntryDataLoaded = this.isEntryDataLoaded();
|
|
return cars === undefined || !isEntryDataLoaded;
|
|
});
|
|
|
|
constructor() {
|
|
this.cars$ = this.carClient
|
|
.getAll()
|
|
.pipe(
|
|
takeUntilDestroyed(),
|
|
map(response => response.cars
|
|
.sort((a, b) => a.name.localeCompare(b.name))),
|
|
tap(cars => {
|
|
const selectedCarId = this.selectedCarService.getSelectedCarId();
|
|
|
|
if (selectedCarId === null) {
|
|
const firstCar = cars[0];
|
|
this.formGroup.controls[this.formFieldNames.car].setValue(firstCar);
|
|
this.selectedCarService.setSelectedCarId(firstCar?.id ?? null);
|
|
return;
|
|
}
|
|
|
|
const selectedCar = cars.find(car => car.id === selectedCarId);
|
|
this.formGroup.controls[this.formFieldNames.car].setValue(selectedCar ?? null);
|
|
this.selectedCarService.setSelectedCarId(selectedCar?.id ?? null);
|
|
}),
|
|
);
|
|
this.cars = toSignal(this.cars$);
|
|
}
|
|
|
|
ngOnInit(): void {
|
|
this.loadEntryDetailsAndEnableControls();
|
|
|
|
this.formGroup.controls[this.formFieldNames.car]
|
|
.valueChanges
|
|
.pipe(
|
|
takeUntilDestroyed(this.destroyRef),
|
|
tap((car) => {
|
|
this.selectedCarService.setSelectedCarId(car?.id ?? null);
|
|
})
|
|
)
|
|
.subscribe();
|
|
}
|
|
|
|
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> {
|
|
await this.routingService.navigateToEntries();
|
|
}
|
|
|
|
onSubmit(): void {
|
|
if (this.formGroup.invalid) {
|
|
this.formGroup.markAllAsTouched();
|
|
return;
|
|
}
|
|
|
|
var entryId = this.id();
|
|
if (entryId === undefined || entryId === null) {
|
|
this.createEntry();
|
|
return;
|
|
}
|
|
|
|
this.updateEntry(entryId);
|
|
}
|
|
|
|
private getFormData() {
|
|
var dateTime = new Date((this.formGroup.controls[this.formFieldNames.date].value ?? new Date).setHours(0, 0, 0, 0));
|
|
|
|
return {
|
|
carId: this.formGroup.controls[this.formFieldNames.car].value!.id,
|
|
dateTime: dateTime.toISOString(),
|
|
distance: this.formGroup.controls[this.formFieldNames.mileage].value!,
|
|
amount: this.formGroup.controls[this.formFieldNames.amount].value!,
|
|
};
|
|
}
|
|
|
|
createEntry() {
|
|
var request: CreateConsumptionEntry = this.getFormData();
|
|
this.consumptionClient.create(request)
|
|
.pipe(
|
|
takeUntilDestroyed(this.destroyRef),
|
|
catchError((error) => this.handleCreateOrUpdateError(error)),
|
|
switchMap(() => this.routingService.navigateToEntries())
|
|
)
|
|
.subscribe();
|
|
}
|
|
|
|
updateEntry(id: string) {
|
|
var request: UpdateConsumptionEntry = this.getFormData();
|
|
this.consumptionClient.update(id, request)
|
|
.pipe(
|
|
takeUntilDestroyed(this.destroyRef),
|
|
catchError((error) => this.handleCreateOrUpdateError(error)),
|
|
switchMap(() => this.routingService.navigateToEntries())
|
|
)
|
|
.subscribe();
|
|
}
|
|
|
|
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)) {
|
|
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;
|
|
}
|
|
|
|
private dateTimeGreaterThanOrEqualToTodayValidator(control: FormControl<Date>): ValidationErrors | null {
|
|
const tomorrowStartOfDay = dayjs().add(1, 'day').startOf('day');
|
|
const controlDate = dayjs(control.value);
|
|
|
|
if (controlDate.isBefore(tomorrowStartOfDay)) {
|
|
return null;
|
|
}
|
|
|
|
return { dateTimeGreaterThanOrEqualToToday: true };
|
|
}
|
|
}
|