kinosaal designer
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -11,6 +11,7 @@ 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
|
||||
@@ -39,6 +40,18 @@ const routes: Routes = [
|
||||
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 },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }[] = [
|
||||
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());
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
@@ -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,27 +326,29 @@ 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 });
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user