New Angular based web version #1

Closed
thomas.nuyken wants to merge 150 commits from main into ddd
11 changed files with 312 additions and 29 deletions
Showing only changes of commit c5555b3003 - Show all commits

View File

@@ -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",

View File

@@ -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)

View File

@@ -0,0 +1,9 @@
export interface Consumption {
id: string;
dateTime: string;
distance: number;
amount: number;
ignoreInCalculation: boolean;
carId: string;
car: Car;
}

View File

@@ -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',

View File

@@ -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 }} &#8467;
</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>

View File

@@ -0,0 +1,3 @@
.edit-button {
cursor: pointer;
}

View File

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

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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$!;
}
}

View 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']);
}
}