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:
@@ -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,
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
|
||||
<div class="performance-info-space"></div>
|
||||
|
||||
@if (selectedSeatsService.hadConflict()) {
|
||||
<app-selection-conflict-info></app-selection-conflict-info>
|
||||
}
|
||||
|
||||
<!-- Seat-Selection Overview -->
|
||||
<div class="mb-4 p-2">
|
||||
@for (seatCategory of seatCategories(); track $index) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<p>shopping-cart works!</p>
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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<TextOnlySnackBar> | 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
|
||||
|
||||
Reference in New Issue
Block a user