Add functionality to cancel tickets

Introduces a cancel order confirmation dialog and integrates it into the order flow. Refactors error and success components to support singular/plural seat messaging and retry actions. Updates navigation and button behaviors for better user experience. Fixes minor UI and logic issues in reservation, purchase, and conversion flows.
This commit is contained in:
2025-11-21 15:53:42 +01:00
parent 3db3876a8b
commit ade5479a74
29 changed files with 266 additions and 58 deletions

View File

@@ -69,6 +69,7 @@ import { CancellationSuccessComponent } from './cancellation-success/cancellatio
import { CancellationFailedComponent } from './cancellation-failed/cancellation-failed.component'; import { CancellationFailedComponent } from './cancellation-failed/cancellation-failed.component';
import { ConversionFailedComponent } from './conversion-failed/conversion-failed.component'; import { ConversionFailedComponent } from './conversion-failed/conversion-failed.component';
import { PayForOrderComponent } from './pay-for-order/pay-for-order.component'; import { PayForOrderComponent } from './pay-for-order/pay-for-order.component';
import { CancelOrderDialog } from './cancel-order/cancel-order.dialog';
@NgModule({ @NgModule({
@@ -117,6 +118,7 @@ import { PayForOrderComponent } from './pay-for-order/pay-for-order.component';
CancellationFailedComponent, CancellationFailedComponent,
ConversionFailedComponent, ConversionFailedComponent,
PayForOrderComponent, PayForOrderComponent,
CancelOrderDialog,
], ],
imports: [ imports: [
AppRoutingModule, AppRoutingModule,

View File

@@ -0,0 +1,3 @@
button {
min-width: 100px;
}

View File

@@ -0,0 +1,10 @@
<h2 mat-dialog-title>Möchten Sie Ihre Bestellung wirklich stornieren?</h2>
<mat-dialog-content class="min-w-[400px]">
<p class="text-sm text-gray-600 mb-2">Nach der Stornierung verlieren Sie Ihr Reservierungsrecht und die Sitzplätze können von anderen Kunden in Anspruch genommen werden. Dieser Prozess kann nicht rückgängig gemacht werden.</p>
</mat-dialog-content>
<mat-dialog-actions class="justify-end gap-2">
<button mat-stroked-button color="warn" (click)="cancel()">Abbrechen</button>
<button mat-flat-button color="primary" (click)="submit()">Stornieren</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,22 @@
import { Component } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
@Component({
selector: 'app-cancel-order',
standalone: false,
templateUrl: './cancel-order.dialog.html',
styleUrl: './cancel-order.dialog.css',
})
export class CancelOrderDialog {
constructor(
private dialogRef: MatDialogRef<CancelOrderDialog>,
) {}
submit(): void {
this.dialogRef.close(true);
}
cancel(): void {
this.dialogRef.close(false);
}
}

View File

@@ -3,9 +3,9 @@
warning warning
</mat-icon> </mat-icon>
<h1 class="text-xl font-bold">Stornierung fehlgeschlagen!</h1> <h1 class="text-xl font-bold">Stornierung fehlgeschlagen!</h1>
<p class="text-center">Leider konnten Ihre Sitzplätze nicht storniert werden. Möglicherweise wurden die Tickets bereits bezahlt oder storniert.</p> <p class="text-center">{{ infoText }}</p>
<button mat-button matButton="filled" class="error-button mt-4 w-80">Erneut versuchen</button> <button mat-button type="button" matButton="filled" class="error-button mt-4 w-80" (click)="retry.emit()">Erneut versuchen</button>
<button routerLink="/order" mat-button matButton="outlined" color="accent" class="error-button w-80 mt-1">Zurück zur Codeeingabe</button> <button routerLink="/order" type="button" mat-button matButton="outlined" color="accent" class="error-button w-80 mt-1">Zurück zur Codeeingabe</button>
</div> </div>

View File

@@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, input, output } from '@angular/core';
@Component({ @Component({
selector: 'app-cancellation-failed', selector: 'app-cancellation-failed',
@@ -7,5 +7,15 @@ import { Component } from '@angular/core';
styleUrl: './cancellation-failed.component.css', styleUrl: './cancellation-failed.component.css',
}) })
export class CancellationFailedComponent { export class CancellationFailedComponent {
moreThanOne = input<boolean>(false);
retry = output<void>();
infoText!: string;
ngOnInit(): void {
this.infoText = this.moreThanOne()?
'Leider konnten Ihre Sitzplätze nicht storniert werden. Möglicherweise wurden die Tickets bereits bezahlt oder storniert.' :
'Leider konnte Ihr Sitzplatz nicht storniert werden. Möglicherweise wurde das Ticket bereits bezahlt oder storniert.';
}
} }

View File

@@ -1,11 +1,11 @@
<div class="bg-green-200 rounded-md shadow-sm w-full h-fit p-6 py-8 items-center justify-center flex flex-col space-y-2"> <div class="bg-indigo-100 rounded-md shadow-sm w-full h-fit p-6 py-8 items-center justify-center flex flex-col space-y-2">
<mat-icon class="material-symbols-outlined mb-5" style="font-size: 50px; width: 50px; height: 50px"> <mat-icon class="material-symbols-outlined mb-5" style="font-size: 50px; width: 50px; height: 50px">
task_alt task_alt
</mat-icon> </mat-icon>
<h1 class="text-xl font-bold">Stornierung erfolgreich!</h1> <h1 class="text-xl font-bold">Stornierung erfolgreich!</h1>
<p class="text-center">Ihre Sitzplätze wurden erfolgreich storniert und stehen wieder zur Buchung zur Verfügnug.</p> <p class="text-center">{{ infoText }}</p>
<button routerLink="/schedule" mat-button matButton="filled" color="accent" class="success-button w-80 mt-4">Zur Programmauswahl</button> <button routerLink="/schedule" type="button" mat-button matButton="filled" color="accent" class="w-80 mt-4">Zur Programmauswahl</button>
<button routerLink="/checkout/performance/{{performanceId()}}" mat-button matButton="outlined" class="success-button w-80 mt-1">Neue Tickets kaufen</button> <button type="button" mat-button matButton="outlined" class="w-80 mt-1" (click)="navigate()">Neue Tickets kaufen</button>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { Component, input } from '@angular/core'; import { Component, inject, input } from '@angular/core';
import { Router } from '@angular/router';
@Component({ @Component({
selector: 'app-cancellation-success', selector: 'app-cancellation-success',
@@ -8,4 +9,19 @@ import { Component, input } from '@angular/core';
}) })
export class CancellationSuccessComponent { export class CancellationSuccessComponent {
performanceId = input.required<number>(); performanceId = input.required<number>();
moreThanOne = input<boolean>(false);
router = inject(Router);
infoText!: string;
ngOnInit(): void {
this.infoText = this.moreThanOne()?
'Ihre Sitzplätze wurden erfolgreich storniert und stehen wieder zur Buchung zur Verfügnug.' :
'Ihr Sitzplatz wurden erfolgreich storniert und steht wieder zur Buchung zur Verfügnug.';
}
navigate() {
window.location.href = `/checkout/performance/${this.performanceId()}`;
}
} }

View File

@@ -3,9 +3,9 @@
warning warning
</mat-icon> </mat-icon>
<h1 class="text-xl font-bold">Kauf fehlgeschlagen!</h1> <h1 class="text-xl font-bold">Kauf fehlgeschlagen!</h1>
<p class="text-center">Leider konnten Ihre Sitzplätze nicht bezahlt werden. Möglicherweise wurden die Tickets bereits storniert.</p> <p class="text-center">{{ infoText }}</p>
<button mat-button matButton="filled" class="error-button mt-4 w-80">Erneut versuchen</button> <button mat-button type="button" matButton="filled" class="error-button mt-4 w-80" (click)="retry.emit()">Erneut versuchen</button>
<button routerLink="/order" mat-button matButton="outlined" color="accent" class="error-button w-80 mt-1">Zurück zur Codeeingabe</button> <button routerLink="/order" type="button" mat-button matButton="outlined" color="accent" class="error-button w-80 mt-1">Zurück zur Codeeingabe</button>
</div> </div>

View File

@@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, input, output } from '@angular/core';
@Component({ @Component({
selector: 'app-conversion-failed', selector: 'app-conversion-failed',
@@ -7,5 +7,15 @@ import { Component } from '@angular/core';
styleUrl: './conversion-failed.component.css', styleUrl: './conversion-failed.component.css',
}) })
export class ConversionFailedComponent { export class ConversionFailedComponent {
moreThanOne = input<boolean>(false);
retry = output<void>();
infoText!: string;
ngOnInit(): void {
this.infoText = this.moreThanOne()?
'Leider konnten Ihre Sitzplätze nicht bezahlt werden. Möglicherweise wurden die Tickets bereits storniert.' :
'Leider konnte Ihr Sitzplatz nicht bezahlt werden. Möglicherweise wurde das Ticket bereits storniert.';
}
} }

View File

@@ -1,7 +1,7 @@
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar, MatSnackBarRef } from '@angular/material/snack-bar';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject, Subscription } from 'rxjs';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@@ -13,6 +13,9 @@ export class LoadingService {
public loading$ = this.loadingSubject.asObservable(); public loading$ = this.loadingSubject.asObservable();
public error$ = this.errorSubject.asObservable(); public error$ = this.errorSubject.asObservable();
private currentSnackBarRef?: MatSnackBarRef<any>;
private currentSubscription?: Subscription;
constructor(private snackBar: MatSnackBar) {} constructor(private snackBar: MatSnackBar) {}
show(): void { show(): void {
@@ -23,6 +26,7 @@ export class LoadingService {
hide(): void { hide(): void {
this.loadingSubject.next(false); this.loadingSubject.next(false);
this.errorSubject.next(false); this.errorSubject.next(false);
this.currentSnackBarRef?.dismiss();
} }
showError(messageOrError?: string | HttpErrorResponse | any): void { showError(messageOrError?: string | HttpErrorResponse | any): void {
@@ -35,15 +39,22 @@ export class LoadingService {
const message = this.getErrorMessage(messageOrError); const message = this.getErrorMessage(messageOrError);
const snackBarRef = this.snackBar.open(message, 'Schließen', { if (this.currentSnackBarRef) {
this.currentSubscription?.unsubscribe();
this.currentSnackBarRef.dismiss();
}
this.currentSnackBarRef = this.snackBar.open(message, 'Schließen', {
duration: 0, duration: 0,
panelClass: ['error-snackbar'], panelClass: ['error-snackbar'],
horizontalPosition: 'center', horizontalPosition: 'center',
verticalPosition: 'bottom' verticalPosition: 'bottom'
}); });
snackBarRef.afterDismissed().subscribe(() => { this.currentSubscription = this.currentSnackBarRef.afterDismissed().subscribe(() => {
this.hide(); if (!this.loadingSubject.value) {
this.hide();
}
}); });
} }

View File

@@ -5,7 +5,7 @@
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<mat-form-field class="w-full" subscriptSizing="dynamic"> <mat-form-field class="w-full" subscriptSizing="dynamic">
<mat-label>Film online suchen</mat-label> <mat-label>Film online suchen</mat-label>
<input class="w-full" type="text" matInput [formControl]="formControl"> <input class="w-full" type="text" matInput [formControl]="formControl"/>
@if (formControl.hasError('noResults')) { @if (formControl.hasError('noResults')) {
<mat-error>Keine Suchergebnisse gefunden</mat-error> <mat-error>Keine Suchergebnisse gefunden</mat-error>
} }

View File

@@ -1,9 +1,10 @@
import { LoadingService } from './../loading.service'; import { LoadingService } from './../loading.service';
import { Component, inject } from '@angular/core'; import { Component, DestroyRef, inject } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { catchError, finalize, of, tap } from 'rxjs'; import { catchError, finalize, of, tap } from 'rxjs';
import { HttpService } from '../http.service'; import { HttpService } from '../http.service';
import { OmdbMovie } from '@infinimotion/model-frontend'; import { OmdbMovie } from '@infinimotion/model-frontend';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({ @Component({
selector: 'app-movie-importer', selector: 'app-movie-importer',
@@ -21,13 +22,15 @@ export class MovieImporterComponent {
private httpService = inject(HttpService) private httpService = inject(HttpService)
public loadingService = inject(LoadingService) public loadingService = inject(LoadingService)
private destroyRef = inject(DestroyRef);
DoSubmit() { DoSubmit() {
this.showAll = false; this.showAll = false;
this.searchForMovies(); this.searchForMovies();
} }
searchForMovies() { private searchForMovies() {
this.search_query = this.formControl.value?.trim() || ''; this.search_query = this.formControl.value?.trim() || '';
if (this.search_query?.length == 0) return; if (this.search_query?.length == 0) return;
@@ -48,7 +51,8 @@ export class MovieImporterComponent {
finalize(() => { finalize(() => {
this.isSearching = false; this.isSearching = false;
this.formControl.enable(); this.formControl.enable();
}) }),
takeUntilDestroyed(this.destroyRef)
).subscribe(); ).subscribe();
} }

View File

@@ -1,3 +1,7 @@
:host {
min-width: 500px;
}
mat-stepper { mat-stepper {
background: transparent !important; background: transparent !important;
} }

View File

@@ -28,7 +28,7 @@
} }
<mat-stepper orientation="horizontal" linear="true" [selectedIndex]="isResuming()? 2 : 0" [disableRipple]="true" (selectionChange)="onStepChange($event)" #stepper> <mat-stepper orientation="horizontal" linear="true" [selectedIndex]="isResuming()? 2 : 0" [disableRipple]="true" (selectionChange)="onStepChange($event)" #stepper>
<mat-step [editable]="!isResuming()"> <mat-step [editable]="!isResuming() && !(isReservationSuccess() && !isSubmitting()) && !(isPurchaseSuccess() && !isSubmitting())">
<ng-template matStepLabel>Warenkorb</ng-template> <ng-template matStepLabel>Warenkorb</ng-template>
<div class="performance-info-space"></div> <div class="performance-info-space"></div>
@@ -67,7 +67,7 @@
</div> </div>
</mat-step> </mat-step>
<mat-step [editable]="!isResuming" [stepControl]="dataForm"> <mat-step [editable]="!isResuming() && !(isPurchaseSuccess() && !isSubmitting())" [completed]="isResuming() || dataForm.valid || isPurchaseSuccess() || isSubmitting()" [stepControl]="dataForm">
<form [formGroup]="dataForm"> <form [formGroup]="dataForm">
<ng-template matStepLabel>Anschrift</ng-template> <ng-template matStepLabel>Anschrift</ng-template>
@@ -75,11 +75,11 @@
@if (isReservationSuccess() && !isSubmitting()) { @if (isReservationSuccess() && !isSubmitting()) {
<div class="h-4"></div> <div class="h-4"></div>
<app-reservation-success [order]="createdOrder()!"></app-reservation-success> <app-reservation-success [order]="createdOrder()!" [moreThanOne]="totalSeats() > 1"></app-reservation-success>
} }
@else if (isReservationError() && !isSubmitting()) { @else if (isReservationError() && !isSubmitting() && performance()) {
<div class="h-4"></div> <div class="h-4"></div>
<app-reservation-failed></app-reservation-failed> <app-reservation-failed (retry)="retryReservation()" [performanceId]="performance()!.id" [moreThanOne]="totalSeats() > 1"></app-reservation-failed>
} }
@else { @else {
@@ -123,23 +123,23 @@
@if (isPurchaseSuccess() && !isSubmitting()) { @if (isPurchaseSuccess() && !isSubmitting()) {
<div class="h-4"></div> <div class="h-4"></div>
<app-purchase-success [tickets]="createdTickets()"></app-purchase-success> <app-purchase-success [tickets]="createdTickets()" [moreThanOne]="totalSeats() > 1"></app-purchase-success>
} }
@else if (isPurchaseError() && !isSubmitting()) { @else if (isPurchaseError() && !isSubmitting() && performance()) {
<div class="h-4"></div> <div class="h-4"></div>
<app-purchase-failed></app-purchase-failed> <app-purchase-failed (retry)="retryPurchase()" [performanceId]="performance()!.id" [moreThanOne]="totalSeats() > 1"></app-purchase-failed>
} }
@else if (isConversionError() && !isSubmitting()) { @else if (isConversionError() && !isSubmitting()) {
<div class="h-4"></div> <div class="h-4"></div>
<app-conversion-failed></app-conversion-failed> <app-conversion-failed (retry)="retryConversion()" [moreThanOne]="totalSeats() > 1"></app-conversion-failed >
} }
@else if (isCancellationSuccess() && !isSubmitting() && performance()) { @else if (isCancellationSuccess() && !isSubmitting() && performance()) {
<div class="h-4"></div> <div class="h-4"></div>
<app-cancellation-success [performanceId]="performance()!.id"></app-cancellation-success> <app-cancellation-success [performanceId]="performance()!.id" [moreThanOne]="totalSeats() > 1"></app-cancellation-success>
} }
@else if (isCancellationError() && !isSubmitting()) { @else if (isCancellationError() && !isSubmitting()) {
<div class="h-4"></div> <div class="h-4"></div>
<app-cancellation-failed></app-cancellation-failed> <app-cancellation-failed (retry)="retryCancellation()" [moreThanOne]="totalSeats() > 1"></app-cancellation-failed>
} }
@else { @else {

View File

@@ -8,6 +8,8 @@ import { HttpService } from '../http.service';
import { catchError, tap, finalize, EMPTY } from 'rxjs'; import { catchError, tap, finalize, EMPTY } from 'rxjs';
import { MatStepper } from '@angular/material/stepper'; import { MatStepper } from '@angular/material/stepper';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatDialog } from '@angular/material/dialog';
import { CancelOrderDialog } from '../cancel-order/cancel-order.dialog';
type OrderState = type OrderState =
| { status: 'idle' } | { status: 'idle' }
@@ -32,6 +34,7 @@ export class OrderComponent {
private fb = inject(FormBuilder); private fb = inject(FormBuilder);
private httpService = inject(HttpService); private httpService = inject(HttpService);
private destroyRef = inject(DestroyRef); private destroyRef = inject(DestroyRef);
private dialog = inject(MatDialog);
readonly loadingService = inject(LoadingService); readonly loadingService = inject(LoadingService);
readonly selectedSeatsService = inject(SelectedSeatsService); readonly selectedSeatsService = inject(SelectedSeatsService);
@@ -41,6 +44,7 @@ export class OrderComponent {
existingOrder = input<Bestellung>(); existingOrder = input<Bestellung>();
existingTickets = input<Eintrittskarte[]>(); existingTickets = input<Eintrittskarte[]>();
resumeWithCancel = input<boolean>(true);
stepChanged = output<number>(); stepChanged = output<number>();
@@ -130,6 +134,10 @@ export class OrderComponent {
cvv: ['', [Validators.required, Validators.pattern(/^\d{3,4}$/)]], cvv: ['', [Validators.required, Validators.pattern(/^\d{3,4}$/)]],
}); });
this.confetti = (await import('canvas-confetti')).default; this.confetti = (await import('canvas-confetti')).default;
if (this.resumeWithCancel()) {
this.cancelReservation();
}
} }
get fData() { return this.dataForm.controls; } get fData() { return this.dataForm.controls; }
@@ -161,6 +169,7 @@ export class OrderComponent {
this.makeReservation(); this.makeReservation();
} else if (this.submissionMode() === 'purchase') { } else if (this.submissionMode() === 'purchase') {
stepper.next(); stepper.next();
} }
} }
@@ -228,7 +237,7 @@ export class OrderComponent {
} }
private submitOrder(order: Bestellung, seats: Sitzplatz[], performance: Vorstellung, mode: SubmissionMode) { private submitOrder(order: Bestellung, seats: Sitzplatz[], performance: Vorstellung, mode: SubmissionMode) {
this.loadingService.hide(); this.loadingService.show();
// Tickets anlegen // Tickets anlegen
const tickets = seats.map(seat => { const tickets = seats.map(seat => {
@@ -324,7 +333,23 @@ export class OrderComponent {
}; };
} }
cancelReservation() { cancelReservation() {
const dialogRef = this.dialog.open(CancelOrderDialog, {
width: '500px',
disableClose: false,
enterAnimationDuration: '200ms',
exitAnimationDuration: '100ms'
});
dialogRef.afterClosed().subscribe(confirmed => {
if (confirmed) {
this.performCancellation();
}
});
}
private performCancellation() {
const order = this.existingOrder()!; const order = this.existingOrder()!;
order.cancelled = new Date(); order.cancelled = new Date();
@@ -365,4 +390,26 @@ export class OrderComponent {
getPriceDisplay(price: number): string { getPriceDisplay(price: number): string {
return `${(price / 100).toFixed(2)}`; return `${(price / 100).toFixed(2)}`;
} }
retryPurchase() {
this.orderState.set({ status: 'idle' });
this.makePurchase();
}
retryReservation() {
this.orderState.set({ status: 'idle' });
this.makeReservation();
}
retryConversion() {
this.orderState.set({ status: 'idle' });
const order = this.existingOrder()!;
order.booked = new Date();
this.convertOrder(order, this.existingTickets()!);
}
retryCancellation() {
this.orderState.set({ status: 'idle' });
this.cancelReservation();
}
} }

View File

@@ -8,7 +8,7 @@
<input class="w-full" type="text" matInput [formControl]="formControl" (input)="onInput($event)" placeholder="XXXXXX" maxlength="6" autocomplete="off"> <input class="w-full" type="text" matInput [formControl]="formControl" (input)="onInput($event)" placeholder="XXXXXX" maxlength="6" autocomplete="off">
<mat-error> <mat-error>
@if (formControl.hasError('invalid')) { @if (formControl.hasError('invalid')) {
Ungültiger Bestellcode Ungültiger Reservierungscode
} }
@else if (formControl.hasError('completed')) { @else if (formControl.hasError('completed')) {
Diese Bestellung wurde bereits abgeschlossen Diese Bestellung wurde bereits abgeschlossen
@@ -23,10 +23,10 @@
Diese Bestellung wurde bereits bezahlt Diese Bestellung wurde bereits bezahlt
} }
@else if (formControl.hasError('cancelled')) { @else if (formControl.hasError('cancelled')) {
Diese Bestellung wurde storniert Diese Reservierung wurde storniert
} }
@else if (formControl.hasError('serverError')) { @else if (formControl.hasError('serverError')) {
Fehler beim Laden der Bestellung Fehler beim Laden der Reservierung
} }
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>

View File

@@ -3,7 +3,7 @@ import { FormControl, Validators } from '@angular/forms';
import { LoadingService } from '../loading.service'; import { LoadingService } from '../loading.service';
import { HttpService } from '../http.service'; import { HttpService } from '../http.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { catchError, map, of, take } from 'rxjs'; import { catchError, finalize, map, of, take } from 'rxjs';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
@Component({ @Component({

View File

@@ -3,9 +3,9 @@
warning warning
</mat-icon> </mat-icon>
<h1 class="text-xl font-bold">Kauf fehlgeschlagen!</h1> <h1 class="text-xl font-bold">Kauf fehlgeschlagen!</h1>
<p class="text-center">Leider konnten Ihre Sitzplätze nicht gekauft werden. Dies kann passieren, wenn andere Nutzer zeitgleich versucht haben, dieselben Sitzplätze zu kaufen.</p> <p class="text-center">{{ infoText }}</p>
<button mat-button matButton="filled" class="error-button mt-4 w-80">Erneut versuchen</button> <button mat-button type="button" matButton="filled" class="error-button mt-4 w-80" (click)="retry.emit()">Erneut versuchen</button>
<button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="error-button w-80 mt-1">Zurück zur Programmauswahl</button> <button mat-button type="button" matButton="outlined" color="accent" class="error-button w-80 mt-1" (click)="navigate()">Zurück zur Sitzplatzauswahl</button>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { Component } from '@angular/core'; import { Component, inject, input, output } from '@angular/core';
import { Router } from '@angular/router';
@Component({ @Component({
selector: 'app-purchase-failed', selector: 'app-purchase-failed',
@@ -7,5 +8,22 @@ import { Component } from '@angular/core';
styleUrl: './purchase-failed.component.css', styleUrl: './purchase-failed.component.css',
}) })
export class PurchaseFailedComponent { export class PurchaseFailedComponent {
performanceId = input.required<number>();
moreThanOne = input<boolean>(false);
retry = output<void>();
private router = inject(Router);
infoText!: string;
ngOnInit(): void {
this.infoText = this.moreThanOne()?
'Leider konnten Ihre Sitzplätze nicht gekauft werden. Dies kann passieren, wenn andere Nutzer zeitgleich versucht haben, dieselben Sitzplätze zu kaufen.' :
'Leider konnte Ihr Sitzplatz nicht gekauft werden. Dies kann passieren, wenn andere Nutzer zeitgleich versucht haben, denselben Sitzplatz zu kaufen.';
}
navigate() {
window.location.href = `/checkout/performance/${this.performanceId()}`;
}
} }

View File

@@ -1,11 +1,11 @@
<div class="bg-green-200 rounded-md shadow-sm w-full h-fit p-6 py-8 items-center justify-center flex flex-col space-y-2"> <div class="bg-green-200 rounded-md shadow-sm w-full h-fit p-6 py-8 items-center justify-center flex flex-col space-y-2">
<h1 class="text-xl font-bold">Vielen Dank für Ihren Einkauf!</h1> <h1 class="text-xl font-bold">Vielen Dank für Ihren Einkauf!</h1>
<p class="text-center">Ihre Sitzplätze wurden erfolgreich gebucht.</p> <p class="text-center">{{ infoText }}</p>
<app-ticket-list [tickets]="tickets()" class="w-8/10 my-4"></app-ticket-list> <app-ticket-list [tickets]="tickets()" class="w-8/10 my-4"></app-ticket-list>
<button mat-button disabled="true" matButton="filled" class="success-button w-80 mt-4">Tickets herunterladen</button> <button mat-button disabled="true" matButton="filled" class="success-button w-80 mt-4">{{ buttonText }}</button>
<button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="success-button w-80 mt-1">Zurück zur Programmauswahl</button> <button routerLink="/schedule" type="button" mat-button matButton="outlined" color="accent" class="success-button w-80 mt-1">Zur Programmauswahl</button>
</div> </div>

View File

@@ -9,4 +9,18 @@ import { Component, input } from '@angular/core';
}) })
export class PurchaseSuccessComponent { export class PurchaseSuccessComponent {
tickets = input.required<Eintrittskarte[]>(); tickets = input.required<Eintrittskarte[]>();
moreThanOne = input<boolean>(false);
infoText!: string;
buttonText!: string;
ngOnInit(): void {
this.infoText = this.moreThanOne()?
'Ihre Sitzplätze wurden erfolgreich gebucht.' :
'Ihr Sitzplatz wurden erfolgreich gebucht.';
this.buttonText = this.moreThanOne()?
'Tickets herunterladen' :
'Ticket herunterladen';
}
} }

View File

@@ -3,9 +3,9 @@
warning warning
</mat-icon> </mat-icon>
<h1 class="text-xl font-bold">Reservierung fehlgeschlagen!</h1> <h1 class="text-xl font-bold">Reservierung fehlgeschlagen!</h1>
<p class="text-center">Leider konnten Ihre Sitzplätze nicht reserviert werden. Dies kann passieren, wenn andere Nutzer gleichzeitig versucht haben, dieselben Sitzplätze zu reservieren.</p> <p class="text-center">{{ infoText }}</p>
<button mat-button matButton="filled" class="error-button mt-4 w-80">Erneut versuchen</button> <button mat-button type="button" matButton="filled" class="error-button mt-4 w-80" (click)="retry.emit()">Erneut versuchen</button>
<button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="error-button w-80 mt-1">Zurück zur Programmauswahl</button> <button mat-button type="button" matButton="outlined" color="accent" class="error-button w-80 mt-1" (click)="navigate()">Zurück zur Sitzplatzauswahl</button>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { Component } from '@angular/core'; import { Router } from '@angular/router';
import { Component, inject, input, output } from '@angular/core';
@Component({ @Component({
selector: 'app-reservation-failed', selector: 'app-reservation-failed',
@@ -7,5 +8,22 @@ import { Component } from '@angular/core';
styleUrl: './reservation-failed.component.css', styleUrl: './reservation-failed.component.css',
}) })
export class ReservationFailedComponent { export class ReservationFailedComponent {
performanceId = input.required<number>();
moreThanOne = input<boolean>(false);
retry = output<void>();
router = inject(Router)
infoText!: string;
ngOnInit(): void {
this.infoText = this.moreThanOne()?
'Leider konnten Ihre Sitzplätze nicht reserviert werden. Dies kann passieren, wenn andere Nutzer gleichzeitig versucht haben, dieselben Sitzplätze zu reservieren.' :
'Leider konnte Ihr Sitzplatz nicht reserviert werden. Dies kann passieren, wenn andere Nutzer gleichzeitig versucht haben, denselben Sitzplatz zu reservieren.';
}
navigate() {
window.location.href = `/checkout/performance/${this.performanceId()}`;
}
} }

View File

@@ -1,14 +1,14 @@
<div class="bg-green-200 rounded-md shadow-sm w-full h-fit p-6 py-8 items-center justify-center flex flex-col space-y-2"> <div class="bg-green-200 rounded-md shadow-sm w-full h-fit p-6 py-8 items-center justify-center flex flex-col space-y-2">
<h1 class="text-xl font-bold">Reservierung erfolgreich!</h1> <h1 class="text-xl font-bold">Reservierung erfolgreich!</h1>
<p class="text-center">Ihre Sitzplätze wurden erfolgreich reserviert. Bitte nennen sie den folgenden Code an der Kasse, um Ihre Reservierung in eine Buchung umzuwandeln.</p> <p class="text-center" style="white-space: pre-line;">{{ infoText }}</p>
<div class="bg-white text-5xl font-mono rounded-md shadow-sm w-fit h-fit p-4 py-2 my-4"> <div class="bg-white text-5xl font-mono rounded-md shadow-sm w-fit h-fit p-4 py-2 my-4">
<strong>{{ order().code }}</strong> <strong>{{ order().code }}</strong>
</div> </div>
<button routerLink="/checkout/order/{{ order().code }}" mat-button matButton="filled" color="accent" class="success-button mt-2 w-80">Tickets jetzt online bezahlen</button> <button routerLink="/checkout/order/{{ order().code }}" type="button" mat-button matButton="filled" color="accent" class="success-button mt-2 w-80">{{ buttonText }}</button>
<button routerLink="/schedule" mat-button matButton="outlined" class="success-button mb-4 w-80 mt-1">Zurück zur Programmauswahl</button> <button routerLink="/schedule" type="button" mat-button matButton="outlined" class="success-button mb-4 w-80 mt-1">Zurück zur Programmauswahl</button>
<div class="text-green-500 cursor-pointer w-fit mt-2" (click)="cancelReservation()"> <div [routerLink]="['/checkout/order', order().code]" [queryParams]="{ action: 'cancel' }" class="text-green-500 cursor-pointer w-fit mt-2">
Reservierung stornieren Reservierung stornieren
</div> </div>

View File

@@ -1,5 +1,5 @@
import { Bestellung } from '@infinimotion/model-frontend'; import { Bestellung } from '@infinimotion/model-frontend';
import { Component, input } from '@angular/core'; import { Component, input, OnInit, output } from '@angular/core';
@Component({ @Component({
selector: 'app-reservation-success', selector: 'app-reservation-success',
@@ -7,10 +7,20 @@ import { Component, input } from '@angular/core';
templateUrl: './reservation-success.component.html', templateUrl: './reservation-success.component.html',
styleUrl: './reservation-success.component.css', styleUrl: './reservation-success.component.css',
}) })
export class ReservationSuccessComponent { export class ReservationSuccessComponent implements OnInit {
order = input.required<Bestellung>(); order = input.required<Bestellung>();
moreThanOne = input<boolean>(false);
cancelReservation() { infoText!: string;
// Logic to cancel the reservation buttonText!: string;
ngOnInit(): void {
this.infoText = this.moreThanOne()?
'Ihre Sitzplätze wurden erfolgreich reserviert.\nBitte nennen sie den folgenden Code an der Kasse, um Ihre Reservierung in eine Buchung umzuwandeln' :
'Ihr Sitzplatz wurde erfolgreich reserviert.\nBitte nennen sie den folgenden Code an der Kasse, um Ihre Reservierung in eine Buchung umzuwandeln';
this.buttonText = this.moreThanOne()?
'Tickets jetzt online bezahlen' :
'Ticket jetzt online bezahlen';
} }
} }

View File

@@ -61,6 +61,7 @@ export class SelectedSeatsService {
} }
commit(): void { commit(): void {
this.erroredSignal.set(false);
this.committedSignal.set(true); this.committedSignal.set(true);
} }
@@ -69,6 +70,7 @@ export class SelectedSeatsService {
} }
cancel(): void { cancel(): void {
this.erroredSignal.set(false);
this.cancelledSignal.set(true); this.cancelledSignal.set(true);
} }

View File

@@ -18,6 +18,6 @@
</div> </div>
</div> </div>
<app-order (stepChanged)="setStepTwoOrHigher($event >= 1)" class="mt-10 mr-30 w-3/10" [performance]="performance()" [seatCategories]="seatCategories" [existingOrder]="isResuming? order : undefined" [existingTickets]="isResuming? tickets : undefined"></app-order> <app-order (stepChanged)="setStepTwoOrHigher($event >= 1)" class="mt-10 mr-30 w-3/10" [performance]="performance()" [seatCategories]="seatCategories" [existingOrder]="isResuming? order : undefined" [existingTickets]="isResuming? tickets : undefined" [resumeWithCancel] = "resumeWithCancel"></app-order>
</div> </div>

View File

@@ -31,6 +31,7 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
orderId?: number; orderId?: number;
orderCode?: string | null; orderCode?: string | null;
isResuming = false; isResuming = false;
resumeWithCancel = false;
tickets: Eintrittskarte[] | undefined; tickets: Eintrittskarte[] | undefined;
order: Bestellung | undefined; order: Bestellung | undefined;
blockedSeats: Sitzplatz[] | undefined; blockedSeats: Sitzplatz[] | undefined;
@@ -56,8 +57,14 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
if (this.orderCode) { if (this.orderCode) {
// Checkout fortsetzen // Checkout fortsetzen
this.isResuming = true; this.isResuming = true;
this.loadExistingOrder(this.orderCode); this.loadExistingOrder(this.orderCode);
this.route.queryParams.subscribe(params => {
if (params['action'] === 'cancel') {
this.resumeWithCancel = true;
}
});
} else if (this.showId) { } else if (this.showId) {
// Neuer Checkout // Neuer Checkout
this.isResuming = false; this.isResuming = false;