Add cancellation and conversion error/success flows
Introduces new components for cancellation and conversion error/success states, updates routing and UI to support resuming and cancelling orders, and refactors seat selection and order handling logic to accommodate these new flows. Also improves seat selection feedback and error handling throughout the checkout process.
This commit is contained in:
@@ -27,47 +27,47 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<mat-stepper orientation="horizontal" linear="true" [disableRipple]="true" (selectionChange)="onStepChange($event)" #stepper>
|
||||
<mat-step>
|
||||
<ng-template matStepLabel>Warenkorb</ng-template>
|
||||
<mat-stepper orientation="horizontal" linear="true" [selectedIndex]="isResuming()? 2 : 0" [disableRipple]="true" (selectionChange)="onStepChange($event)" #stepper>
|
||||
<mat-step [editable]="!isResuming()">
|
||||
<ng-template matStepLabel>Warenkorb</ng-template>
|
||||
|
||||
<div class="performance-info-space"></div>
|
||||
<div class="performance-info-space"></div>
|
||||
|
||||
@if (selectedSeatsService.hadConflict()) {
|
||||
<app-selection-conflict-info></app-selection-conflict-info>
|
||||
@if (selectedSeatsService.hadConflict()) {
|
||||
<app-selection-conflict-info></app-selection-conflict-info>
|
||||
}
|
||||
|
||||
<!-- Seat-Selection Overview -->
|
||||
<div class="mb-4 p-2">
|
||||
@for (seatCategory of seatCategories(); track $index) {
|
||||
<div class="h-2"></div>
|
||||
<app-seat-selection [seatCategory]="seatCategory"></app-seat-selection>
|
||||
}
|
||||
@empty {
|
||||
<app-no-seats-in-hall></app-no-seats-in-hall>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Seat-Selection Overview -->
|
||||
<div class="mb-4 p-2">
|
||||
@for (seatCategory of seatCategories(); track $index) {
|
||||
<div class="h-2"></div>
|
||||
<app-seat-selection [seatCategory]="seatCategory"></app-seat-selection>
|
||||
}
|
||||
@empty {
|
||||
<app-no-seats-in-hall></app-no-seats-in-hall>
|
||||
}
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
<!-- Total Price -->
|
||||
<div class="flex justify-between p-2 mt-1 items-baseline">
|
||||
<p class="font-semibold text-lg">
|
||||
Tickets gesamt:
|
||||
</p>
|
||||
<p class="font-semibold text-2xl bg-linear-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent">
|
||||
{{ getPriceDisplay(totalPrice()) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Total Price -->
|
||||
<div class="flex justify-between p-2 mt-1 items-baseline">
|
||||
<p class="font-semibold text-lg">
|
||||
Tickets gesamt:
|
||||
</p>
|
||||
<p class="font-semibold text-2xl bg-linear-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent">
|
||||
{{ getPriceDisplay(totalPrice()) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex space-x-5 mt-10">
|
||||
<button mat-button matButton="outlined" matStepperNext class="w-1/2" [disabled]="totalSeats()==0" (click)="reservationClicked()">Reservieren</button>
|
||||
<button mat-button matButton="filled" matStepperNext class="w-1/2" [disabled]="totalSeats()==0" (click)="purchaseClicked()">Kaufen</button>
|
||||
</div>
|
||||
<!-- Buttons -->
|
||||
<div class="flex space-x-5 mt-10">
|
||||
<button mat-button matButton="outlined" matStepperNext class="w-1/2" [disabled]="totalSeats()==0" (click)="reservationClicked()">Reservieren</button>
|
||||
<button mat-button matButton="filled" matStepperNext class="w-1/2" [disabled]="totalSeats()==0" (click)="purchaseClicked()">Kaufen</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
|
||||
<mat-step [stepControl]="dataForm">
|
||||
<mat-step [editable]="!isResuming" [stepControl]="dataForm">
|
||||
<form [formGroup]="dataForm">
|
||||
<ng-template matStepLabel>Anschrift</ng-template>
|
||||
|
||||
@@ -129,6 +129,18 @@
|
||||
<div class="h-4"></div>
|
||||
<app-purchase-failed></app-purchase-failed>
|
||||
}
|
||||
@else if (isConversionError() && !isSubmitting()) {
|
||||
<div class="h-4"></div>
|
||||
<app-conversion-failed></app-conversion-failed>
|
||||
}
|
||||
@else if (isCancellationSuccess() && !isSubmitting() && performance()) {
|
||||
<div class="h-4"></div>
|
||||
<app-cancellation-success [performanceId]="performance()!.id"></app-cancellation-success>
|
||||
}
|
||||
@else if (isCancellationError() && !isSubmitting()) {
|
||||
<div class="h-4"></div>
|
||||
<app-cancellation-failed></app-cancellation-failed>
|
||||
}
|
||||
@else {
|
||||
|
||||
<!-- Card Number -->
|
||||
@@ -183,9 +195,17 @@
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex space-x-4 mt-8">
|
||||
<button mat-stroked-button color="primary" matStepperPrevious type="button" [disabled]="isSubmitting()" class="w-1/3">
|
||||
Zurück
|
||||
</button>
|
||||
|
||||
@if (isResuming()) {
|
||||
<button mat-stroked-button color="primary" type="button" [disabled]="isSubmitting()" class="w-1/3" (click)="cancelReservation()">
|
||||
Stornieren
|
||||
</button>
|
||||
} @else {
|
||||
<button mat-stroked-button color="primary" matStepperPrevious type="button" [disabled]="isSubmitting()" class="w-1/3">
|
||||
Zurück
|
||||
</button>
|
||||
}
|
||||
|
||||
<button mat-flat-button color="accent" matStepperNext type="submit" [disabled]="isSubmitting()" class="w-2/3">
|
||||
{{ getPriceDisplay(totalPrice()) }} jetzt bezahlen
|
||||
</button>
|
||||
|
||||
@@ -15,7 +15,10 @@ type OrderState =
|
||||
| { status: 'reservation-success'; order: Bestellung }
|
||||
| { status: 'reservation-error'; error: any }
|
||||
| { status: 'purchase-success'; tickets: Eintrittskarte[] }
|
||||
| { status: 'purchase-error'; error: any };
|
||||
| { status: 'purchase-error'; error: any }
|
||||
| { status: 'conversion-error'; error: any }
|
||||
| { status: 'cancellation-success'; }
|
||||
| { status: 'cancellation-error'; error: any };
|
||||
|
||||
type SubmissionMode = 'reservation' | 'purchase';
|
||||
|
||||
@@ -36,6 +39,9 @@ export class OrderComponent {
|
||||
performance = input<Vorstellung>();
|
||||
seatCategories = input.required<Sitzkategorie[]>();
|
||||
|
||||
existingOrder = input<Bestellung>();
|
||||
existingTickets = input<Eintrittskarte[]>();
|
||||
|
||||
stepChanged = output<number>();
|
||||
|
||||
paymentForm!: FormGroup;
|
||||
@@ -52,6 +58,16 @@ export class OrderComponent {
|
||||
|
||||
isSubmitting = computed(() => this.orderState().status === 'submitting');
|
||||
|
||||
isResuming = computed(() => {
|
||||
const order = this.existingOrder();
|
||||
const tickets = this.existingTickets();
|
||||
|
||||
if (!order || !tickets || tickets.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
secondPhaseButtonText = computed(() => {
|
||||
const mode = this.submissionMode();
|
||||
if (!mode) return 'Loading...';
|
||||
@@ -70,6 +86,10 @@ export class OrderComponent {
|
||||
this.orderState().status === 'purchase-success'
|
||||
);
|
||||
|
||||
isCancellationSuccess = computed(() =>
|
||||
this.orderState().status === 'cancellation-success'
|
||||
);
|
||||
|
||||
isReservationError = computed(() =>
|
||||
this.orderState().status === 'reservation-error'
|
||||
);
|
||||
@@ -78,6 +98,14 @@ export class OrderComponent {
|
||||
this.orderState().status === 'purchase-error'
|
||||
);
|
||||
|
||||
isConversionError = computed(() =>
|
||||
this.orderState().status === 'conversion-error'
|
||||
);
|
||||
|
||||
isCancellationError = computed(() =>
|
||||
this.orderState().status === 'cancellation-error'
|
||||
);
|
||||
|
||||
createdOrder = computed(() => {
|
||||
const state = this.orderState();
|
||||
return state.status === 'reservation-success' ? state.order : null;
|
||||
@@ -138,7 +166,6 @@ export class OrderComponent {
|
||||
|
||||
makeReservation() {
|
||||
this.orderState.set({ status: 'submitting' });
|
||||
this.loadingService.show();
|
||||
this.disableForms();
|
||||
|
||||
const order = this.generateNewOrderObject(this.dataForm.value.email, false);
|
||||
@@ -157,15 +184,51 @@ export class OrderComponent {
|
||||
this.loadingService.show();
|
||||
this.disableForms();
|
||||
|
||||
const order = this.generateNewOrderObject(this.dataForm.value.email, true);
|
||||
const seats = this.selectedSeatsService.selectedSeats();
|
||||
const performance = this.performance()!;
|
||||
if (this.isResuming()) {
|
||||
const order = this.existingOrder()!;
|
||||
order.booked = new Date();
|
||||
this.convertOrder(order, this.existingTickets()!);
|
||||
} else {
|
||||
const order = this.generateNewOrderObject(this.dataForm.value.email, true);
|
||||
const seats = this.selectedSeatsService.selectedSeats();
|
||||
const performance = this.performance()!;
|
||||
|
||||
this.submitOrder(order, seats, performance, 'purchase');
|
||||
this.submitOrder(order, seats, performance, 'purchase');
|
||||
}
|
||||
}
|
||||
|
||||
private convertOrder(order: Bestellung, tickets: Eintrittskarte[]) {
|
||||
this.loadingService.show();
|
||||
this.httpService.updateOrder(order).pipe(
|
||||
tap(() => {
|
||||
// Success Handling
|
||||
this.orderState.set({
|
||||
status: 'purchase-success',
|
||||
tickets: tickets
|
||||
});
|
||||
|
||||
submitOrder(order: Bestellung, seats: Sitzplatz[], performance: Vorstellung, mode: SubmissionMode) {
|
||||
this.selectedSeatsService.commit();
|
||||
this.loadingService.hide();
|
||||
this.showConfetti();
|
||||
}),
|
||||
catchError(err => {
|
||||
// Error handling
|
||||
this.selectedSeatsService.error();
|
||||
this.loadingService.showError(err);
|
||||
console.error('Fehler bei der Umwandlung der Bestellung:', err);
|
||||
this.orderState.set({status: 'conversion-error', error: err});
|
||||
|
||||
return EMPTY;
|
||||
}),
|
||||
finalize(() => {
|
||||
this.enableForms();
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
private submitOrder(order: Bestellung, seats: Sitzplatz[], performance: Vorstellung, mode: SubmissionMode) {
|
||||
this.loadingService.hide();
|
||||
|
||||
// Tickets anlegen
|
||||
const tickets = seats.map(seat => {
|
||||
@@ -194,8 +257,9 @@ export class OrderComponent {
|
||||
}),
|
||||
catchError(err => {
|
||||
// Error handling
|
||||
console.error('Fehler beim Anlegen der Bestellung/Tickets:', err);
|
||||
this.selectedSeatsService.error();
|
||||
this.loadingService.showError(err);
|
||||
console.error('Fehler beim Anlegen der Bestellung/Tickets:', err);
|
||||
|
||||
if (mode === 'reservation') {
|
||||
this.orderState.set({ status: 'reservation-error', error: err });
|
||||
@@ -260,6 +324,34 @@ export class OrderComponent {
|
||||
};
|
||||
}
|
||||
|
||||
cancelReservation() {
|
||||
const order = this.existingOrder()!;
|
||||
order.cancelled = new Date();
|
||||
|
||||
this.loadingService.show();
|
||||
this.httpService.updateOrder(order).pipe(
|
||||
tap(() => {
|
||||
// Success Handling
|
||||
this.orderState.set({
|
||||
status: 'cancellation-success'
|
||||
});
|
||||
|
||||
this.selectedSeatsService.cancel();
|
||||
this.loadingService.hide();
|
||||
}),
|
||||
catchError(err => {
|
||||
// Error handling
|
||||
this.selectedSeatsService.error();
|
||||
this.loadingService.showError(err);
|
||||
console.error('Fehler bei der Bezahlung der Bestellung:', err);
|
||||
this.orderState.set({status: 'cancellation-error', error: err});
|
||||
|
||||
return EMPTY;
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
private disableForms(): void {
|
||||
this.dataForm.disable();
|
||||
this.paymentForm.disable();
|
||||
|
||||
Reference in New Issue
Block a user