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.
This commit is contained in:
2025-11-19 19:53:13 +01:00
parent b619d744c1
commit 711ad39dd6
10 changed files with 83 additions and 20 deletions

View File

@@ -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 { MovieImportSearchInfoComponent } from './movie-import-search-info/movie-import-search-info.component';
import { LoginDialog } from './login/login.dialog'; import { LoginDialog } from './login/login.dialog';
import { PerformanceInfoComponent } from './performance-info/performance-info.component'; import { PerformanceInfoComponent } from './performance-info/performance-info.component';
import { ShoppingCartComponent } from './shopping-cart/shopping-cart.component';
import { OrderComponent } from './order/order.component'; import { OrderComponent } from './order/order.component';
import { SeatSelectionComponent } from './seat-selection/seat-selection.component'; import { SeatSelectionComponent } from './seat-selection/seat-selection.component';
import { NoSeatsInHallComponent } from './no-seats-in-hall/no-seats-in-hall.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 { TicketSmallComponent } from './ticket-small/ticket-small.component';
import { TicketListComponent } from './ticket-list/ticket-list.component'; import { TicketListComponent } from './ticket-list/ticket-list.component';
import { ZoomWarningComponent } from './zoom-warning/zoom-warning.component'; import { ZoomWarningComponent } from './zoom-warning/zoom-warning.component';
import { SelectionConflictInfoComponent } from './selection-conflict-info/selection-conflict-info.component';
@NgModule({ @NgModule({
@@ -98,7 +98,6 @@ import { ZoomWarningComponent } from './zoom-warning/zoom-warning.component';
MovieImportSearchInfoComponent, MovieImportSearchInfoComponent,
LoginDialog, LoginDialog,
PerformanceInfoComponent, PerformanceInfoComponent,
ShoppingCartComponent,
OrderComponent, OrderComponent,
SeatSelectionComponent, SeatSelectionComponent,
NoSeatsInHallComponent, NoSeatsInHallComponent,
@@ -109,6 +108,7 @@ import { ZoomWarningComponent } from './zoom-warning/zoom-warning.component';
TicketSmallComponent, TicketSmallComponent,
TicketListComponent, TicketListComponent,
ZoomWarningComponent, ZoomWarningComponent,
SelectionConflictInfoComponent,
], ],
imports: [ imports: [
AppRoutingModule, AppRoutingModule,

View File

@@ -33,6 +33,10 @@
<div class="performance-info-space"></div> <div class="performance-info-space"></div>
@if (selectedSeatsService.hadConflict()) {
<app-selection-conflict-info></app-selection-conflict-info>
}
<!-- Seat-Selection Overview --> <!-- Seat-Selection Overview -->
<div class="mb-4 p-2"> <div class="mb-4 p-2">
@for (seatCategory of seatCategories(); track $index) { @for (seatCategory of seatCategories(); track $index) {

View File

@@ -1,11 +1,11 @@
import { SelectedSeatsService } from './../selected-seats.service'; import { SelectedSeatsService } from './../selected-seats.service';
import { LoadingService } from './../loading.service'; import { LoadingService } from './../loading.service';
import { Bestellung, Eintrittskarte, Sitzkategorie, Sitzplatz, Vorstellung } from '@infinimotion/model-frontend'; 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 { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { StepperSelectionEvent } from '@angular/cdk/stepper'; import { StepperSelectionEvent } from '@angular/cdk/stepper';
import { HttpService } from '../http.service'; 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 { MatStepper } from '@angular/material/stepper';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

View File

@@ -10,21 +10,25 @@ export class SelectedSeatsService {
private seatIsSelectableSignal = signal(true); private seatIsSelectableSignal = signal(true);
private committedSignal = signal(false); private committedSignal = signal(false);
private debugSignal = signal(false); private debugSignal = signal(false);
private hadConflictSignal = signal(false);
readonly selectedSeats = this.selectedSeatsSignal.asReadonly(); readonly selectedSeats = this.selectedSeatsSignal.asReadonly();
readonly seatIsSelectable = this.seatIsSelectableSignal.asReadonly(); readonly seatIsSelectable = this.seatIsSelectableSignal.asReadonly();
readonly committed = this.committedSignal.asReadonly(); readonly committed = this.committedSignal.asReadonly();
readonly debug = this.debugSignal.asReadonly(); readonly debug = this.debugSignal.asReadonly();
readonly hadConflict = this.hadConflictSignal.asReadonly();
readonly totalSeats = computed(() => this.selectedSeats().length); readonly totalSeats = computed(() => this.selectedSeats().length);
readonly totalPrice = computed(() => this.selectedSeats().reduce((sum, seat) => sum + seat.row.category.price, 0)); readonly totalPrice = computed(() => this.selectedSeats().reduce((sum, seat) => sum + seat.row.category.price, 0));
pushSelectedSeat(selectedSeat: Sitzplatz): void { pushSelectedSeat(selectedSeat: Sitzplatz): void {
this.selectedSeatsSignal.update(seats => [...seats, selectedSeat]); this.selectedSeatsSignal.update(seats => [...seats, selectedSeat]);
this.setConflict(false);
} }
removeSelectedSeat(selectedSeat: Sitzplatz): void { removeSelectedSeat(selectedSeat: Sitzplatz): void {
this.selectedSeatsSignal.update(seats => seats.filter(seat => seat.id !== selectedSeat.id)); this.selectedSeatsSignal.update(seats => seats.filter(seat => seat.id !== selectedSeat.id));
this.setConflict(false);
} }
getSeatsByCategory(categoryId: number): Sitzplatz[] { getSeatsByCategory(categoryId: number): Sitzplatz[] {
@@ -58,4 +62,8 @@ export class SelectedSeatsService {
isSeatSelected(seatId: number): boolean { isSeatSelected(seatId: number): boolean {
return this.selectedSeats().some(seat => seat.id === seatId); return this.selectedSeats().some(seat => seat.id === seatId);
} }
setConflict(value: boolean): void {
this.hadConflictSignal.set(value);
}
} }

View File

@@ -0,0 +1,16 @@
<div class="info-box bg-orange-100 h-22 w-full flex items-center space-x-4 rounded-md p-1 px-3 shadow-sm">
<div class="w-fit">
<mat-icon class="material-symbols-outlined mt-1" style="font-size: 40px; width: 40px; height: 40px;">
warning
</mat-icon>
</div>
<div class="text-md">
<div class="flex space-x-1.5">
<h3 class="font-semibold">Sitzplatz aus dem Warenkorb entfernt!</h3>
</div>
<p>Leider ist der von Ihnen gewählte Sitzplazt nicht mehr verfügbar. Bitte wählen Sie einen anderen.</p>
</div>
</div>

View File

@@ -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 {
}

View File

@@ -1 +0,0 @@
<p>shopping-cart works!</p>

View File

@@ -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 {
}

View File

@@ -7,7 +7,6 @@ import { TheaterSeatState } from '../model/theater-seat-state.model';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { SelectedSeatsService } from '../selected-seats.service'; import { SelectedSeatsService } from '../selected-seats.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar, MatSnackBarRef, TextOnlySnackBar } from '@angular/material/snack-bar'; import { MatSnackBar, MatSnackBarRef, TextOnlySnackBar } from '@angular/material/snack-bar';
const POLLING_INTERVAL_MS = 5 * 1000; const POLLING_INTERVAL_MS = 5 * 1000;
@@ -24,14 +23,14 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private destroyRef = inject(DestroyRef); private destroyRef = inject(DestroyRef);
private selectedSeatService = inject(SelectedSeatsService); private selectedSeatService = inject(SelectedSeatsService);
private dialog = inject(MatDialog);
readonly loading = inject(LoadingService); readonly loading = inject(LoadingService);
showId!: number; showId!: number;
orderId?: string; orderId?: string;
seatsPerRow = signal<{ seat: Sitzplatz | null, state: TheaterSeatState | null }[][]>([]);
performance: Vorstellung | undefined; performance: Vorstellung | undefined;
blockedSeats: Sitzplatz[] | undefined;
seatsPerRow = signal<{ seat: Sitzplatz | null, state: TheaterSeatState | null }[][]>([]);
seatCategories: Sitzkategorie[] = []; seatCategories: Sitzkategorie[] = [];
snackBarRef: MatSnackBarRef<TextOnlySnackBar> | undefined snackBarRef: MatSnackBarRef<TextOnlySnackBar> | undefined
@@ -169,6 +168,21 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
}).pipe( }).pipe(
tap(({ performance, seats }) => { tap(({ performance, seats }) => {
this.performance = performance; 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)); this.seatsPerRow.set(this.converter(seats));
if (this.isInitialLoad()) { 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[] }): { private converter(resp: { seats: Sitzplatz[], reserved: Sitzplatz[], booked: Sitzplatz[] }): {
seat: Sitzplatz | null, seat: Sitzplatz | null,
state: TheaterSeatState | null state: TheaterSeatState | null