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 { 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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { 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
|
||||||
|
|||||||
Reference in New Issue
Block a user