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.
This commit is contained in:
2025-11-21 02:32:43 +01:00
parent 11ba07d550
commit 3db3876a8b
7 changed files with 170 additions and 4 deletions

View File

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

View File

@@ -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},
],
},

View File

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

View File

@@ -0,0 +1,7 @@
.middle {
position: relative;
top: 40%;
-webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%);
transform: translateY(-50%);
}

View File

@@ -0,0 +1,40 @@
<app-menu-header label="Tickets bezahlen" icon="payment_card"></app-menu-header>
<div class="w-100 m-auto middle">
<form class="order-search-form w-full" (ngSubmit)="DoSubmit()">
<div class="flex items-center space-x-4">
<mat-form-field class="w-full" subscriptSizing="dynamic">
<mat-label>Reservierungsnummer eingeben</mat-label>
<input class="w-full" type="text" matInput [formControl]="formControl" (input)="onInput($event)" placeholder="XXXXXX" maxlength="6" autocomplete="off">
<mat-error>
@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
}
</mat-error>
</mat-form-field>
</div>
@if (formControl.valid || !formControl.touched) {
<div class="h-6"></div>
}
<button mat-button class="w-100 mt-2" matButton="filled" color="accent" [disabled]="(loadingService.loading$ | async) || formControl.invalid" type="submit">Tickets jetzt online bezahlen</button>
</form>
</div>

View File

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

View File

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