Files
vegasco/src/Vegasco-Web/src/app/modules/entries/edit-entry/edit-entry.component.ts
ThompsonNye 9c372b31a6
All checks were successful
continuous-integration/drone/push Build is passing
Add managing cars
2025-06-23 16:20:44 +02:00

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 };
}
}