From b619d744c1d9709674f127c897baa22994cf0c8b Mon Sep 17 00:00:00 2001 From: Piet Ostendorp Date: Wed, 19 Nov 2025 10:59:34 +0100 Subject: [PATCH 1/7] Add inactivity timeout and polling control to seat overlay Introduces user inactivity tracking and timeout for seat polling in TheaterOverlayComponent. Polling is paused after 2 minutes of inactivity or when the user advances past step 1, with a snackbar notification and resume option. Refactors polling logic, adds step change event to OrderComponent, and updates custom theme for snackbar styling. --- src/app/order/order.component.ts | 7 +- .../theater-overlay.component.html | 2 +- .../theater-overlay.component.ts | 156 ++++++++++++++++-- src/custom-theme.scss | 18 +- 4 files changed, 161 insertions(+), 22 deletions(-) diff --git a/src/app/order/order.component.ts b/src/app/order/order.component.ts index 84aa2d0..04717de 100644 --- a/src/app/order/order.component.ts +++ b/src/app/order/order.component.ts @@ -1,7 +1,7 @@ import { SelectedSeatsService } from './../selected-seats.service'; import { LoadingService } from './../loading.service'; import { Bestellung, Eintrittskarte, Sitzkategorie, Sitzplatz, Vorstellung } from '@infinimotion/model-frontend'; -import { Component, computed, DestroyRef, inject, input, signal } from '@angular/core'; +import { Component, computed, DestroyRef, EventEmitter, inject, input, output, Output, signal, ViewChild } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { StepperSelectionEvent } from '@angular/cdk/stepper'; import { HttpService } from '../http.service'; @@ -29,12 +29,15 @@ export class OrderComponent { private fb = inject(FormBuilder); private httpService = inject(HttpService); private destroyRef = inject(DestroyRef); + readonly loadingService = inject(LoadingService); readonly selectedSeatsService = inject(SelectedSeatsService); performance = input(); seatCategories = input.required(); + stepChanged = output(); + paymentForm!: FormGroup; dataForm!: FormGroup; @@ -107,6 +110,8 @@ export class OrderComponent { onStepChange(event: StepperSelectionEvent) { this.submitted.set(false); this.selectedSeatsService.setSeatSelectable(event.selectedIndex === 0); + + this.stepChanged.emit(event.selectedIndex); } reservationClicked() { diff --git a/src/app/theater-overlay/theater-overlay.component.html b/src/app/theater-overlay/theater-overlay.component.html index 78bd5b8..d255376 100644 --- a/src/app/theater-overlay/theater-overlay.component.html +++ b/src/app/theater-overlay/theater-overlay.component.html @@ -18,6 +18,6 @@ - + diff --git a/src/app/theater-overlay/theater-overlay.component.ts b/src/app/theater-overlay/theater-overlay.component.ts index f588044..961fb45 100644 --- a/src/app/theater-overlay/theater-overlay.component.ts +++ b/src/app/theater-overlay/theater-overlay.component.ts @@ -1,14 +1,17 @@ -import {Component, DestroyRef, inject, OnDestroy, OnInit, signal} from '@angular/core'; -import {HttpService} from '../http.service'; -import {LoadingService} from '../loading.service'; -import {catchError, filter, forkJoin, interval, of, startWith, switchMap, tap} from 'rxjs'; -import {Sitzkategorie, Sitzplatz, Vorstellung} from '@infinimotion/model-frontend'; -import {TheaterSeatState} from '../model/theater-seat-state.model'; -import {ActivatedRoute} from '@angular/router'; -import {SelectedSeatsService} from '../selected-seats.service'; +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 { TheaterSeatState } from '../model/theater-seat-state.model'; +import { ActivatedRoute } from '@angular/router'; +import { SelectedSeatsService } from '../selected-seats.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatDialog } from '@angular/material/dialog'; +import { MatSnackBar, MatSnackBarRef, TextOnlySnackBar } from '@angular/material/snack-bar'; -const POLLING_INTERVAL_MS = 5000; +const POLLING_INTERVAL_MS = 5 * 1000; +const INACTIVITY_TIMEOUT_MS = 2 * 60 * 1000; @Component({ selector: 'app-theater-overlay', @@ -21,6 +24,7 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy { private route = inject(ActivatedRoute); private destroyRef = inject(DestroyRef); private selectedSeatService = inject(SelectedSeatsService); + private dialog = inject(MatDialog); readonly loading = inject(LoadingService); @@ -29,9 +33,16 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy { seatsPerRow = signal<{ seat: Sitzplatz | null, state: TheaterSeatState | null }[][]>([]); performance: Vorstellung | undefined; seatCategories: Sitzkategorie[] = []; + snackBarRef: MatSnackBarRef | undefined private isPollingEnabled = signal(true); private isInitialLoad = signal(true); + private lastActivityTimestamp = signal(Date.now()); + private inactivityTimeoutReached = signal(false); + private isRequestInProgress = signal(false); + private isStepTwoOrHigher = signal(false); + + constructor(private snackBar: MatSnackBar) {} ngOnInit() { this.showId = Number(this.route.snapshot.paramMap.get('performanceId')!); @@ -39,18 +50,107 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy { this.selectedSeatService.clearSelection(); this.selectedSeatService.setSeatSelectable(true); + this.setupActivityTracking(); this.startAutoRefresh(); + this.startInactivityCheck(); } ngOnDestroy() { this.isPollingEnabled.set(false); + console.info('[TheaterOverlay] Stopped auto-refresh polling'); + + if(this.snackBarRef) { + this.snackBar.dismiss(); + } + } + + private setupActivityTracking() { + console.info('[TheaterOverlay] Setting up activity tracking'); + const events$ = merge( + fromEvent(document, 'mousemove'), + fromEvent(document, 'mousedown'), + fromEvent(document, 'keypress'), + fromEvent(document, 'scroll'), + fromEvent(document, 'touchstart') + ); + + events$.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.lastActivityTimestamp.set(Date.now()); + }); + + fromEvent(document, 'visibilitychange').pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + if (!this.isStepTwoOrHigher()) { + if (document.hidden) { + console.info('[TheaterOverlay] Tab hidden - pausing polling'); + this.pausePolling(); + } else { + // Nur in Schritt 1 reaktivieren + if (!this.inactivityTimeoutReached()) { + console.info('[TheaterOverlay] Tab visible - resumed polling'); + this.isPollingEnabled.set(true); + this.refreshSeats(); + } + } + } + }); + } + + private startInactivityCheck() { + interval(10000).pipe( + filter(() => this.isPollingEnabled()), + filter(() => !this.isStepTwoOrHigher()), // Kein Timeout ab Schritt 2 + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + const inactiveDuration = Date.now() - this.lastActivityTimestamp(); + + if (inactiveDuration >= INACTIVITY_TIMEOUT_MS && !this.inactivityTimeoutReached()) { + console.info('[TheaterOverlay] Inactivity timeout reached'); + this.handleInactivityTimeout(); + } + }); + } + + private handleInactivityTimeout() { + this.inactivityTimeoutReached.set(true); + this.pausePolling(); + this.showInactivitySnackBar(); + } + + private showInactivitySnackBar() { + + this.snackBarRef = this.snackBar.open( + 'Sitzplatzaktuallisierung wegen Inaktivität gestoppt.', + 'Fortsetzen', + { + duration: 0, + panelClass: ['timeout-snackbar'], + horizontalPosition: 'center', + verticalPosition: 'bottom' + } + ); + + this.snackBarRef.afterDismissed().subscribe(() => { + this.lastActivityTimestamp.set(Date.now()); + this.inactivityTimeoutReached.set(false); + this.resumePolling(); + this.refreshSeats(); + }); } private startAutoRefresh() { + console.info('[TheaterOverlay] Starting auto-refresh polling'); interval(POLLING_INTERVAL_MS).pipe( - startWith(0), + startWith(POLLING_INTERVAL_MS), filter(() => this.isPollingEnabled()), filter(() => !this.selectedSeatService.committed()), + filter(() => !document.hidden), // Nicht pollen, wenn Tab nicht sichtbar + filter(() => !this.inactivityTimeoutReached()), // Nicht pollen nach Timeout + filter(() => !this.isRequestInProgress()), // Nicht pollen, wenn Request läuft + filter(() => !this.isStepTwoOrHigher()), // Nicht pollen ab Schritt 2 switchMap(() => this.loadPerformanceAndSeats()), takeUntilDestroyed(this.destroyRef) ).subscribe(); @@ -61,6 +161,8 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy { this.loading.show(); } + this.isRequestInProgress.set(true); + return forkJoin({ performance: this.http.getPerformaceById(this.showId), seats: this.http.getSeatsByShowId(this.showId) @@ -73,6 +175,8 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy { this.loading.hide(); this.isInitialLoad.set(false); } + + this.isRequestInProgress.set(false); }), catchError(err => { if (this.isInitialLoad()) { @@ -86,12 +190,14 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy { this.isInitialLoad.set(false); } + this.isRequestInProgress.set(false); + return of({ performance: null, seats: { seats: [], reserved: [], booked: [] } }); }) ); } - converter(resp: { seats: Sitzplatz[], reserved: Sitzplatz[], booked: Sitzplatz[] }): { + private converter(resp: { seats: Sitzplatz[], reserved: Sitzplatz[], booked: Sitzplatz[] }): { seat: Sitzplatz | null, state: TheaterSeatState | null }[][] { @@ -163,16 +269,36 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy { return filledRows; } - refreshSeats(): void { + private refreshSeats(): void { + console.info('[TheaterOverlay] Manual refresh triggered'); this.loadPerformanceAndSeats().subscribe(); } - pausePolling(): void { //TODO: Ab Stepper Schritt 2 Polling pausieren + private pausePolling(): void { + console.info('[TheaterOverlay] Polling paused'); this.isPollingEnabled.set(false); } - resumePolling(): void { - this.isPollingEnabled.set(true); + private resumePolling(): void { + console.info('[TheaterOverlay] Resume polling attempted'); + if (!this.inactivityTimeoutReached() && !this.isStepTwoOrHigher()) { + this.isPollingEnabled.set(true); + } + } + + setStepTwoOrHigher(isStep2OrHigher: boolean): void { + this.isStepTwoOrHigher.set(isStep2OrHigher); + + if (isStep2OrHigher) { + console.info('[TheaterOverlay] Moving to step 2+ - disabling polling'); + this.pausePolling(); + } else { + console.info('[TheaterOverlay] Back to step 1 - reactivating polling'); + this.lastActivityTimestamp.set(Date.now()); + this.inactivityTimeoutReached.set(false); + this.isPollingEnabled.set(true); + this.refreshSeats(); + } } } diff --git a/src/custom-theme.scss b/src/custom-theme.scss index 6eddae9..9862129 100644 --- a/src/custom-theme.scss +++ b/src/custom-theme.scss @@ -42,17 +42,25 @@ html.dark { background: linear-gradient(to right, #6366f1, #db2777) !important; } -@include mat.snack-bar-overrides(( - container-color: red, -)); + +.error-snackbar { + @include mat.snack-bar-overrides(( + container-color: red, + )); +} +.timeout-snackbar { + @include mat.snack-bar-overrides(( + container-color: #ff9900 + )); +} -.error-snackbar .mat-mdc-snack-bar-label { +.mat-mdc-snack-bar-label { color: white !important; } -.error-snackbar .mat-mdc-button { +.mat-mdc-button { color: white !important; } From 711ad39dd6f801a20f4ee46b097bf957e421a2e8 Mon Sep 17 00:00:00 2001 From: Piet Ostendorp Date: Wed, 19 Nov 2025 19:53:13 +0100 Subject: [PATCH 2/7] Add selection conflict info component and handle seat conflicts Introduces SelectionConflictInfoComponent to display seat selection conflicts when a seat is removed due to external booking. Removes ShoppingCartComponent and its references. Updates SelectedSeatsService to track conflict state and modifies TheaterOverlayComponent to detect and handle seat conflicts, updating the UI accordingly. --- src/app/app-module.ts | 4 +- src/app/order/order.component.html | 4 ++ src/app/order/order.component.ts | 4 +- src/app/selected-seats.service.ts | 10 ++++- .../selection-conflict-info.component.css} | 0 .../selection-conflict-info.component.html | 16 +++++++ .../selection-conflict-info.component.ts | 11 +++++ .../shopping-cart.component.html | 1 - .../shopping-cart/shopping-cart.component.ts | 11 ----- .../theater-overlay.component.ts | 42 +++++++++++++++++-- 10 files changed, 83 insertions(+), 20 deletions(-) rename src/app/{shopping-cart/shopping-cart.component.css => selection-conflict-info/selection-conflict-info.component.css} (100%) create mode 100644 src/app/selection-conflict-info/selection-conflict-info.component.html create mode 100644 src/app/selection-conflict-info/selection-conflict-info.component.ts delete mode 100644 src/app/shopping-cart/shopping-cart.component.html delete mode 100644 src/app/shopping-cart/shopping-cart.component.ts diff --git a/src/app/app-module.ts b/src/app/app-module.ts index 2794819..a8554ec 100644 --- a/src/app/app-module.ts +++ b/src/app/app-module.ts @@ -54,7 +54,6 @@ import { MovieImportNoSearchResultComponent } from './movie-import-no-search-res import { MovieImportSearchInfoComponent } from './movie-import-search-info/movie-import-search-info.component'; import { LoginDialog } from './login/login.dialog'; import { PerformanceInfoComponent } from './performance-info/performance-info.component'; -import { ShoppingCartComponent } from './shopping-cart/shopping-cart.component'; import { OrderComponent } from './order/order.component'; import { SeatSelectionComponent } from './seat-selection/seat-selection.component'; import { NoSeatsInHallComponent } from './no-seats-in-hall/no-seats-in-hall.component'; @@ -65,6 +64,7 @@ import { PurchaseFailedComponent } from './purchase-failed/purchase-failed.compo 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'; @NgModule({ @@ -98,7 +98,6 @@ import { ZoomWarningComponent } from './zoom-warning/zoom-warning.component'; MovieImportSearchInfoComponent, LoginDialog, PerformanceInfoComponent, - ShoppingCartComponent, OrderComponent, SeatSelectionComponent, NoSeatsInHallComponent, @@ -109,6 +108,7 @@ import { ZoomWarningComponent } from './zoom-warning/zoom-warning.component'; TicketSmallComponent, TicketListComponent, ZoomWarningComponent, + SelectionConflictInfoComponent, ], imports: [ AppRoutingModule, diff --git a/src/app/order/order.component.html b/src/app/order/order.component.html index 7b5b71d..5e8dffd 100644 --- a/src/app/order/order.component.html +++ b/src/app/order/order.component.html @@ -33,6 +33,10 @@
+ @if (selectedSeatsService.hadConflict()) { + + } +
@for (seatCategory of seatCategories(); track $index) { diff --git a/src/app/order/order.component.ts b/src/app/order/order.component.ts index 04717de..4d2665d 100644 --- a/src/app/order/order.component.ts +++ b/src/app/order/order.component.ts @@ -1,11 +1,11 @@ import { SelectedSeatsService } from './../selected-seats.service'; import { LoadingService } from './../loading.service'; import { Bestellung, Eintrittskarte, Sitzkategorie, Sitzplatz, Vorstellung } from '@infinimotion/model-frontend'; -import { Component, computed, DestroyRef, EventEmitter, inject, input, output, Output, signal, ViewChild } from '@angular/core'; +import { Component, computed, DestroyRef, inject, input, output, signal } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { StepperSelectionEvent } from '@angular/cdk/stepper'; import { HttpService } from '../http.service'; -import { catchError, tap, finalize, switchMap, map, EMPTY, forkJoin } from 'rxjs'; +import { catchError, tap, finalize, EMPTY } from 'rxjs'; import { MatStepper } from '@angular/material/stepper'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; diff --git a/src/app/selected-seats.service.ts b/src/app/selected-seats.service.ts index 18a84ff..2fe8456 100644 --- a/src/app/selected-seats.service.ts +++ b/src/app/selected-seats.service.ts @@ -1,5 +1,5 @@ import { computed, Injectable, signal } from '@angular/core'; -import {Sitzplatz} from '@infinimotion/model-frontend'; +import { Sitzplatz } from '@infinimotion/model-frontend'; @Injectable({ providedIn: 'root', @@ -10,21 +10,25 @@ export class SelectedSeatsService { private seatIsSelectableSignal = signal(true); private committedSignal = 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 debug = this.debugSignal.asReadonly(); + readonly hadConflict = this.hadConflictSignal.asReadonly(); readonly totalSeats = computed(() => this.selectedSeats().length); readonly totalPrice = computed(() => this.selectedSeats().reduce((sum, seat) => sum + seat.row.category.price, 0)); pushSelectedSeat(selectedSeat: Sitzplatz): void { this.selectedSeatsSignal.update(seats => [...seats, selectedSeat]); + this.setConflict(false); } removeSelectedSeat(selectedSeat: Sitzplatz): void { this.selectedSeatsSignal.update(seats => seats.filter(seat => seat.id !== selectedSeat.id)); + this.setConflict(false); } getSeatsByCategory(categoryId: number): Sitzplatz[] { @@ -58,4 +62,8 @@ export class SelectedSeatsService { isSeatSelected(seatId: number): boolean { return this.selectedSeats().some(seat => seat.id === seatId); } + + setConflict(value: boolean): void { + this.hadConflictSignal.set(value); + } } diff --git a/src/app/shopping-cart/shopping-cart.component.css b/src/app/selection-conflict-info/selection-conflict-info.component.css similarity index 100% rename from src/app/shopping-cart/shopping-cart.component.css rename to src/app/selection-conflict-info/selection-conflict-info.component.css diff --git a/src/app/selection-conflict-info/selection-conflict-info.component.html b/src/app/selection-conflict-info/selection-conflict-info.component.html new file mode 100644 index 0000000..d6f9b7e --- /dev/null +++ b/src/app/selection-conflict-info/selection-conflict-info.component.html @@ -0,0 +1,16 @@ +
+ +
+ + warning + +
+ +
+
+

Sitzplatz aus dem Warenkorb entfernt!

+
+

Leider ist der von Ihnen gewählte Sitzplazt nicht mehr verfügbar. Bitte wählen Sie einen anderen.

+
+ +
diff --git a/src/app/selection-conflict-info/selection-conflict-info.component.ts b/src/app/selection-conflict-info/selection-conflict-info.component.ts new file mode 100644 index 0000000..57ed39c --- /dev/null +++ b/src/app/selection-conflict-info/selection-conflict-info.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-selection-conflict-info', + standalone: false, + templateUrl: './selection-conflict-info.component.html', + styleUrl: './selection-conflict-info.component.css', +}) +export class SelectionConflictInfoComponent { + +} diff --git a/src/app/shopping-cart/shopping-cart.component.html b/src/app/shopping-cart/shopping-cart.component.html deleted file mode 100644 index 5aff33e..0000000 --- a/src/app/shopping-cart/shopping-cart.component.html +++ /dev/null @@ -1 +0,0 @@ -

shopping-cart works!

diff --git a/src/app/shopping-cart/shopping-cart.component.ts b/src/app/shopping-cart/shopping-cart.component.ts deleted file mode 100644 index 382aba1..0000000 --- a/src/app/shopping-cart/shopping-cart.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-shopping-cart', - standalone: false, - templateUrl: './shopping-cart.component.html', - styleUrl: './shopping-cart.component.css' -}) -export class ShoppingCartComponent { - -} diff --git a/src/app/theater-overlay/theater-overlay.component.ts b/src/app/theater-overlay/theater-overlay.component.ts index 961fb45..8fc9a88 100644 --- a/src/app/theater-overlay/theater-overlay.component.ts +++ b/src/app/theater-overlay/theater-overlay.component.ts @@ -7,7 +7,6 @@ import { TheaterSeatState } from '../model/theater-seat-state.model'; import { ActivatedRoute } from '@angular/router'; import { SelectedSeatsService } from '../selected-seats.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { MatDialog } from '@angular/material/dialog'; import { MatSnackBar, MatSnackBarRef, TextOnlySnackBar } from '@angular/material/snack-bar'; const POLLING_INTERVAL_MS = 5 * 1000; @@ -24,14 +23,14 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy { private route = inject(ActivatedRoute); private destroyRef = inject(DestroyRef); private selectedSeatService = inject(SelectedSeatsService); - private dialog = inject(MatDialog); readonly loading = inject(LoadingService); showId!: number; orderId?: string; - seatsPerRow = signal<{ seat: Sitzplatz | null, state: TheaterSeatState | null }[][]>([]); performance: Vorstellung | undefined; + blockedSeats: Sitzplatz[] | undefined; + seatsPerRow = signal<{ seat: Sitzplatz | null, state: TheaterSeatState | null }[][]>([]); seatCategories: Sitzkategorie[] = []; snackBarRef: MatSnackBarRef | undefined @@ -169,6 +168,21 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy { }).pipe( tap(({ performance, seats }) => { this.performance = performance; + + if (this.blockedSeats && !this.equalSeats(this.blockedSeats, seats.reserved)) { + console.info('[TheaterOverlay] External booking detected. Checking for conflicts.'); + + const conflicts = this.getConflictingSeats(seats.reserved); + if (conflicts.length > 0) { + console.info('[TheaterOverlay] Conflicts! Updating shopping cart.'); + conflicts.forEach(seat => this.selectedSeatService.removeSelectedSeat(seat)); + this.selectedSeatService.setConflict(true); + } + + this.selectedSeatService.selectedSeats + } + this.blockedSeats = seats.reserved; + this.seatsPerRow.set(this.converter(seats)); if (this.isInitialLoad()) { @@ -197,6 +211,28 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy { ); } + private equalSeats(a: Sitzplatz[], b: Sitzplatz[]): boolean { + if (a === b) return true; + if (a == null || b == null) return false; + if (a.length !== b.length) return false; + + // Arrays kopieren und sortieren + const sortedA = [...a].sort((a, b) => a.id - b.id); + const sortedB = [...b].sort((a, b) => a.id - b.id); + + for (let i = 0; i < sortedA.length; ++i) { + if (sortedA[i].id !== sortedB[i].id) return false; + } + return true; + } + + private getConflictingSeats(blockedSeats: Sitzplatz[]): Sitzplatz[] { + const blockedIds = new Set(blockedSeats.map(bs => bs.id)); + return this.selectedSeatService.selectedSeats().filter( + selectedSeat => blockedIds.has(selectedSeat.id) + ); + } + private converter(resp: { seats: Sitzplatz[], reserved: Sitzplatz[], booked: Sitzplatz[] }): { seat: Sitzplatz | null, state: TheaterSeatState | null From 53bbef417b34d5107cbe46f43ef58d2549b28df4 Mon Sep 17 00:00:00 2001 From: Piet Ostendorp Date: Wed, 19 Nov 2025 23:14:52 +0100 Subject: [PATCH 3/7] Bugfix conflict info --- src/app/selected-seats.service.ts | 5 +++-- src/app/theater-layout/theater-layout.component.html | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/selected-seats.service.ts b/src/app/selected-seats.service.ts index 2fe8456..72841ce 100644 --- a/src/app/selected-seats.service.ts +++ b/src/app/selected-seats.service.ts @@ -23,12 +23,12 @@ export class SelectedSeatsService { pushSelectedSeat(selectedSeat: Sitzplatz): void { this.selectedSeatsSignal.update(seats => [...seats, selectedSeat]); - this.setConflict(false); + this.hadConflictSignal.set(false); } removeSelectedSeat(selectedSeat: Sitzplatz): void { this.selectedSeatsSignal.update(seats => seats.filter(seat => seat.id !== selectedSeat.id)); - this.setConflict(false); + this.hadConflictSignal.set(false); } getSeatsByCategory(categoryId: number): Sitzplatz[] { @@ -38,6 +38,7 @@ export class SelectedSeatsService { clearSelection(): void { this.selectedSeatsSignal.set([]); this.committedSignal.set(false); + this.hadConflictSignal.set(false); } getSeatIsSelectable(): boolean{ diff --git a/src/app/theater-layout/theater-layout.component.html b/src/app/theater-layout/theater-layout.component.html index 0563b21..7d3060e 100644 --- a/src/app/theater-layout/theater-layout.component.html +++ b/src/app/theater-layout/theater-layout.component.html @@ -1,4 +1,4 @@ -
+

Leinwand

From 3bc5b9cd3ac0d3ca7f60a9feead5ff49316470c2 Mon Sep 17 00:00:00 2001 From: Piet Ostendorp Date: Thu, 20 Nov 2025 23:19:16 +0100 Subject: [PATCH 4/7] 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. --- src/app/app-module.ts | 6 + src/app/app-routing-module.ts | 3 +- .../cancellation-failed.component.css | 0 .../cancellation-failed.component.html | 11 ++ .../cancellation-failed.component.ts | 11 ++ .../cancellation-success.component.css | 0 .../cancellation-success.component.html | 11 ++ .../cancellation-success.component.ts | 11 ++ .../conversion-failed.component.css | 0 .../conversion-failed.component.html | 11 ++ .../conversion-failed.component.ts | 11 ++ src/app/http.service.ts | 6 +- .../movie-performance.component.ts | 2 +- src/app/order/order.component.html | 92 +++++++------ src/app/order/order.component.ts | 108 ++++++++++++++-- .../reservation-success.component.html | 2 +- src/app/seat/seat.component.html | 2 +- src/app/seat/seat.component.ts | 29 +++-- src/app/selected-seats.service.ts | 16 +++ .../theater-overlay.component.html | 4 +- .../theater-overlay.component.ts | 122 ++++++++++++++---- 21 files changed, 365 insertions(+), 93 deletions(-) create mode 100644 src/app/cancellation-failed/cancellation-failed.component.css create mode 100644 src/app/cancellation-failed/cancellation-failed.component.html create mode 100644 src/app/cancellation-failed/cancellation-failed.component.ts create mode 100644 src/app/cancellation-success/cancellation-success.component.css create mode 100644 src/app/cancellation-success/cancellation-success.component.html create mode 100644 src/app/cancellation-success/cancellation-success.component.ts create mode 100644 src/app/conversion-failed/conversion-failed.component.css create mode 100644 src/app/conversion-failed/conversion-failed.component.html create mode 100644 src/app/conversion-failed/conversion-failed.component.ts diff --git a/src/app/app-module.ts b/src/app/app-module.ts index a8554ec..c625000 100644 --- a/src/app/app-module.ts +++ b/src/app/app-module.ts @@ -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, diff --git a/src/app/app-routing-module.ts b/src/app/app-routing-module.ts index e5692a4..1678c5f 100644 --- a/src/app/app-routing-module.ts +++ b/src/app/app-routing-module.ts @@ -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}, ], }, diff --git a/src/app/cancellation-failed/cancellation-failed.component.css b/src/app/cancellation-failed/cancellation-failed.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/cancellation-failed/cancellation-failed.component.html b/src/app/cancellation-failed/cancellation-failed.component.html new file mode 100644 index 0000000..0bd5a1d --- /dev/null +++ b/src/app/cancellation-failed/cancellation-failed.component.html @@ -0,0 +1,11 @@ +
+ + warning + +

Stornierung fehlgeschlagen!

+

Leider konnten Ihre Sitzplätze nicht storniert werden. Möglicherweise wurden die Tickets bereits bezahlt oder storniert.

+ + + +
+ diff --git a/src/app/cancellation-failed/cancellation-failed.component.ts b/src/app/cancellation-failed/cancellation-failed.component.ts new file mode 100644 index 0000000..a6e7519 --- /dev/null +++ b/src/app/cancellation-failed/cancellation-failed.component.ts @@ -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 { + +} diff --git a/src/app/cancellation-success/cancellation-success.component.css b/src/app/cancellation-success/cancellation-success.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/cancellation-success/cancellation-success.component.html b/src/app/cancellation-success/cancellation-success.component.html new file mode 100644 index 0000000..d7d1c22 --- /dev/null +++ b/src/app/cancellation-success/cancellation-success.component.html @@ -0,0 +1,11 @@ +
+ + task_alt + +

Stornierung erfolgreich!

+

Ihre Sitzplätze wurden erfolgreich storniert und stehen wieder zur Buchung zur Verfügnug.

+ + + + +
diff --git a/src/app/cancellation-success/cancellation-success.component.ts b/src/app/cancellation-success/cancellation-success.component.ts new file mode 100644 index 0000000..75e645f --- /dev/null +++ b/src/app/cancellation-success/cancellation-success.component.ts @@ -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(); +} diff --git a/src/app/conversion-failed/conversion-failed.component.css b/src/app/conversion-failed/conversion-failed.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/conversion-failed/conversion-failed.component.html b/src/app/conversion-failed/conversion-failed.component.html new file mode 100644 index 0000000..d822ae3 --- /dev/null +++ b/src/app/conversion-failed/conversion-failed.component.html @@ -0,0 +1,11 @@ +
+ + warning + +

Kauf fehlgeschlagen!

+

Leider konnten Ihre Sitzplätze nicht bezahlt werden. Möglicherweise wurden die Tickets bereits storniert.

+ + + +
+ diff --git a/src/app/conversion-failed/conversion-failed.component.ts b/src/app/conversion-failed/conversion-failed.component.ts new file mode 100644 index 0000000..9ba4c68 --- /dev/null +++ b/src/app/conversion-failed/conversion-failed.component.ts @@ -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 { + +} diff --git a/src/app/http.service.ts b/src/app/http.service.ts index ede6c37..3db7819 100644 --- a/src/app/http.service.ts +++ b/src/app/http.service.ts @@ -33,9 +33,9 @@ export class HttpService { return this.http.post(`${this.baseUrl}bestellung`, order); } - /* PUT /api/bestellung/{id} */ - updateOrder(id: number, order: Partial): Observable { - return this.http.put(`${this.baseUrl}bestellung/${id}`, order); + /* PUT /api/bestellung */ + updateOrder(order: Partial): Observable { + return this.http.put(`${this.baseUrl}bestellung`, order); } /* DELETE /api/bestellung/{id} */ diff --git a/src/app/movie-performance/movie-performance.component.ts b/src/app/movie-performance/movie-performance.component.ts index 6f98ff9..217cec6 100644 --- a/src/app/movie-performance/movie-performance.component.ts +++ b/src/app/movie-performance/movie-performance.component.ts @@ -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(() => diff --git a/src/app/order/order.component.html b/src/app/order/order.component.html index 5e8dffd..f9a2b58 100644 --- a/src/app/order/order.component.html +++ b/src/app/order/order.component.html @@ -27,47 +27,47 @@
} - - - Warenkorb + + + Warenkorb -
+
- @if (selectedSeatsService.hadConflict()) { - + @if (selectedSeatsService.hadConflict()) { + + } + + +
+ @for (seatCategory of seatCategories(); track $index) { +
+ } + @empty { + + } +
- -
- @for (seatCategory of seatCategories(); track $index) { -
- - } - @empty { - - } -
+ - + +
+

+ Tickets gesamt: +

+

+ {{ getPriceDisplay(totalPrice()) }} +

+
- -
-

- Tickets gesamt: -

-

- {{ getPriceDisplay(totalPrice()) }} -

-
- - -
- - -
+ +
+ + +
- +
Anschrift @@ -129,6 +129,18 @@
} + @else if (isConversionError() && !isSubmitting()) { +
+ + } + @else if (isCancellationSuccess() && !isSubmitting() && performance()) { +
+ + } + @else if (isCancellationError() && !isSubmitting()) { +
+ + } @else { @@ -183,9 +195,17 @@
- + + @if (isResuming()) { + + } @else { + + } + diff --git a/src/app/order/order.component.ts b/src/app/order/order.component.ts index 4d2665d..e3aa851 100644 --- a/src/app/order/order.component.ts +++ b/src/app/order/order.component.ts @@ -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(); seatCategories = input.required(); + existingOrder = input(); + existingTickets = input(); + stepChanged = output(); 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(); diff --git a/src/app/reservation-success/reservation-success.component.html b/src/app/reservation-success/reservation-success.component.html index 8d28e59..a8c961c 100644 --- a/src/app/reservation-success/reservation-success.component.html +++ b/src/app/reservation-success/reservation-success.component.html @@ -6,7 +6,7 @@ {{ order().code }}
- +
Reservierung stornieren diff --git a/src/app/seat/seat.component.html b/src/app/seat/seat.component.html index 975c099..f4a1015 100644 --- a/src/app/seat/seat.component.html +++ b/src/app/seat/seat.component.html @@ -1,4 +1,4 @@ - + + +
diff --git a/src/app/pay-for-order/pay-for-order.component.ts b/src/app/pay-for-order/pay-for-order.component.ts new file mode 100644 index 0000000..c12b83b --- /dev/null +++ b/src/app/pay-for-order/pay-for-order.component.ts @@ -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(); + } +} diff --git a/src/app/theater-overlay/theater-overlay.component.ts b/src/app/theater-overlay/theater-overlay.component.ts index 270f272..4545191 100644 --- a/src/app/theater-overlay/theater-overlay.component.ts +++ b/src/app/theater-overlay/theater-overlay.component.ts @@ -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(); } From ade5479a74b60296155f80d1e218c611a6057be1 Mon Sep 17 00:00:00 2001 From: Piet Ostendorp Date: Fri, 21 Nov 2025 15:53:42 +0100 Subject: [PATCH 7/7] 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. --- src/app/app-module.ts | 2 + src/app/cancel-order/cancel-order.dialog.css | 3 ++ src/app/cancel-order/cancel-order.dialog.html | 10 ++++ src/app/cancel-order/cancel-order.dialog.ts | 22 +++++++++ .../cancellation-failed.component.html | 6 +-- .../cancellation-failed.component.ts | 12 ++++- .../cancellation-success.component.html | 8 +-- .../cancellation-success.component.ts | 18 ++++++- .../conversion-failed.component.html | 6 +-- .../conversion-failed.component.ts | 12 ++++- src/app/loading.service.ts | 21 ++++++-- .../movie-importer.component.html | 2 +- .../movie-importer.component.ts | 10 ++-- src/app/order/order.component.css | 4 ++ src/app/order/order.component.html | 22 ++++----- src/app/order/order.component.ts | 49 ++++++++++++++++++- .../pay-for-order.component.html | 6 +-- .../pay-for-order/pay-for-order.component.ts | 2 +- .../purchase-failed.component.html | 6 +-- .../purchase-failed.component.ts | 20 +++++++- .../purchase-success.component.html | 6 +-- .../purchase-success.component.ts | 14 ++++++ .../reservation-failed.component.html | 6 +-- .../reservation-failed.component.ts | 20 +++++++- .../reservation-success.component.html | 8 +-- .../reservation-success.component.ts | 18 +++++-- src/app/selected-seats.service.ts | 2 + .../theater-overlay.component.html | 2 +- .../theater-overlay.component.ts | 7 +++ 29 files changed, 266 insertions(+), 58 deletions(-) create mode 100644 src/app/cancel-order/cancel-order.dialog.css create mode 100644 src/app/cancel-order/cancel-order.dialog.html create mode 100644 src/app/cancel-order/cancel-order.dialog.ts diff --git a/src/app/app-module.ts b/src/app/app-module.ts index 59d2558..2558a4e 100644 --- a/src/app/app-module.ts +++ b/src/app/app-module.ts @@ -69,6 +69,7 @@ import { CancellationSuccessComponent } from './cancellation-success/cancellatio 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'; +import { CancelOrderDialog } from './cancel-order/cancel-order.dialog'; @NgModule({ @@ -117,6 +118,7 @@ import { PayForOrderComponent } from './pay-for-order/pay-for-order.component'; CancellationFailedComponent, ConversionFailedComponent, PayForOrderComponent, + CancelOrderDialog, ], imports: [ AppRoutingModule, diff --git a/src/app/cancel-order/cancel-order.dialog.css b/src/app/cancel-order/cancel-order.dialog.css new file mode 100644 index 0000000..f785beb --- /dev/null +++ b/src/app/cancel-order/cancel-order.dialog.css @@ -0,0 +1,3 @@ +button { + min-width: 100px; +} diff --git a/src/app/cancel-order/cancel-order.dialog.html b/src/app/cancel-order/cancel-order.dialog.html new file mode 100644 index 0000000..0c953f3 --- /dev/null +++ b/src/app/cancel-order/cancel-order.dialog.html @@ -0,0 +1,10 @@ +

Möchten Sie Ihre Bestellung wirklich stornieren?

+ + +

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.

+
+ + + + + diff --git a/src/app/cancel-order/cancel-order.dialog.ts b/src/app/cancel-order/cancel-order.dialog.ts new file mode 100644 index 0000000..44dc764 --- /dev/null +++ b/src/app/cancel-order/cancel-order.dialog.ts @@ -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, + ) {} + + submit(): void { + this.dialogRef.close(true); + } + + cancel(): void { + this.dialogRef.close(false); + } +} diff --git a/src/app/cancellation-failed/cancellation-failed.component.html b/src/app/cancellation-failed/cancellation-failed.component.html index 0bd5a1d..773143e 100644 --- a/src/app/cancellation-failed/cancellation-failed.component.html +++ b/src/app/cancellation-failed/cancellation-failed.component.html @@ -3,9 +3,9 @@ warning

Stornierung fehlgeschlagen!

-

Leider konnten Ihre Sitzplätze nicht storniert werden. Möglicherweise wurden die Tickets bereits bezahlt oder storniert.

+

{{ infoText }}

- - + +
diff --git a/src/app/cancellation-failed/cancellation-failed.component.ts b/src/app/cancellation-failed/cancellation-failed.component.ts index a6e7519..05663f0 100644 --- a/src/app/cancellation-failed/cancellation-failed.component.ts +++ b/src/app/cancellation-failed/cancellation-failed.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, input, output } from '@angular/core'; @Component({ selector: 'app-cancellation-failed', @@ -7,5 +7,15 @@ import { Component } from '@angular/core'; styleUrl: './cancellation-failed.component.css', }) export class CancellationFailedComponent { + moreThanOne = input(false); + retry = output(); + + 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.'; + } } diff --git a/src/app/cancellation-success/cancellation-success.component.html b/src/app/cancellation-success/cancellation-success.component.html index d7d1c22..166309e 100644 --- a/src/app/cancellation-success/cancellation-success.component.html +++ b/src/app/cancellation-success/cancellation-success.component.html @@ -1,11 +1,11 @@ -
+
task_alt

Stornierung erfolgreich!

-

Ihre Sitzplätze wurden erfolgreich storniert und stehen wieder zur Buchung zur Verfügnug.

+

{{ infoText }}

- - + +
diff --git a/src/app/cancellation-success/cancellation-success.component.ts b/src/app/cancellation-success/cancellation-success.component.ts index 75e645f..6110bed 100644 --- a/src/app/cancellation-success/cancellation-success.component.ts +++ b/src/app/cancellation-success/cancellation-success.component.ts @@ -1,4 +1,5 @@ -import { Component, input } from '@angular/core'; +import { Component, inject, input } from '@angular/core'; +import { Router } from '@angular/router'; @Component({ selector: 'app-cancellation-success', @@ -8,4 +9,19 @@ import { Component, input } from '@angular/core'; }) export class CancellationSuccessComponent { performanceId = input.required(); + moreThanOne = input(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()}`; + } } diff --git a/src/app/conversion-failed/conversion-failed.component.html b/src/app/conversion-failed/conversion-failed.component.html index d822ae3..018a3ad 100644 --- a/src/app/conversion-failed/conversion-failed.component.html +++ b/src/app/conversion-failed/conversion-failed.component.html @@ -3,9 +3,9 @@ warning

Kauf fehlgeschlagen!

-

Leider konnten Ihre Sitzplätze nicht bezahlt werden. Möglicherweise wurden die Tickets bereits storniert.

+

{{ infoText }}

- - + +
diff --git a/src/app/conversion-failed/conversion-failed.component.ts b/src/app/conversion-failed/conversion-failed.component.ts index 9ba4c68..3957541 100644 --- a/src/app/conversion-failed/conversion-failed.component.ts +++ b/src/app/conversion-failed/conversion-failed.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, input, output } from '@angular/core'; @Component({ selector: 'app-conversion-failed', @@ -7,5 +7,15 @@ import { Component } from '@angular/core'; styleUrl: './conversion-failed.component.css', }) export class ConversionFailedComponent { + moreThanOne = input(false); + retry = output(); + + 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.'; + } } diff --git a/src/app/loading.service.ts b/src/app/loading.service.ts index 988e652..532b309 100644 --- a/src/app/loading.service.ts +++ b/src/app/loading.service.ts @@ -1,7 +1,7 @@ import { HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { MatSnackBar } from '@angular/material/snack-bar'; -import { BehaviorSubject } from 'rxjs'; +import { MatSnackBar, MatSnackBarRef } from '@angular/material/snack-bar'; +import { BehaviorSubject, Subscription } from 'rxjs'; @Injectable({ providedIn: 'root' @@ -13,6 +13,9 @@ export class LoadingService { public loading$ = this.loadingSubject.asObservable(); public error$ = this.errorSubject.asObservable(); + private currentSnackBarRef?: MatSnackBarRef; + private currentSubscription?: Subscription; + constructor(private snackBar: MatSnackBar) {} show(): void { @@ -23,6 +26,7 @@ export class LoadingService { hide(): void { this.loadingSubject.next(false); this.errorSubject.next(false); + this.currentSnackBarRef?.dismiss(); } showError(messageOrError?: string | HttpErrorResponse | any): void { @@ -35,15 +39,22 @@ export class LoadingService { 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, panelClass: ['error-snackbar'], horizontalPosition: 'center', verticalPosition: 'bottom' }); - snackBarRef.afterDismissed().subscribe(() => { - this.hide(); + this.currentSubscription = this.currentSnackBarRef.afterDismissed().subscribe(() => { + if (!this.loadingSubject.value) { + this.hide(); + } }); } diff --git a/src/app/movie-importer/movie-importer.component.html b/src/app/movie-importer/movie-importer.component.html index 6f614b8..59385ca 100644 --- a/src/app/movie-importer/movie-importer.component.html +++ b/src/app/movie-importer/movie-importer.component.html @@ -5,7 +5,7 @@
Film online suchen - + @if (formControl.hasError('noResults')) { Keine Suchergebnisse gefunden } diff --git a/src/app/movie-importer/movie-importer.component.ts b/src/app/movie-importer/movie-importer.component.ts index c68289b..2419b29 100644 --- a/src/app/movie-importer/movie-importer.component.ts +++ b/src/app/movie-importer/movie-importer.component.ts @@ -1,9 +1,10 @@ import { LoadingService } from './../loading.service'; -import { Component, inject } from '@angular/core'; +import { Component, DestroyRef, inject } from '@angular/core'; import { FormControl } from '@angular/forms'; import { catchError, finalize, of, tap } from 'rxjs'; import { HttpService } from '../http.service'; import { OmdbMovie } from '@infinimotion/model-frontend'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'app-movie-importer', @@ -21,13 +22,15 @@ export class MovieImporterComponent { private httpService = inject(HttpService) public loadingService = inject(LoadingService) + private destroyRef = inject(DestroyRef); + DoSubmit() { this.showAll = false; this.searchForMovies(); } - searchForMovies() { + private searchForMovies() { this.search_query = this.formControl.value?.trim() || ''; if (this.search_query?.length == 0) return; @@ -48,7 +51,8 @@ export class MovieImporterComponent { finalize(() => { this.isSearching = false; this.formControl.enable(); - }) + }), + takeUntilDestroyed(this.destroyRef) ).subscribe(); } diff --git a/src/app/order/order.component.css b/src/app/order/order.component.css index 367c809..9732f3d 100644 --- a/src/app/order/order.component.css +++ b/src/app/order/order.component.css @@ -1,3 +1,7 @@ +:host { + min-width: 500px; +} + mat-stepper { background: transparent !important; } diff --git a/src/app/order/order.component.html b/src/app/order/order.component.html index f9a2b58..ab9e36b 100644 --- a/src/app/order/order.component.html +++ b/src/app/order/order.component.html @@ -28,7 +28,7 @@ } - + Warenkorb
@@ -67,7 +67,7 @@
- +
Anschrift @@ -75,11 +75,11 @@ @if (isReservationSuccess() && !isSubmitting()) {
- + } - @else if (isReservationError() && !isSubmitting()) { + @else if (isReservationError() && !isSubmitting() && performance()) {
- + } @else { @@ -123,23 +123,23 @@ @if (isPurchaseSuccess() && !isSubmitting()) {
- + } - @else if (isPurchaseError() && !isSubmitting()) { + @else if (isPurchaseError() && !isSubmitting() && performance()) {
- + } @else if (isConversionError() && !isSubmitting()) {
- + } @else if (isCancellationSuccess() && !isSubmitting() && performance()) {
- + } @else if (isCancellationError() && !isSubmitting()) {
- + } @else { diff --git a/src/app/order/order.component.ts b/src/app/order/order.component.ts index e3aa851..e4631ef 100644 --- a/src/app/order/order.component.ts +++ b/src/app/order/order.component.ts @@ -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(); existingTickets = input(); + resumeWithCancel = input(true); stepChanged = output(); @@ -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(); + } } diff --git a/src/app/pay-for-order/pay-for-order.component.html b/src/app/pay-for-order/pay-for-order.component.html index bac0bdd..dff3f0f 100644 --- a/src/app/pay-for-order/pay-for-order.component.html +++ b/src/app/pay-for-order/pay-for-order.component.html @@ -8,7 +8,7 @@ @if (formControl.hasError('invalid')) { - Ungültiger Bestellcode + Ungültiger Reservierungscode } @else if (formControl.hasError('completed')) { Diese Bestellung wurde bereits abgeschlossen @@ -23,10 +23,10 @@ Diese Bestellung wurde bereits bezahlt } @else if (formControl.hasError('cancelled')) { - Diese Bestellung wurde storniert + Diese Reservierung wurde storniert } @else if (formControl.hasError('serverError')) { - Fehler beim Laden der Bestellung + Fehler beim Laden der Reservierung } diff --git a/src/app/pay-for-order/pay-for-order.component.ts b/src/app/pay-for-order/pay-for-order.component.ts index c12b83b..3c2c06f 100644 --- a/src/app/pay-for-order/pay-for-order.component.ts +++ b/src/app/pay-for-order/pay-for-order.component.ts @@ -3,7 +3,7 @@ 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 { catchError, finalize, map, of, take } from 'rxjs'; import { ActivatedRoute, Router } from '@angular/router'; @Component({ diff --git a/src/app/purchase-failed/purchase-failed.component.html b/src/app/purchase-failed/purchase-failed.component.html index d841ce0..b2a104a 100644 --- a/src/app/purchase-failed/purchase-failed.component.html +++ b/src/app/purchase-failed/purchase-failed.component.html @@ -3,9 +3,9 @@ warning

Kauf fehlgeschlagen!

-

Leider konnten Ihre Sitzplätze nicht gekauft werden. Dies kann passieren, wenn andere Nutzer zeitgleich versucht haben, dieselben Sitzplätze zu kaufen.

+

{{ infoText }}

- - + +
diff --git a/src/app/purchase-failed/purchase-failed.component.ts b/src/app/purchase-failed/purchase-failed.component.ts index 619d3e9..50c3db5 100644 --- a/src/app/purchase-failed/purchase-failed.component.ts +++ b/src/app/purchase-failed/purchase-failed.component.ts @@ -1,4 +1,5 @@ -import { Component } from '@angular/core'; +import { Component, inject, input, output } from '@angular/core'; +import { Router } from '@angular/router'; @Component({ selector: 'app-purchase-failed', @@ -7,5 +8,22 @@ import { Component } from '@angular/core'; styleUrl: './purchase-failed.component.css', }) export class PurchaseFailedComponent { + performanceId = input.required(); + moreThanOne = input(false); + retry = output(); + + 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()}`; + } } diff --git a/src/app/purchase-success/purchase-success.component.html b/src/app/purchase-success/purchase-success.component.html index c439043..68777c2 100644 --- a/src/app/purchase-success/purchase-success.component.html +++ b/src/app/purchase-success/purchase-success.component.html @@ -1,11 +1,11 @@

Vielen Dank für Ihren Einkauf!

-

Ihre Sitzplätze wurden erfolgreich gebucht.

+

{{ infoText }}

- - + +
diff --git a/src/app/purchase-success/purchase-success.component.ts b/src/app/purchase-success/purchase-success.component.ts index 8dad591..3805ad4 100644 --- a/src/app/purchase-success/purchase-success.component.ts +++ b/src/app/purchase-success/purchase-success.component.ts @@ -9,4 +9,18 @@ import { Component, input } from '@angular/core'; }) export class PurchaseSuccessComponent { tickets = input.required(); + moreThanOne = input(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'; + } } diff --git a/src/app/reservation-failed/reservation-failed.component.html b/src/app/reservation-failed/reservation-failed.component.html index abd1532..8e89629 100644 --- a/src/app/reservation-failed/reservation-failed.component.html +++ b/src/app/reservation-failed/reservation-failed.component.html @@ -3,9 +3,9 @@ warning

Reservierung fehlgeschlagen!

-

Leider konnten Ihre Sitzplätze nicht reserviert werden. Dies kann passieren, wenn andere Nutzer gleichzeitig versucht haben, dieselben Sitzplätze zu reservieren.

+

{{ infoText }}

- - + + diff --git a/src/app/reservation-failed/reservation-failed.component.ts b/src/app/reservation-failed/reservation-failed.component.ts index 7693d1e..0018215 100644 --- a/src/app/reservation-failed/reservation-failed.component.ts +++ b/src/app/reservation-failed/reservation-failed.component.ts @@ -1,4 +1,5 @@ -import { Component } from '@angular/core'; +import { Router } from '@angular/router'; +import { Component, inject, input, output } from '@angular/core'; @Component({ selector: 'app-reservation-failed', @@ -7,5 +8,22 @@ import { Component } from '@angular/core'; styleUrl: './reservation-failed.component.css', }) export class ReservationFailedComponent { + performanceId = input.required(); + moreThanOne = input(false); + retry = output(); + + 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()}`; + } } diff --git a/src/app/reservation-success/reservation-success.component.html b/src/app/reservation-success/reservation-success.component.html index a8c961c..fac0714 100644 --- a/src/app/reservation-success/reservation-success.component.html +++ b/src/app/reservation-success/reservation-success.component.html @@ -1,14 +1,14 @@

Reservierung erfolgreich!

-

Ihre Sitzplätze wurden erfolgreich reserviert. Bitte nennen sie den folgenden Code an der Kasse, um Ihre Reservierung in eine Buchung umzuwandeln.

+

{{ infoText }}

{{ order().code }}
- - -
+ + +
Reservierung stornieren
diff --git a/src/app/reservation-success/reservation-success.component.ts b/src/app/reservation-success/reservation-success.component.ts index 8754ae6..1efca8e 100644 --- a/src/app/reservation-success/reservation-success.component.ts +++ b/src/app/reservation-success/reservation-success.component.ts @@ -1,5 +1,5 @@ import { Bestellung } from '@infinimotion/model-frontend'; -import { Component, input } from '@angular/core'; +import { Component, input, OnInit, output } from '@angular/core'; @Component({ selector: 'app-reservation-success', @@ -7,10 +7,20 @@ import { Component, input } from '@angular/core'; templateUrl: './reservation-success.component.html', styleUrl: './reservation-success.component.css', }) -export class ReservationSuccessComponent { +export class ReservationSuccessComponent implements OnInit { order = input.required(); + moreThanOne = input(false); - cancelReservation() { - // Logic to cancel the reservation + infoText!: string; + 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'; } } diff --git a/src/app/selected-seats.service.ts b/src/app/selected-seats.service.ts index 8f54b22..9be1b06 100644 --- a/src/app/selected-seats.service.ts +++ b/src/app/selected-seats.service.ts @@ -61,6 +61,7 @@ export class SelectedSeatsService { } commit(): void { + this.erroredSignal.set(false); this.committedSignal.set(true); } @@ -69,6 +70,7 @@ export class SelectedSeatsService { } cancel(): void { + this.erroredSignal.set(false); this.cancelledSignal.set(true); } diff --git a/src/app/theater-overlay/theater-overlay.component.html b/src/app/theater-overlay/theater-overlay.component.html index d224028..d7599bd 100644 --- a/src/app/theater-overlay/theater-overlay.component.html +++ b/src/app/theater-overlay/theater-overlay.component.html @@ -18,6 +18,6 @@
- + diff --git a/src/app/theater-overlay/theater-overlay.component.ts b/src/app/theater-overlay/theater-overlay.component.ts index 4545191..d85cb6d 100644 --- a/src/app/theater-overlay/theater-overlay.component.ts +++ b/src/app/theater-overlay/theater-overlay.component.ts @@ -31,6 +31,7 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy { orderId?: number; orderCode?: string | null; isResuming = false; + resumeWithCancel = false; tickets: Eintrittskarte[] | undefined; order: Bestellung | undefined; blockedSeats: Sitzplatz[] | undefined; @@ -56,8 +57,14 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy { if (this.orderCode) { // Checkout fortsetzen this.isResuming = true; + this.loadExistingOrder(this.orderCode); + this.route.queryParams.subscribe(params => { + if (params['action'] === 'cancel') { + this.resumeWithCancel = true; + } + }); } else if (this.showId) { // Neuer Checkout this.isResuming = false;