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

@@ -1,3 +1,7 @@
:host {
min-width: 500px;
}
mat-stepper {
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-step [editable]="!isResuming()">
<mat-step [editable]="!isResuming() && !(isReservationSuccess() && !isSubmitting()) && !(isPurchaseSuccess() && !isSubmitting())">
<ng-template matStepLabel>Warenkorb</ng-template>
<div class="performance-info-space"></div>
@@ -67,7 +67,7 @@
</div>
</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">
<ng-template matStepLabel>Anschrift</ng-template>
@@ -75,11 +75,11 @@
@if (isReservationSuccess() && !isSubmitting()) {
<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>
<app-reservation-failed></app-reservation-failed>
<app-reservation-failed (retry)="retryReservation()" [performanceId]="performance()!.id" [moreThanOne]="totalSeats() > 1"></app-reservation-failed>
}
@else {
@@ -123,23 +123,23 @@
@if (isPurchaseSuccess() && !isSubmitting()) {
<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>
<app-purchase-failed></app-purchase-failed>
<app-purchase-failed (retry)="retryPurchase()" [performanceId]="performance()!.id" [moreThanOne]="totalSeats() > 1"></app-purchase-failed>
}
@else if (isConversionError() && !isSubmitting()) {
<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()) {
<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()) {
<div class="h-4"></div>
<app-cancellation-failed></app-cancellation-failed>
<app-cancellation-failed (retry)="retryCancellation()" [moreThanOne]="totalSeats() > 1"></app-cancellation-failed>
}
@else {

View File

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