Add inactivity timeout and polling control to seat overlay
Introduces user inactivity tracking and timeout for seat polling in TheaterOverlayComponent. Polling is paused after 2 minutes of inactivity or when the user advances past step 1, with a snackbar notification and resume option. Refactors polling logic, adds step change event to OrderComponent, and updates custom theme for snackbar styling.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
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, inject, input, signal } from '@angular/core';
|
||||
import { Component, computed, DestroyRef, EventEmitter, inject, input, output, Output, signal, ViewChild } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { StepperSelectionEvent } from '@angular/cdk/stepper';
|
||||
import { HttpService } from '../http.service';
|
||||
@@ -29,12 +29,15 @@ export class OrderComponent {
|
||||
private fb = inject(FormBuilder);
|
||||
private httpService = inject(HttpService);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
readonly loadingService = inject(LoadingService);
|
||||
readonly selectedSeatsService = inject(SelectedSeatsService);
|
||||
|
||||
performance = input<Vorstellung>();
|
||||
seatCategories = input.required<Sitzkategorie[]>();
|
||||
|
||||
stepChanged = output<number>();
|
||||
|
||||
paymentForm!: FormGroup;
|
||||
dataForm!: FormGroup;
|
||||
|
||||
@@ -107,6 +110,8 @@ export class OrderComponent {
|
||||
onStepChange(event: StepperSelectionEvent) {
|
||||
this.submitted.set(false);
|
||||
this.selectedSeatsService.setSeatSelectable(event.selectedIndex === 0);
|
||||
|
||||
this.stepChanged.emit(event.selectedIndex);
|
||||
}
|
||||
|
||||
reservationClicked() {
|
||||
|
||||
@@ -18,6 +18,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-order class="mt-10 mr-30 w-3/10" [performance]="performance" [seatCategories]="seatCategories"></app-order>
|
||||
<app-order (stepChanged)="setStepTwoOrHigher($event >= 1)" class="mt-10 mr-30 w-3/10" [performance]="performance" [seatCategories]="seatCategories"></app-order>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -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,16 +269,36 @@ 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 {
|
||||
this.isPollingEnabled.set(true);
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user