From 711ad39dd6f801a20f4ee46b097bf957e421a2e8 Mon Sep 17 00:00:00 2001 From: Piet Ostendorp Date: Wed, 19 Nov 2025 19:53:13 +0100 Subject: [PATCH] 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