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:
@@ -65,6 +65,9 @@ import { TicketSmallComponent } from './ticket-small/ticket-small.component';
|
||||
import { TicketListComponent } from './ticket-list/ticket-list.component';
|
||||
import { ZoomWarningComponent } from './zoom-warning/zoom-warning.component';
|
||||
import { SelectionConflictInfoComponent } from './selection-conflict-info/selection-conflict-info.component';
|
||||
import { CancellationSuccessComponent } from './cancellation-success/cancellation-success.component';
|
||||
import { CancellationFailedComponent } from './cancellation-failed/cancellation-failed.component';
|
||||
import { ConversionFailedComponent } from './conversion-failed/conversion-failed.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
@@ -109,6 +112,9 @@ import { SelectionConflictInfoComponent } from './selection-conflict-info/select
|
||||
TicketListComponent,
|
||||
ZoomWarningComponent,
|
||||
SelectionConflictInfoComponent,
|
||||
CancellationSuccessComponent,
|
||||
CancellationFailedComponent,
|
||||
ConversionFailedComponent,
|
||||
],
|
||||
imports: [
|
||||
AppRoutingModule,
|
||||
|
||||
@@ -27,7 +27,8 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
data: { roles: ['admin'] }, // Array von erlaubten Rollen. Derzeit gäbe es 'admin' und 'employee'
|
||||
},
|
||||
{ path: 'performance/:performanceId/checkout', component: TheaterOverlayComponent},
|
||||
{ path: 'checkout/performance/:performanceId', component: TheaterOverlayComponent},
|
||||
{ path: 'checkout/order/:orderId', component: TheaterOverlayComponent},
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="bg-red-100 text-red-500 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">
|
||||
warning
|
||||
</mat-icon>
|
||||
<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>
|
||||
|
||||
<button mat-button matButton="filled" class="error-button mt-4 w-80">Erneut versuchen</button>
|
||||
<button routerLink="/order" mat-button matButton="outlined" color="accent" class="error-button w-80 mt-1">Zurück zur Codeeingabe</button>
|
||||
</div>
|
||||
|
||||
11
src/app/cancellation-failed/cancellation-failed.component.ts
Normal file
11
src/app/cancellation-failed/cancellation-failed.component.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-cancellation-failed',
|
||||
standalone: false,
|
||||
templateUrl: './cancellation-failed.component.html',
|
||||
styleUrl: './cancellation-failed.component.css',
|
||||
})
|
||||
export class CancellationFailedComponent {
|
||||
|
||||
}
|
||||
@@ -0,0 +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">
|
||||
<mat-icon class="material-symbols-outlined mb-5" style="font-size: 50px; width: 50px; height: 50px">
|
||||
task_alt
|
||||
</mat-icon>
|
||||
<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>
|
||||
|
||||
<button routerLink="/schedule" mat-button matButton="filled" color="accent" class="success-button 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>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Component, input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-cancellation-success',
|
||||
standalone: false,
|
||||
templateUrl: './cancellation-success.component.html',
|
||||
styleUrl: './cancellation-success.component.css',
|
||||
})
|
||||
export class CancellationSuccessComponent {
|
||||
performanceId = input.required<number>();
|
||||
}
|
||||
11
src/app/conversion-failed/conversion-failed.component.html
Normal file
11
src/app/conversion-failed/conversion-failed.component.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<div class="bg-red-100 text-red-500 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">
|
||||
warning
|
||||
</mat-icon>
|
||||
<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>
|
||||
|
||||
<button mat-button matButton="filled" class="error-button mt-4 w-80">Erneut versuchen</button>
|
||||
<button routerLink="/order" mat-button matButton="outlined" color="accent" class="error-button w-80 mt-1">Zurück zur Codeeingabe</button>
|
||||
</div>
|
||||
|
||||
11
src/app/conversion-failed/conversion-failed.component.ts
Normal file
11
src/app/conversion-failed/conversion-failed.component.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-conversion-failed',
|
||||
standalone: false,
|
||||
templateUrl: './conversion-failed.component.html',
|
||||
styleUrl: './conversion-failed.component.css',
|
||||
})
|
||||
export class ConversionFailedComponent {
|
||||
|
||||
}
|
||||
@@ -33,9 +33,9 @@ export class HttpService {
|
||||
return this.http.post<Bestellung>(`${this.baseUrl}bestellung`, order);
|
||||
}
|
||||
|
||||
/* PUT /api/bestellung/{id} */
|
||||
updateOrder(id: number, order: Partial<Bestellung>): Observable<Bestellung> {
|
||||
return this.http.put<Bestellung>(`${this.baseUrl}bestellung/${id}`, order);
|
||||
/* PUT /api/bestellung */
|
||||
updateOrder(order: Partial<Bestellung>): Observable<Bestellung> {
|
||||
return this.http.put<Bestellung>(`${this.baseUrl}bestellung`, order);
|
||||
}
|
||||
|
||||
/* DELETE /api/bestellung/{id} */
|
||||
|
||||
@@ -15,7 +15,7 @@ export class MoviePerformanceComponent implements OnInit {
|
||||
route: string = '';
|
||||
|
||||
ngOnInit() {
|
||||
this.route = `../performance/${this.id()}/checkout`;
|
||||
this.route = `../checkout/performance/${this.id()}`;
|
||||
}
|
||||
|
||||
startTime = computed(() =>
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<mat-stepper orientation="horizontal" linear="true" [disableRipple]="true" (selectionChange)="onStepChange($event)" #stepper>
|
||||
<mat-step>
|
||||
<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>
|
||||
@@ -67,7 +67,7 @@
|
||||
</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">
|
||||
|
||||
@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();
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<strong>{{ order().code }}</strong>
|
||||
</div>
|
||||
|
||||
<button [disabled]="true" mat-button matButton="filled" color="accent" class="success-button mt-2 w-80">Tickets jetzt online bezahlen</button>
|
||||
<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="/schedule" 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()">
|
||||
Reservierung stornieren
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<button (click)="updateSelectedSeats(this.seat())" [disabled]="state() == TheaterSeatState.BOOKED || state() == TheaterSeatState.RESERVED || !seatService.getSeatIsSelectable()" class="mx-0.5">
|
||||
<button (click)="updateSelectedSeats(this.seat())" [disabled]="!isClickable()" class="mx-0.5">
|
||||
<mat-icon
|
||||
[class]="isHoverable()? 'hover:opacity-50' : ''"
|
||||
[ngStyle]="{color : getSeatStateColor() }"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Component, inject, input} from '@angular/core';
|
||||
import {TheaterSeatState} from '../model/theater-seat-state.model';
|
||||
import {Sitzplatz} from '@infinimotion/model-frontend';
|
||||
import {SelectedSeatsService} from '../selected-seats.service';
|
||||
import { Component, computed, inject, input } from '@angular/core';
|
||||
import { TheaterSeatState } from '../model/theater-seat-state.model';
|
||||
import { Sitzplatz } from '@infinimotion/model-frontend';
|
||||
import { SelectedSeatsService } from '../selected-seats.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-seat',
|
||||
@@ -13,14 +13,17 @@ export class SeatComponent{
|
||||
seat = input.required<Sitzplatz>();
|
||||
state = input.required<TheaterSeatState>()
|
||||
|
||||
selected: boolean = false;
|
||||
selected = computed(() => this.seatService.isSeatSelected(this.seat().id));
|
||||
|
||||
protected seatService = inject(SelectedSeatsService)
|
||||
protected readonly TheaterSeatState = TheaterSeatState;
|
||||
|
||||
getSeatStateColor(): string {
|
||||
if (this.isSelectedAndAvaliable()) {
|
||||
return this.seatService.committed()? '#00c951' : '#6366f1';
|
||||
if (this.selected()) {
|
||||
if(this.seatService.errored()) return '#f01d05';
|
||||
if(this.seatService.committed()) return '#00c951';
|
||||
if(this.seatService.cancelled()) return '#c0c0c0';
|
||||
return '#6366f1';
|
||||
}
|
||||
|
||||
if (!this.seatService.getSeatIsSelectable()) {
|
||||
@@ -50,16 +53,18 @@ export class SeatComponent{
|
||||
}
|
||||
|
||||
updateSelectedSeats(selectedSeat: Sitzplatz) : void {
|
||||
if(!this.selected){
|
||||
if (!this.selected()){
|
||||
this.seatService.pushSelectedSeat(selectedSeat);
|
||||
} else {
|
||||
this.seatService.removeSelectedSeat(selectedSeat);
|
||||
}
|
||||
|
||||
this.selected = !this.selected;
|
||||
}
|
||||
|
||||
isSelectedAndAvaliable(): boolean {
|
||||
return this.selected && this.state() == TheaterSeatState.AVAILABLE;
|
||||
isClickable(): boolean {
|
||||
if (this.state() === TheaterSeatState.BOOKED ||
|
||||
this.state() === TheaterSeatState.RESERVED) {
|
||||
return false;
|
||||
}
|
||||
return this.seatService.getSeatIsSelectable();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,16 @@ export class SelectedSeatsService {
|
||||
private selectedSeatsSignal = signal<Sitzplatz[]>([]);
|
||||
private seatIsSelectableSignal = signal(true);
|
||||
private committedSignal = signal(false);
|
||||
private erroredSignal = signal(false);
|
||||
private cancelledSignal = signal(false);
|
||||
private debugSignal = signal(false);
|
||||
private hadConflictSignal = signal(false);
|
||||
|
||||
readonly selectedSeats = this.selectedSeatsSignal.asReadonly();
|
||||
readonly seatIsSelectable = this.seatIsSelectableSignal.asReadonly();
|
||||
readonly committed = this.committedSignal.asReadonly();
|
||||
readonly errored = this.erroredSignal.asReadonly();
|
||||
readonly cancelled = this.cancelledSignal.asReadonly();
|
||||
readonly debug = this.debugSignal.asReadonly();
|
||||
readonly hadConflict = this.hadConflictSignal.asReadonly();
|
||||
|
||||
@@ -38,6 +42,8 @@ export class SelectedSeatsService {
|
||||
clearSelection(): void {
|
||||
this.selectedSeatsSignal.set([]);
|
||||
this.committedSignal.set(false);
|
||||
this.cancelledSignal.set(false);
|
||||
this.erroredSignal.set(false);
|
||||
this.hadConflictSignal.set(false);
|
||||
}
|
||||
|
||||
@@ -49,6 +55,8 @@ export class SelectedSeatsService {
|
||||
this.seatIsSelectableSignal.set(selectable);
|
||||
if (selectable) {
|
||||
this.committedSignal.set(false);
|
||||
this.cancelledSignal.set(false);
|
||||
this.erroredSignal.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +64,14 @@ export class SelectedSeatsService {
|
||||
this.committedSignal.set(true);
|
||||
}
|
||||
|
||||
error(): void {
|
||||
this.erroredSignal.set(true);
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.cancelledSignal.set(true);
|
||||
}
|
||||
|
||||
toggleDebug(): void {
|
||||
this.debugSignal.update(debug => !debug);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<div class="w-7/10 p-10 h-fit">
|
||||
<div>
|
||||
@if (!performance && (loading.loading$ | async)){
|
||||
@if (!performance() && (loading.loading$ | async)){
|
||||
<div class="w-full h-full flex items-center justify-center mt-70">
|
||||
<mat-progress-spinner
|
||||
mode="indeterminate"
|
||||
@@ -18,6 +18,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-order (stepChanged)="setStepTwoOrHigher($event >= 1)" class="mt-10 mr-30 w-3/10" [performance]="performance" [seatCategories]="seatCategories"></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"></app-order>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component, DestroyRef, inject, OnDestroy, OnInit, signal } from '@angular/core';
|
||||
import { HttpService } from '../http.service';
|
||||
import { LoadingService } from '../loading.service';
|
||||
import { catchError, filter, forkJoin, fromEvent, interval, merge, of, startWith, switchMap, tap } from 'rxjs';
|
||||
import { Sitzkategorie, Sitzplatz, Vorstellung } from '@infinimotion/model-frontend';
|
||||
import { catchError, filter, finalize, forkJoin, from, fromEvent, interval, merge, of, startWith, switchMap, tap } from 'rxjs';
|
||||
import { Bestellung, Eintrittskarte, Sitzkategorie, Sitzplatz, Vorstellung } from '@infinimotion/model-frontend';
|
||||
import { TheaterSeatState } from '../model/theater-seat-state.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { SelectedSeatsService } from '../selected-seats.service';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { MatSnackBar, MatSnackBarRef, TextOnlySnackBar } from '@angular/material/snack-bar';
|
||||
@@ -21,19 +21,25 @@ const INACTIVITY_TIMEOUT_MS = 2 * 60 * 1000;
|
||||
export class TheaterOverlayComponent implements OnInit, OnDestroy {
|
||||
private http = inject(HttpService);
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
private selectedSeatService = inject(SelectedSeatsService);
|
||||
|
||||
readonly loading = inject(LoadingService);
|
||||
|
||||
showId!: number;
|
||||
orderId?: string;
|
||||
performance: Vorstellung | undefined;
|
||||
showId?: number | null;
|
||||
orderId?: number;
|
||||
orderCode?: string | null;
|
||||
isResuming = false;
|
||||
tickets: Eintrittskarte[] | undefined;
|
||||
order: Bestellung | undefined;
|
||||
blockedSeats: Sitzplatz[] | undefined;
|
||||
seatsPerRow = signal<{ seat: Sitzplatz | null, state: TheaterSeatState | null }[][]>([]);
|
||||
seatCategories: Sitzkategorie[] = [];
|
||||
snackBarRef: MatSnackBarRef<TextOnlySnackBar> | undefined
|
||||
|
||||
performance = signal<Vorstellung | undefined>(undefined);
|
||||
seatsPerRow = signal<{ seat: Sitzplatz | null, state: TheaterSeatState | null }[][]>([]);
|
||||
|
||||
private isPollingEnabled = signal(true);
|
||||
private isInitialLoad = signal(true);
|
||||
private lastActivityTimestamp = signal(Date.now());
|
||||
@@ -45,13 +51,29 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit() {
|
||||
this.showId = Number(this.route.snapshot.paramMap.get('performanceId')!);
|
||||
this.orderId = this.route.snapshot.queryParams['paramName'];
|
||||
this.orderCode = this.route.snapshot.paramMap.get('orderId');
|
||||
|
||||
if (this.orderCode) {
|
||||
// Checkout fortsetzen
|
||||
this.isResuming = true;
|
||||
this.loadExistingOrder(this.orderCode);
|
||||
|
||||
} else if (this.showId) {
|
||||
// Neuer Checkout
|
||||
this.isResuming = false;
|
||||
|
||||
this.selectedSeatService.clearSelection();
|
||||
this.selectedSeatService.setSeatSelectable(true);
|
||||
|
||||
this.setupActivityTracking();
|
||||
this.startAutoRefresh();
|
||||
this.startInactivityCheck();
|
||||
|
||||
} else {
|
||||
// Fallback
|
||||
console.error('Ungültige Checkout-Route');
|
||||
this.router.navigate(['/performances']);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -156,18 +178,20 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private loadPerformanceAndSeats() {
|
||||
if (this.isInitialLoad()) {
|
||||
const isInitial = this.isInitialLoad();
|
||||
|
||||
if (isInitial) {
|
||||
this.loading.show();
|
||||
}
|
||||
|
||||
this.isRequestInProgress.set(true);
|
||||
|
||||
return forkJoin({
|
||||
performance: this.http.getPerformaceById(this.showId),
|
||||
seats: this.http.getSeatsByShowId(this.showId)
|
||||
performance: this.http.getPerformaceById(this.showId!),
|
||||
seats: this.http.getSeatsByShowId(this.showId!)
|
||||
}).pipe(
|
||||
tap(({ performance, seats }) => {
|
||||
this.performance = performance;
|
||||
this.performance.set(performance);
|
||||
|
||||
if (this.blockedSeats && !this.equalSeats(this.blockedSeats, seats.reserved)) {
|
||||
console.info('[TheaterOverlay] External booking detected. Checking for conflicts.');
|
||||
@@ -181,36 +205,78 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.selectedSeatService.selectedSeats
|
||||
}
|
||||
this.blockedSeats = seats.reserved;
|
||||
|
||||
this.blockedSeats = seats.reserved;
|
||||
this.seatsPerRow.set(this.converter(seats));
|
||||
|
||||
if (this.isInitialLoad()) {
|
||||
if (isInitial) {
|
||||
this.loading.hide();
|
||||
this.isInitialLoad.set(false);
|
||||
}
|
||||
|
||||
this.isRequestInProgress.set(false);
|
||||
}),
|
||||
catchError(err => {
|
||||
if (this.isInitialLoad()) {
|
||||
if (isInitial) {
|
||||
this.loading.showError(err);
|
||||
} else {
|
||||
console.warn('Fehler beim Aktualisieren der Sitze:', err);
|
||||
}
|
||||
|
||||
if (this.isInitialLoad()) {
|
||||
this.loading.hide();
|
||||
return of({ performance: null, seats: { seats: [], reserved: [], booked: [] } });
|
||||
}),
|
||||
finalize(() => {
|
||||
if (isInitial) {
|
||||
this.isInitialLoad.set(false);
|
||||
}
|
||||
|
||||
this.isRequestInProgress.set(false);
|
||||
|
||||
return of({ performance: null, seats: { seats: [], reserved: [], booked: [] } });
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private loadExistingOrder(orderCode: string) {
|
||||
this.loading.show();
|
||||
const ticketFilter = [`eq;order.code;string;${orderCode}`];
|
||||
|
||||
this.http.getTicketsByFilter(ticketFilter).pipe(
|
||||
switchMap(tickets => {
|
||||
this.tickets = tickets;
|
||||
|
||||
if (!tickets.length) {
|
||||
return from(this.router.navigate(
|
||||
['/checkout/order'],
|
||||
{ queryParams: { error: 'invalid' } }
|
||||
));
|
||||
}
|
||||
|
||||
this.order = tickets[0].order;
|
||||
|
||||
if (this.order.booked || this.order.cancelled) {
|
||||
return from(this.router.navigate(
|
||||
['/checkout/order'],
|
||||
{ queryParams: { error: 'completed' } }
|
||||
));
|
||||
}
|
||||
|
||||
this.showId = this.tickets[0].show.id;
|
||||
|
||||
this.selectedSeatService.clearSelection();
|
||||
this.tickets.forEach(t => this.selectedSeatService.pushSelectedSeat(t.seat));
|
||||
this.selectedSeatService.setSeatSelectable(false);
|
||||
|
||||
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' } }
|
||||
));
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
|
||||
private equalSeats(a: Sitzplatz[], b: Sitzplatz[]): boolean {
|
||||
if (a === b) return true;
|
||||
if (a == null || b == null) return false;
|
||||
|
||||
Reference in New Issue
Block a user