From 3db3876a8bf19f6efaca6563d7c00dfec610e757 Mon Sep 17 00:00:00 2001 From: Piet Ostendorp Date: Fri, 21 Nov 2025 02:32:43 +0100 Subject: [PATCH] Add pay-for-order component for ticket payment Introduces PayForOrderComponent with form validation and error handling for ticket payment by order code. Updates routing, navigation, and module declarations to support the new feature. --- src/app/app-module.ts | 2 + src/app/app-routing-module.ts | 2 + src/app/navbar/navbar.component.ts | 1 + .../pay-for-order/pay-for-order.component.css | 7 ++ .../pay-for-order.component.html | 40 +++++++ .../pay-for-order/pay-for-order.component.ts | 112 ++++++++++++++++++ .../theater-overlay.component.ts | 10 +- 7 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 src/app/pay-for-order/pay-for-order.component.css create mode 100644 src/app/pay-for-order/pay-for-order.component.html create mode 100644 src/app/pay-for-order/pay-for-order.component.ts diff --git a/src/app/app-module.ts b/src/app/app-module.ts index c625000..59d2558 100644 --- a/src/app/app-module.ts +++ b/src/app/app-module.ts @@ -68,6 +68,7 @@ import { SelectionConflictInfoComponent } from './selection-conflict-info/select import { CancellationSuccessComponent } from './cancellation-success/cancellation-success.component'; import { CancellationFailedComponent } from './cancellation-failed/cancellation-failed.component'; import { ConversionFailedComponent } from './conversion-failed/conversion-failed.component'; +import { PayForOrderComponent } from './pay-for-order/pay-for-order.component'; @NgModule({ @@ -115,6 +116,7 @@ import { ConversionFailedComponent } from './conversion-failed/conversion-failed CancellationSuccessComponent, CancellationFailedComponent, ConversionFailedComponent, + PayForOrderComponent, ], imports: [ AppRoutingModule, diff --git a/src/app/app-routing-module.ts b/src/app/app-routing-module.ts index 1678c5f..5451ddc 100644 --- a/src/app/app-routing-module.ts +++ b/src/app/app-routing-module.ts @@ -8,6 +8,7 @@ import { ScheduleComponent } from './schedule/schedule.component'; import { TheaterOverlayComponent} from './theater-overlay/theater-overlay.component'; import { MovieImporterComponent } from './movie-importer/movie-importer.component'; import { AuthGuard } from './auth.guard'; +import { PayForOrderComponent } from './pay-for-order/pay-for-order.component'; const routes: Routes = [ // Seiten ohne Layout @@ -29,6 +30,7 @@ const routes: Routes = [ }, { path: 'checkout/performance/:performanceId', component: TheaterOverlayComponent}, { path: 'checkout/order/:orderId', component: TheaterOverlayComponent}, + { path: 'checkout/order', component: PayForOrderComponent}, ], }, diff --git a/src/app/navbar/navbar.component.ts b/src/app/navbar/navbar.component.ts index 5f2de5f..56a5ff0 100644 --- a/src/app/navbar/navbar.component.ts +++ b/src/app/navbar/navbar.component.ts @@ -10,6 +10,7 @@ import { Component, inject, computed, OnInit } from '@angular/core'; export class NavbarComponent { navItems: { label:string, path:string }[] = [ {label: 'Programm', path: '/schedule'}, + {label: 'Bezahlen', path: '/checkout/order'}, {label: 'Film importieren', path: '/admin/movie-importer'}, ] diff --git a/src/app/pay-for-order/pay-for-order.component.css b/src/app/pay-for-order/pay-for-order.component.css new file mode 100644 index 0000000..7292745 --- /dev/null +++ b/src/app/pay-for-order/pay-for-order.component.css @@ -0,0 +1,7 @@ +.middle { + position: relative; + top: 40%; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); +} diff --git a/src/app/pay-for-order/pay-for-order.component.html b/src/app/pay-for-order/pay-for-order.component.html new file mode 100644 index 0000000..bac0bdd --- /dev/null +++ b/src/app/pay-for-order/pay-for-order.component.html @@ -0,0 +1,40 @@ + + +
+
+
+ + Reservierungsnummer eingeben + + + @if (formControl.hasError('invalid')) { + Ungültiger Bestellcode + } + @else if (formControl.hasError('completed')) { + Diese Bestellung wurde bereits abgeschlossen + } + @else if (formControl.hasError('required')) { + Bitte geben Sie Ihren Code ein + } + @else if (formControl.hasError('severalOrders')) { + Mehrere Bestellungen gefunden - bitte kontaktieren Sie den Support + } + @else if (formControl.hasError('alreadyBooked')) { + Diese Bestellung wurde bereits bezahlt + } + @else if (formControl.hasError('cancelled')) { + Diese Bestellung wurde storniert + } + @else if (formControl.hasError('serverError')) { + Fehler beim Laden der Bestellung + } + + +
+ @if (formControl.valid || !formControl.touched) { +
+ } + +
+ +
diff --git a/src/app/pay-for-order/pay-for-order.component.ts b/src/app/pay-for-order/pay-for-order.component.ts new file mode 100644 index 0000000..c12b83b --- /dev/null +++ b/src/app/pay-for-order/pay-for-order.component.ts @@ -0,0 +1,112 @@ +import { Component, DestroyRef, inject, OnInit } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { LoadingService } from '../loading.service'; +import { HttpService } from '../http.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { catchError, map, of, take } from 'rxjs'; +import { ActivatedRoute, Router } from '@angular/router'; + +@Component({ + selector: 'app-pay-for-order', + standalone: false, + templateUrl: './pay-for-order.component.html', + styleUrl: './pay-for-order.component.css', +}) +export class PayForOrderComponent implements OnInit { + private httpService = inject(HttpService); + private router = inject(Router); + private route = inject(ActivatedRoute); + private destroyRef = inject(DestroyRef); + public loadingService = inject(LoadingService); + + queryError?: string; + + formControl = new FormControl('', { + validators: [ + Validators.required, + Validators.minLength(6), + Validators.maxLength(6) + ] + }); + + ngOnInit() { + const error = this.route.snapshot.queryParamMap.get('error'); + const code = this.route.snapshot.queryParamMap.get('code'); + + if (code) { + this.formControl.setValue(code); + } + + if (error) { + // Warte einen Tick, damit Angular das FormControl initialisiert hat + setTimeout(() => { + this.formControl.clearValidators(); + this.formControl.setErrors({ [error]: true }); + this.formControl.markAsTouched(); + }); + + // Bei erster Änderung: Validatoren wieder aktivieren + this.formControl.valueChanges.pipe( + take(1), + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.formControl.setValidators([ + Validators.required, + Validators.minLength(6), + Validators.maxLength(6) + ]); + this.formControl.updateValueAndValidity(); + }); + } + } + + onInput(event: Event) { + this.queryError = undefined; + const input = event.target as HTMLInputElement; + const filtered = input.value.toUpperCase().replace(/[^A-Z0-9]/g, ''); + this.formControl.setValue(filtered, { emitEvent: false }); + } + + DoSubmit() { + this.formControl.markAsTouched(); + if (this.formControl.invalid) return; + + const code = this.formControl.value?.trim(); + if (!code || code.length !== 6) return; + + this.loadingService.show(); + const orderFilter = [`eq;code;string;${code}`]; + + this.httpService.getOrdersByFilter(orderFilter).pipe( + map(orders => { + this.loadingService.hide(); + if (orders.length === 0) { + this.formControl.setErrors({ invalid: true }); + return + } + + if (orders.length > 1) { + this.formControl.setErrors({ severalOrders: true }); + return; + } + const order = orders[0]; + if (order.booked) { + this.formControl.setErrors({ alreadyBooked: true }); + return; + } + if (order.cancelled) { + this.formControl.setErrors({ cancelled: true }); + return; + } + this.router.navigate(['/checkout/order', order.code]); + }), + catchError(err => { + this.loadingService.hide(); + this.loadingService.showError(err); + this.formControl.setErrors({ serverError: true }); + return of(null); + }), + takeUntilDestroyed(this.destroyRef) + ).subscribe(); + } +} diff --git a/src/app/theater-overlay/theater-overlay.component.ts b/src/app/theater-overlay/theater-overlay.component.ts index 270f272..4545191 100644 --- a/src/app/theater-overlay/theater-overlay.component.ts +++ b/src/app/theater-overlay/theater-overlay.component.ts @@ -242,7 +242,7 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy { if (!tickets.length) { return from(this.router.navigate( ['/checkout/order'], - { queryParams: { error: 'invalid' } } + { queryParams: { error: 'invalid', code: orderCode } } )); } @@ -251,7 +251,7 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy { if (this.order.booked || this.order.cancelled) { return from(this.router.navigate( ['/checkout/order'], - { queryParams: { error: 'completed' } } + { queryParams: { error: 'completed', code: orderCode } } )); } @@ -264,14 +264,16 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy { return this.loadPerformanceAndSeats(); }), catchError(err => { - this.loading.hide(); console.error('Fehler beim Laden der Bestellung', err); return from(this.router.navigate( ['/checkout/order'], - { queryParams: { error: 'invalid' } } + { queryParams: { error: 'invalid', code: orderCode } } )); }), + finalize(() => { + this.loading.hide(); + }), takeUntilDestroyed(this.destroyRef) ).subscribe(); }