kinosaal designer

This commit is contained in:
2025-11-23 16:43:22 +01:00
parent 2ecaf2d526
commit 3a8fc2cc63
9 changed files with 526 additions and 169 deletions

View File

@@ -76,6 +76,7 @@ import { ConversionFailedComponent } from './conversion-failed/conversion-failed
import { PayForOrderComponent } from './pay-for-order/pay-for-order.component';
import { CancelOrderDialog } from './cancel-order/cancel-order.dialog';
import { PricelistComponent } from './pricelist/pricelist.component';
import { TheaterLayoutDesignerComponent } from './theater-layout-designer/theater-layout-designer.component';
@@ -129,6 +130,7 @@ import { PricelistComponent } from './pricelist/pricelist.component';
PayForOrderComponent,
CancelOrderDialog,
PricelistComponent,
TheaterLayoutDesignerComponent,
],
imports: [
AppRoutingModule,

View File

@@ -5,12 +5,13 @@ import { HomeComponent } from './home/home.component';
import { MainLayoutComponent } from './layouts/main-layout/main-layout.component';
import { MainComponent } from './main/main.component';
import { ScheduleComponent } from './schedule/schedule.component';
import { TheaterOverlayComponent} from './theater-overlay/theater-overlay.component';
import { TheaterOverlayComponent } from './theater-overlay/theater-overlay.component';
import { MovieImporterComponent } from './movie-importer/movie-importer.component';
import { AuthGuard } from './auth.guard';
import { PayForOrderComponent } from './pay-for-order/pay-for-order.component';
import { StatisticsComponent } from './statistics/statistics.component';
import { PricelistComponent } from './pricelist/pricelist.component';
import { TheaterLayoutDesignerComponent } from './theater-layout-designer/theater-layout-designer.component';
const routes: Routes = [
// Seiten ohne Layout
@@ -30,15 +31,27 @@ const routes: Routes = [
canActivate: [AuthGuard],
data: { roles: ['admin'] }, // Array von erlaubten Rollen. Derzeit gäbe es 'admin' und 'employee'
},
{ path: 'checkout/performance/:performanceId', component: TheaterOverlayComponent},
{ path: 'checkout/order/:orderId', component: TheaterOverlayComponent},
{ path: 'checkout/order', component: PayForOrderComponent},
{ path: 'checkout/performance/:performanceId', component: TheaterOverlayComponent },
{ path: 'checkout/order/:orderId', component: TheaterOverlayComponent },
{ path: 'checkout/order', component: PayForOrderComponent },
{
path: 'admin/statistics',
component: StatisticsComponent,
canActivate: [AuthGuard],
data: { roles: ['admin'] }, // Array von erlaubten Rollen. Derzeit gäbe es 'admin' und 'employee'
},
{
path: 'admin/designer',
component: TheaterLayoutDesignerComponent,
canActivate: [AuthGuard],
data: { roles: ['admin'] }, // Array von erlaubten Rollen. Derzeit gäbe es 'admin' und 'employee'
},
{
path: 'admin/designer/:hallId',
component: TheaterLayoutDesignerComponent,
canActivate: [AuthGuard],
data: { roles: ['admin'] }, // Array von erlaubten Rollen. Derzeit gäbe es 'admin' und 'employee'
},
{ path: 'prices', component: PricelistComponent },
],
},

View File

@@ -6,21 +6,22 @@ import {
OmdbSearch,
Bestellung,
Eintrittskarte,
StatisticsFilm, StatisticsVorstellung,
Sitzkategorie
StatisticsFilm,
StatisticsVorstellung,
Sitzkategorie,
Sitzreihe,
} from '@infinimotion/model-frontend';
import { HttpClient } from "@angular/common/http";
import { inject, Injectable } from "@angular/core";
import { Observable } from "rxjs";
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class HttpService {
private http = inject(HttpClient);
private baseUrl = 'https://infinimotion.de/api/';
/* Bestellung APIs */
/* POST /api/bestellung/filter */
@@ -39,11 +40,16 @@ export class HttpService {
}
/* POST /api/order-transaction/create */
saveAddOrder(req: {order:Bestellung, tickets:Eintrittskarte[]}): Observable<{order:Bestellung, tickets:Eintrittskarte[]}> {
return this.http.post<{order: Bestellung, tickets: Eintrittskarte[]}>(`${this.baseUrl}order-transaction/create`, req);
saveAddOrder(req: {
order: Bestellung;
tickets: Eintrittskarte[];
}): Observable<{ order: Bestellung; tickets: Eintrittskarte[] }> {
return this.http.post<{ order: Bestellung; tickets: Eintrittskarte[] }>(
`${this.baseUrl}order-transaction/create`,
req
);
}
/* Eintrittskarte APIs */
/* POST /api/eintrittskarte/filter */
@@ -51,7 +57,6 @@ export class HttpService {
return this.http.post<Eintrittskarte[]>(`${this.baseUrl}eintrittskarte/filter`, filter);
}
/* Kinosaal APIs */
/* GET /api/kinosaal */
@@ -64,7 +69,6 @@ export class HttpService {
return this.http.post<Kinosaal>(`${this.baseUrl}kinosaal`, kinosaal);
}
/* Vorstellung APIs */
/* GET /api/vorstellung/{id} */
@@ -77,7 +81,6 @@ export class HttpService {
return this.http.post<Vorstellung[]>(`${this.baseUrl}vorstellung/filter`, filter);
}
/* Film APIs */
/* GET /api/film */
@@ -90,51 +93,70 @@ export class HttpService {
return this.http.post<Film[]>(`${this.baseUrl}film/filter`, filter);
}
/* Show-Seats APIs */
/* GET /api/show-seats/{show} */
getSeatsByShowId(show: number): Observable<{ seats: Sitzplatz[], reserved: Sitzplatz[], booked: Sitzplatz[] }> {
getSeatsByShowId(
show: number
): Observable<{ seats: Sitzplatz[]; reserved: Sitzplatz[]; booked: Sitzplatz[] }> {
return this.http.get<{
seats: Sitzplatz[],
reserved: Sitzplatz[],
booked: Sitzplatz[]
seats: Sitzplatz[];
reserved: Sitzplatz[];
booked: Sitzplatz[];
}>(`${this.baseUrl}show-seats/${show}`);
}
/* Sitzplatz APIs */
/* POST /api/seat/filter */
getSeatsByHallId(hall: number): Observable<Sitzplatz[]> {
return this.http.post<Sitzplatz[]>(`${this.baseUrl}sitzplatz/filter`, [
`eq;row.hall.id;int;${hall}`,
]);
}
/* POST /api/sitzplatz */
createSeat(seat: Sitzplatz): Observable<Sitzplatz> {
return this.http.post<Sitzplatz>(`${this.baseUrl}sitzplatz`, seat);
}
/* Sitzreihe APIs */
/* POST /api/sitzreihe */
createSeatRow(row: Sitzreihe): Observable<Sitzreihe> {
return this.http.post<Sitzreihe>(`${this.baseUrl}sitzreihe`, row);
}
/* Movie Importer APIs */
/* GET /api/importer/search */
searchMovie(query: string): Observable<OmdbSearch> {
return this.http.get<OmdbSearch>(`${this.baseUrl}importer/search`, {
params: {title: query}
params: { title: query },
});
}
/* POST /api/importer/import */
importMovie(imdbId: string): Observable<Film> {
return this.http.post<Film>(`${this.baseUrl}importer/import?id=${imdbId}`, {})
return this.http.post<Film>(`${this.baseUrl}importer/import?id=${imdbId}`, {});
}
/* Statistics APIs */
/* GET /api/statistics/movies */
getMovieStatistics(): Observable<StatisticsFilm[]> {
return this.http.get<StatisticsFilm[]>(`${this.baseUrl}statistics/movies`)
return this.http.get<StatisticsFilm[]>(`${this.baseUrl}statistics/movies`);
}
/* GET /api/statistics/shows */
getShowStatistics(): Observable<StatisticsVorstellung[]> {
return this.http.get<StatisticsVorstellung[]>(`${this.baseUrl}statistics/shows`)
return this.http.get<StatisticsVorstellung[]>(`${this.baseUrl}statistics/shows`);
}
/* Sitzkategorie APIs */
/* GET /api/sitzkategorie */
getSeatCategories(): Observable<Sitzkategorie[]> {
return this.http.get<Sitzkategorie[]>(`${this.baseUrl}sitzkategorie`)
return this.http.get<Sitzkategorie[]>(`${this.baseUrl}sitzkategorie`);
}
}

View File

@@ -5,18 +5,19 @@ import { Component, inject, computed, OnInit } from '@angular/core';
selector: 'app-navbar',
standalone: false,
templateUrl: './navbar.component.html',
styleUrl: './navbar.component.css'
styleUrl: './navbar.component.css',
})
export class NavbarComponent {
navItems: { label:string, path:string }[] = [
{label: 'Programm', path: '/schedule'},
{label: 'Preise', path: '/prices'},
{label: 'Bezahlen', path: '/checkout/order'},
{label: 'Film importieren', path: '/admin/movie-importer'},
{label: 'Statistiken', path: '/admin/statistics'},
]
navItems: { label: string; path: string }[] = [
{ label: 'Programm', path: '/schedule' },
{ label: 'Preise', path: '/prices' },
{ label: 'Bezahlen', path: '/checkout/order' },
{ label: 'Film importieren', path: '/admin/movie-importer' },
{ label: 'Statistiken', path: '/admin/statistics' },
{ label: 'Saal-Designer', path: '/admin/designer' },
];
private auth = inject(AuthService)
private auth = inject(AuthService);
currentUser = computed(() => this.auth.user());

View File

@@ -1,11 +1,12 @@
import { computed, Injectable, signal } from '@angular/core';
import { Sitzplatz } from '@infinimotion/model-frontend';
import { Sitzplatz, Sitzreihe } from '@infinimotion/model-frontend';
import { TheaterLayoutDesignerComponent } from './theater-layout-designer/theater-layout-designer.component';
import { TheaterSeatState } from './model/theater-seat-state.model';
@Injectable({
providedIn: 'root',
})
export class SelectedSeatsService {
private selectedSeatsSignal = signal<Sitzplatz[]>([]);
private seatIsSelectableSignal = signal(true);
private committedSignal = signal(false);
@@ -23,20 +24,26 @@ export class SelectedSeatsService {
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));
readonly totalPrice = computed(() =>
this.selectedSeats().reduce((sum, seat) => sum + seat.row.category.price, 0)
);
pushSelectedSeat(selectedSeat: Sitzplatz): void {
this.selectedSeatsSignal.update(seats => [...seats, selectedSeat]);
if (selectedSeat.id < 0) {
TheaterLayoutDesignerComponent.interceptSeatSelection(selectedSeat);
return;
}
this.selectedSeatsSignal.update((seats) => [...seats, selectedSeat]);
this.hadConflictSignal.set(false);
}
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.hadConflictSignal.set(false);
}
getSeatsByCategory(categoryId: number): Sitzplatz[] {
return this.selectedSeats().filter(seat => seat.row.category.id === categoryId);
return this.selectedSeats().filter((seat) => seat.row.category.id === categoryId);
}
clearSelection(): void {
@@ -47,7 +54,7 @@ export class SelectedSeatsService {
this.hadConflictSignal.set(false);
}
getSeatIsSelectable(): boolean{
getSeatIsSelectable(): boolean {
return this.seatIsSelectable();
}
@@ -75,11 +82,11 @@ export class SelectedSeatsService {
}
toggleDebug(): void {
this.debugSignal.update(debug => !debug);
this.debugSignal.update((debug) => !debug);
}
isSeatSelected(seatId: number): boolean {
return this.selectedSeats().some(seat => seat.id === seatId);
return this.selectedSeats().some((seat) => seat.id === seatId);
}
setConflict(value: boolean): void {

View File

@@ -0,0 +1,89 @@
@if(getHallId() >= 0) {
<div
class="m-auto w-200 h-10 bg-gray-200 mb-22 cursor-pointer"
style="clip-path: polygon(0% 0%, 100% 0%, 90% 100%, 10% 100%)"
>
<p class="flex justify-center text-lg font-bold p-1.5">Leinwand</p>
</div>
<div class="mb-5">
@for (row of seatsPerRow(); track $index) {
<div class="flex items-center justify-between">
<!-- Speaker -->
<div class="shrink-0 pl-20">
@if ($index % 4 === 0) {
<mat-icon
class="material-symbols-outlined opacity-25"
style="font-size: 30px; width: 30px; height: 30px"
>
speaker
</mat-icon>
} @if ($index % 4 === 2) {
<mat-icon
class="material-symbols-outlined opacity-25"
style="font-size: 30px; width: 30px; height: 30px"
>
wall_lamp
</mat-icon>
}
</div>
<!-- Sitzreihe -->
<app-seat-row class="flex justify-center" [rowSeatList]="row"></app-seat-row>
<!-- Speaker -->
<div class="shrink-0 pr-20">
@if ($index % 4 === 0) {
<mat-icon
class="material-symbols-outlined opacity-25 mirrored"
style="font-size: 30px; width: 30px; height: 30px"
>
speaker
</mat-icon>
} @if ($index % 4 === 2) {
<mat-icon
class="material-symbols-outlined opacity-25 mirrored"
style="font-size: 30px; width: 30px; height: 30px"
>
wall_lamp
</mat-icon>
}
</div>
</div>
}
<app-seat-row class="flex justify-center" [rowSeatList]="addRow"></app-seat-row>
<br />
<div class="flex justify-center">
<button mat-button matButton="filled" class="w-1/4" (click)="save()">Speichern</button>
</div>
</div>
} @else {
<br />
<div class="w-100 m-auto middle">
<form class="order-search-form w-full" (ngSubmit)="navigate()">
<div class="flex items-center space-x-4">
<mat-form-field class="w-full" subscriptSizing="dynamic">
<mat-label>Kinosaal-Name</mat-label>
<input
class="w-full"
type="text"
matInput
placeholder="Galactus"
autocomplete="off"
name="hallName"
[(ngModel)]="hallName"
/>
</mat-form-field>
</div>
<button
mat-button
class="w-100 mt-2"
matButton="filled"
color="accent"
[disabled]="hallName.length == 0"
type="submit"
>
Bearbeiten oder Erstellen
</button>
</form>
</div>
}

View File

@@ -0,0 +1,185 @@
import { Component, inject, OnInit } from '@angular/core';
import { Kinosaal, Sitzkategorie, Sitzplatz, Sitzreihe } from '@infinimotion/model-frontend';
import { TheaterSeatState } from '../model/theater-seat-state.model';
import { SelectedSeatsService } from '../selected-seats.service';
import { HttpService } from '../http.service';
import { first, firstValueFrom } from 'rxjs';
import { TheaterOverlayComponent } from '../theater-overlay/theater-overlay.component';
import { ActivatedRoute, Router } from '@angular/router';
import { LoadingService } from '../loading.service';
@Component({
selector: 'app-theater-layout-designer',
standalone: false,
templateUrl: './theater-layout-designer.component.html',
styleUrl: './theater-layout-designer.component.css',
})
export class TheaterLayoutDesignerComponent implements OnInit {
public static seatsPerRow: { seat: Sitzplatz | null; state: TheaterSeatState | null }[][] = [];
addRow: { seat: Sitzplatz | null; state: TheaterSeatState | null }[] = [];
protected selectedSeatsService = inject(SelectedSeatsService);
protected http = inject(HttpService);
protected route = inject(ActivatedRoute);
protected loading = inject(LoadingService);
protected router = inject(Router);
getHallId() {
return parseInt(this.route.snapshot.paramMap.get('hallId') ?? '-1');
}
async ngOnInit() {
let hallId = this.getHallId();
if (hallId == -1) {
return;
}
let seats = await firstValueFrom(this.http.getSeatsByHallId(hallId));
let rows: { seat: Sitzplatz | null; state: TheaterSeatState | null }[][] = [];
const categoryMap = new Map<number, Sitzkategorie>();
seats.forEach((seat) => {
if (!rows[seat.row.position]) {
rows[seat.row.position] = [];
}
rows[seat.row.position].push({
seat: seat,
state: TheaterSeatState.RESERVED,
});
if (seat.row.category && !categoryMap.has(seat.row.category.id)) {
categoryMap.set(seat.row.category.id, seat.row.category);
}
});
TheaterLayoutDesignerComponent.seatsPerRow = TheaterOverlayComponent.fillSeatsPerRow(rows);
let rowCounter = 0;
TheaterLayoutDesignerComponent.seatsPerRow.forEach(
(row) => (rowCounter = TheaterLayoutDesignerComponent.fillRowWithSettings(rowCounter, row))
);
let realCategories = await firstValueFrom(this.http.getSeatCategories());
this.addRow = realCategories.map((category) => {
return {
seat: {
id: -2,
position: 1,
row: {
id: 0,
hall: { id: hallId } as Kinosaal,
category: category,
position: -1,
},
},
state: TheaterSeatState.AVAILABLE,
};
});
}
seatsPerRow() {
return TheaterLayoutDesignerComponent.seatsPerRow;
}
public static fillRowWithSettings(
rowCounter: number,
row: {
seat: Sitzplatz | null;
state: TheaterSeatState | null;
}[]
) {
if (!row[0].seat) {
return rowCounter + 1;
}
let categories: Sitzkategorie[] = [
{ id: 0, name: '', price: -1, icon: 'check_box_outline_blank' },
{ id: 1, name: '', price: -1, icon: 'add' },
];
categories.forEach((category) => {
row.push({
seat: {
id: -1,
position: row.length,
row: {
id: -1,
hall: undefined as unknown as Kinosaal,
position: rowCounter,
category: category,
},
},
state: TheaterSeatState.AVAILABLE,
});
});
return rowCounter + 1;
}
static addSeatDesigner(selectedSeat: Sitzplatz): void {
let row = TheaterLayoutDesignerComponent.seatsPerRow[selectedSeat.row.position];
if (selectedSeat.row.category.id == 0) {
row.splice(row.length - 2, 0, {
seat: null,
state: null,
});
} else {
row.splice(row.length - 2, 0, {
seat: { id: 0, position: row.length - 1, row: row[0].seat!.row },
state: TheaterSeatState.RESERVED,
});
}
}
static addRowDesigner(selectedSeat: Sitzplatz): void {
let rows = TheaterLayoutDesignerComponent.seatsPerRow;
let firstSeat = {
seat: { id: 0, position: 1, row: { ...selectedSeat.row } },
state: TheaterSeatState.RESERVED,
};
firstSeat.seat!.row.position = rows.length + 1;
rows.push([firstSeat]);
TheaterLayoutDesignerComponent.fillRowWithSettings(rows.length - 1, rows[rows.length - 1]);
}
public static interceptSeatSelection(selectedSeat: Sitzplatz) {
if (selectedSeat.id == -1) {
this.addSeatDesigner(selectedSeat);
} else if (selectedSeat.id == -2) {
this.addRowDesigner(selectedSeat);
} else {
console.log('Fehler: unerwartete Seat-ID: ' + selectedSeat.id);
}
}
async save() {
this.loading.show();
for (let row of TheaterLayoutDesignerComponent.seatsPerRow) {
let seatRow;
if (!row[0].seat) {
return;
} else if (row[0].seat.row.id == 0) {
seatRow = await firstValueFrom(this.http.createSeatRow(row[0].seat.row));
} else {
seatRow = row[0].seat.row;
}
for (const { seat, state } of row) {
if (seat != null && seat.id == 0) {
seat.row = seatRow;
let createdSeat = await firstValueFrom(this.http.createSeat(seat));
seat.id = createdSeat.id;
}
}
}
this.loading.showError('Kinosaal erfolgreich aktualisiert');
}
hallName: string = '';
async navigate() {
let halls = await firstValueFrom(this.http.getAllKinosaal());
let hall = halls.filter((hall) => hall.name == this.hallName);
if (hall.length == 0) {
hall[0] = await firstValueFrom(this.http.addKinosaal({ name: this.hallName }));
}
this.router.navigate(['/admin/designer', hall[0].id]);
}
}

View File

@@ -1,8 +1,27 @@
import { Component, DestroyRef, inject, OnDestroy, OnInit, signal } from '@angular/core';
import { HttpService } from '../http.service';
import { LoadingService } from '../loading.service';
import { catchError, filter, finalize, forkJoin, from, fromEvent, interval, merge, of, startWith, switchMap, tap } from 'rxjs';
import { Bestellung, Eintrittskarte, Sitzkategorie, Sitzplatz, Vorstellung } from '@infinimotion/model-frontend';
import {
catchError,
filter,
finalize,
forkJoin,
from,
fromEvent,
interval,
merge,
of,
startWith,
switchMap,
tap,
} from 'rxjs';
import {
Bestellung,
Eintrittskarte,
Sitzkategorie,
Sitzplatz,
Vorstellung,
} from '@infinimotion/model-frontend';
import { TheaterSeatState } from '../model/theater-seat-state.model';
import { ActivatedRoute, Router } from '@angular/router';
import { SelectedSeatsService } from '../selected-seats.service';
@@ -16,7 +35,7 @@ const INACTIVITY_TIMEOUT_MS = 2 * 60 * 1000;
selector: 'app-theater-overlay',
standalone: false,
templateUrl: './theater-overlay.component.html',
styleUrl: './theater-overlay.component.css'
styleUrl: './theater-overlay.component.css',
})
export class TheaterOverlayComponent implements OnInit, OnDestroy {
private http = inject(HttpService);
@@ -36,10 +55,10 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
order: Bestellung | undefined;
blockedSeats: Sitzplatz[] | undefined;
seatCategories: Sitzkategorie[] = [];
snackBarRef: MatSnackBarRef<TextOnlySnackBar> | undefined
snackBarRef: MatSnackBarRef<TextOnlySnackBar> | undefined;
performance = signal<Vorstellung | undefined>(undefined);
seatsPerRow = signal<{ seat: Sitzplatz | null, state: TheaterSeatState | null }[][]>([]);
seatsPerRow = signal<{ seat: Sitzplatz | null; state: TheaterSeatState | null }[][]>([]);
private isPollingEnabled = signal(true);
private isInitialLoad = signal(true);
@@ -60,7 +79,7 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
this.loadExistingOrder(this.orderCode);
this.route.queryParams.subscribe(params => {
this.route.queryParams.subscribe((params) => {
if (params['action'] === 'cancel') {
this.resumeWithCancel = true;
}
@@ -75,7 +94,6 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
this.setupActivityTracking();
this.startAutoRefresh();
this.startInactivityCheck();
} else {
// Fallback
console.error('Ungültige Checkout-Route');
@@ -87,7 +105,7 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
this.isPollingEnabled.set(false);
console.info('[TheaterOverlay] Stopped auto-refresh polling');
if(this.snackBarRef) {
if (this.snackBarRef) {
this.snackBar.dismiss();
}
}
@@ -102,15 +120,13 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
fromEvent(document, 'touchstart')
);
events$.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
events$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
this.lastActivityTimestamp.set(Date.now());
});
fromEvent(document, 'visibilitychange').pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
fromEvent(document, 'visibilitychange')
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
if (!this.isStepTwoOrHigher()) {
if (document.hidden) {
console.info('[TheaterOverlay] Tab hidden - pausing polling');
@@ -128,11 +144,13 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
}
private startInactivityCheck() {
interval(10000).pipe(
interval(10000)
.pipe(
filter(() => this.isPollingEnabled()),
filter(() => !this.isStepTwoOrHigher()), // Kein Timeout ab Schritt 2
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
)
.subscribe(() => {
const inactiveDuration = Date.now() - this.lastActivityTimestamp();
if (inactiveDuration >= INACTIVITY_TIMEOUT_MS && !this.inactivityTimeoutReached()) {
@@ -149,7 +167,6 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
}
private showInactivitySnackBar() {
this.snackBarRef = this.snackBar.open(
'Sitzplatzaktuallisierung wegen Inaktivität gestoppt.',
'Fortsetzen',
@@ -157,7 +174,7 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
duration: 0,
panelClass: ['timeout-snackbar'],
horizontalPosition: 'center',
verticalPosition: 'bottom'
verticalPosition: 'bottom',
}
);
@@ -171,7 +188,8 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
private startAutoRefresh() {
console.info('[TheaterOverlay] Starting auto-refresh polling');
interval(POLLING_INTERVAL_MS).pipe(
interval(POLLING_INTERVAL_MS)
.pipe(
startWith(POLLING_INTERVAL_MS),
filter(() => this.isPollingEnabled()),
filter(() => !this.selectedSeatService.committed()),
@@ -181,7 +199,8 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
filter(() => !this.isStepTwoOrHigher()), // Nicht pollen ab Schritt 2
switchMap(() => this.loadPerformanceAndSeats()),
takeUntilDestroyed(this.destroyRef)
).subscribe();
)
.subscribe();
}
private loadPerformanceAndSeats() {
@@ -195,7 +214,7 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
return forkJoin({
performance: this.http.getPerformaceById(this.showId!),
seats: this.http.getSeatsByShowId(this.showId!)
seats: this.http.getSeatsByShowId(this.showId!),
}).pipe(
tap(({ performance, seats }) => {
this.performance.set(performance);
@@ -206,11 +225,11 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
const conflicts = this.getConflictingSeats(seats.reserved);
if (conflicts.length > 0) {
console.info('[TheaterOverlay] Conflicts! Updating shopping cart.');
conflicts.forEach(seat => this.selectedSeatService.removeSelectedSeat(seat));
conflicts.forEach((seat) => this.selectedSeatService.removeSelectedSeat(seat));
this.selectedSeatService.setConflict(true);
}
this.selectedSeatService.selectedSeats
this.selectedSeatService.selectedSeats;
}
this.blockedSeats = seats.reserved;
@@ -220,7 +239,7 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
this.loading.hide();
}
}),
catchError(err => {
catchError((err) => {
if (isInitial) {
this.loading.showError(err);
} else {
@@ -242,50 +261,55 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
this.loading.show();
const ticketFilter = [`eq;order.code;string;${orderCode}`];
this.http.getTicketsByFilter(ticketFilter).pipe(
switchMap(tickets => {
this.http
.getTicketsByFilter(ticketFilter)
.pipe(
switchMap((tickets) => {
this.tickets = tickets;
if (!tickets.length) {
return from(this.router.navigate(
['/checkout/order'],
{ queryParams: { error: 'invalid', code: orderCode } }
));
return from(
this.router.navigate(['/checkout/order'], {
queryParams: { error: 'invalid', code: orderCode },
})
);
}
this.order = tickets[0].order;
if (this.order.booked || this.order.cancelled) {
return from(this.router.navigate(
['/checkout/order'],
{ queryParams: { error: 'completed', code: orderCode } }
));
return from(
this.router.navigate(['/checkout/order'], {
queryParams: { error: 'completed', code: orderCode },
})
);
}
this.showId = this.tickets[0].show.id;
this.selectedSeatService.clearSelection();
this.tickets.forEach(t => this.selectedSeatService.pushSelectedSeat(t.seat));
this.tickets.forEach((t) => this.selectedSeatService.pushSelectedSeat(t.seat));
this.selectedSeatService.setSeatSelectable(false);
return this.loadPerformanceAndSeats();
}),
catchError(err => {
catchError((err) => {
console.error('Fehler beim Laden der Bestellung', err);
return from(this.router.navigate(
['/checkout/order'],
{ queryParams: { error: 'invalid', code: orderCode } }
));
return from(
this.router.navigate(['/checkout/order'], {
queryParams: { error: 'invalid', code: orderCode },
})
);
}),
finalize(() => {
this.loading.hide();
}),
takeUntilDestroyed(this.destroyRef)
).subscribe();
)
.subscribe();
}
private equalSeats(a: Sitzplatz[], b: Sitzplatz[]): boolean {
if (a === b) return true;
if (a == null || b == null) return false;
@@ -302,30 +326,32 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
}
private getConflictingSeats(blockedSeats: Sitzplatz[]): Sitzplatz[] {
const blockedIds = new Set(blockedSeats.map(bs => bs.id));
return this.selectedSeatService.selectedSeats().filter(
selectedSeat => blockedIds.has(selectedSeat.id)
);
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
private converter(resp: { seats: Sitzplatz[]; reserved: Sitzplatz[]; booked: Sitzplatz[] }): {
seat: Sitzplatz | null;
state: TheaterSeatState | null;
}[][] {
let rows: { seat: Sitzplatz | null, state: TheaterSeatState | null }[][] = [];
let rows: { seat: Sitzplatz | null; state: TheaterSeatState | null }[][] = [];
const categoryMap = new Map<number, Sitzkategorie>();
// Sitzplätze sammeln
resp.seats.forEach(seat => {
resp.seats.forEach((seat) => {
if (!rows[seat.row.position]) {
rows[seat.row.position] = [];
}
let state = resp.booked.find(other => other.id == seat.id) ? TheaterSeatState.BOOKED
: resp.reserved.find(other => other.id == seat.id) ? TheaterSeatState.RESERVED
let state = resp.booked.find((other) => other.id == seat.id)
? TheaterSeatState.BOOKED
: resp.reserved.find((other) => other.id == seat.id)
? TheaterSeatState.RESERVED
: TheaterSeatState.AVAILABLE;
rows[seat.row.position].push({seat: seat, state: state});
rows[seat.row.position].push({ seat: seat, state: state });
if (seat.row.category && !categoryMap.has(seat.row.category.id)) {
categoryMap.set(seat.row.category.id, seat.row.category);
@@ -334,24 +360,35 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
this.seatCategories = Array.from(categoryMap.values()).sort((a, b) => a.id - b.id);
rows = rows.filter(row => row && row.length > 0).sort((a, b) => a[0].seat!.row.position - b[0].seat!.row.position);
return TheaterOverlayComponent.fillSeatsPerRow(rows);
}
public static fillSeatsPerRow(
rows: {
seat: Sitzplatz | null;
state: TheaterSeatState | null;
}[][]
) {
rows = rows
.filter((row) => row && row.length > 0)
.sort((a, b) => a[0].seat!.row.position - b[0].seat!.row.position);
if (rows.length === 0) {
return [];
}
// Leere Plätze auffüllen
const filledSeats: { seat: Sitzplatz | null, state: TheaterSeatState | null }[][] = [];
const filledSeats: { seat: Sitzplatz | null; state: TheaterSeatState | null }[][] = [];
rows.forEach(row => {
row.sort((a, b) => a.seat!.position - b.seat!.position)
rows.forEach((row) => {
row.sort((a, b) => a.seat!.position - b.seat!.position);
const minPos = row[0].seat!.position;
const maxPos = row[row.length - 1].seat!.position;
const filledRow: { seat: Sitzplatz | null, state: TheaterSeatState | null }[] = [];
const filledRow: { seat: Sitzplatz | null; state: TheaterSeatState | null }[] = [];
for (let pos = minPos; pos <= maxPos; pos++) {
const existingSeat = row.find(s => s.seat!.position === pos);
const existingSeat = row.find((s) => s.seat!.position === pos);
if (existingSeat) {
filledRow.push(existingSeat);
} else {
@@ -365,11 +402,14 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
// Leere Reihen auffüllen
const minRowPos = rows[0][0].seat!.row.position;
const maxRowPos = rows[rows.length - 1][0].seat!.row.position;
const filledRows: { seat: Sitzplatz | null, state: TheaterSeatState | null }[][] = [];
const filledRows: { seat: Sitzplatz | null; state: TheaterSeatState | null }[][] = [];
let processedIndex = 0;
for (let rowPos = minRowPos; rowPos <= maxRowPos; rowPos++) {
if (processedIndex < filledSeats.length && filledSeats[processedIndex][0].seat!.row.position === rowPos) {
if (
processedIndex < filledSeats.length &&
filledSeats[processedIndex][0].seat!.row.position === rowPos
) {
filledRows.push(filledSeats[processedIndex]);
processedIndex++;
} else {
@@ -412,5 +452,3 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
}
}
}