|
|
|
|
@@ -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<TextOnlySnackBar> | 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,17 +269,37 @@ 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 {
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|