Merge branch 'main' of git.infinimotion.de:infinimotion/frontend
This commit is contained in:
@@ -57,7 +57,6 @@ import { MovieImportNoSearchResultComponent } from './movie-import-no-search-res
|
||||
import { MovieImportSearchInfoComponent } from './movie-import-search-info/movie-import-search-info.component';
|
||||
import { LoginDialog } from './login/login.dialog';
|
||||
import { PerformanceInfoComponent } from './performance-info/performance-info.component';
|
||||
import { ShoppingCartComponent } from './shopping-cart/shopping-cart.component';
|
||||
import { OrderComponent } from './order/order.component';
|
||||
import { SeatSelectionComponent } from './seat-selection/seat-selection.component';
|
||||
import { NoSeatsInHallComponent } from './no-seats-in-hall/no-seats-in-hall.component';
|
||||
@@ -69,6 +68,12 @@ import { TicketSmallComponent } from './ticket-small/ticket-small.component';
|
||||
import { TicketListComponent } from './ticket-list/ticket-list.component';
|
||||
import { StatisticsComponent } from './statistics/statistics.component';
|
||||
import { ZoomWarningComponent } from './zoom-warning/zoom-warning.component';
|
||||
import { SelectionConflictInfoComponent } from './selection-conflict-info/selection-conflict-info.component';
|
||||
import { CancellationSuccessComponent } from './cancellation-success/cancellation-success.component';
|
||||
import { CancellationFailedComponent } from './cancellation-failed/cancellation-failed.component';
|
||||
import { ConversionFailedComponent } from './conversion-failed/conversion-failed.component';
|
||||
import { PayForOrderComponent } from './pay-for-order/pay-for-order.component';
|
||||
import { CancelOrderDialog } from './cancel-order/cancel-order.dialog';
|
||||
import { PricelistComponent } from './pricelist/pricelist.component';
|
||||
|
||||
|
||||
@@ -104,7 +109,6 @@ import { PricelistComponent } from './pricelist/pricelist.component';
|
||||
MovieImportSearchInfoComponent,
|
||||
LoginDialog,
|
||||
PerformanceInfoComponent,
|
||||
ShoppingCartComponent,
|
||||
OrderComponent,
|
||||
SeatSelectionComponent,
|
||||
NoSeatsInHallComponent,
|
||||
@@ -116,6 +120,12 @@ import { PricelistComponent } from './pricelist/pricelist.component';
|
||||
TicketListComponent,
|
||||
StatisticsComponent,
|
||||
ZoomWarningComponent,
|
||||
SelectionConflictInfoComponent,
|
||||
CancellationSuccessComponent,
|
||||
CancellationFailedComponent,
|
||||
ConversionFailedComponent,
|
||||
PayForOrderComponent,
|
||||
CancelOrderDialog,
|
||||
PricelistComponent,
|
||||
],
|
||||
imports: [
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ScheduleComponent } from './schedule/schedule.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';
|
||||
|
||||
@@ -29,7 +30,9 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
data: { roles: ['admin'] }, // Array von erlaubten Rollen. Derzeit gäbe es 'admin' und 'employee'
|
||||
},
|
||||
{ path: 'performance/:performanceId/checkout', component: TheaterOverlayComponent},
|
||||
{ path: 'checkout/performance/:performanceId', component: TheaterOverlayComponent},
|
||||
{ path: 'checkout/order/:orderId', component: TheaterOverlayComponent},
|
||||
{ path: 'checkout/order', component: PayForOrderComponent},
|
||||
{
|
||||
path: 'admin/statistics',
|
||||
component: StatisticsComponent,
|
||||
|
||||
3
src/app/cancel-order/cancel-order.dialog.css
Normal file
3
src/app/cancel-order/cancel-order.dialog.css
Normal file
@@ -0,0 +1,3 @@
|
||||
button {
|
||||
min-width: 100px;
|
||||
}
|
||||
10
src/app/cancel-order/cancel-order.dialog.html
Normal file
10
src/app/cancel-order/cancel-order.dialog.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<h2 mat-dialog-title>Möchten Sie Ihre Bestellung wirklich stornieren?</h2>
|
||||
|
||||
<mat-dialog-content class="min-w-[400px]">
|
||||
<p class="text-sm text-gray-600 mb-2">Nach der Stornierung verlieren Sie Ihr Reservierungsrecht und die Sitzplätze können von anderen Kunden in Anspruch genommen werden. Dieser Prozess kann nicht rückgängig gemacht werden.</p>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="justify-end gap-2">
|
||||
<button mat-stroked-button color="warn" (click)="cancel()">Abbrechen</button>
|
||||
<button mat-flat-button color="primary" (click)="submit()">Stornieren</button>
|
||||
</mat-dialog-actions>
|
||||
22
src/app/cancel-order/cancel-order.dialog.ts
Normal file
22
src/app/cancel-order/cancel-order.dialog.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { MatDialogRef } from '@angular/material/dialog';
|
||||
|
||||
@Component({
|
||||
selector: 'app-cancel-order',
|
||||
standalone: false,
|
||||
templateUrl: './cancel-order.dialog.html',
|
||||
styleUrl: './cancel-order.dialog.css',
|
||||
})
|
||||
export class CancelOrderDialog {
|
||||
constructor(
|
||||
private dialogRef: MatDialogRef<CancelOrderDialog>,
|
||||
) {}
|
||||
|
||||
submit(): void {
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="bg-red-100 text-red-500 rounded-md shadow-sm w-full h-fit p-6 py-8 items-center justify-center flex flex-col space-y-2">
|
||||
<mat-icon class="material-symbols-outlined mb-5" style="font-size: 50px; width: 50px; height: 50px">
|
||||
warning
|
||||
</mat-icon>
|
||||
<h1 class="text-xl font-bold">Stornierung fehlgeschlagen!</h1>
|
||||
<p class="text-center">{{ infoText }}</p>
|
||||
|
||||
<button mat-button type="button" matButton="filled" class="error-button mt-4 w-80" (click)="retry.emit()">Erneut versuchen</button>
|
||||
<button routerLink="/order" type="button" mat-button matButton="outlined" color="accent" class="error-button w-80 mt-1">Zurück zur Codeeingabe</button>
|
||||
</div>
|
||||
|
||||
21
src/app/cancellation-failed/cancellation-failed.component.ts
Normal file
21
src/app/cancellation-failed/cancellation-failed.component.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Component, input, output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-cancellation-failed',
|
||||
standalone: false,
|
||||
templateUrl: './cancellation-failed.component.html',
|
||||
styleUrl: './cancellation-failed.component.css',
|
||||
})
|
||||
export class CancellationFailedComponent {
|
||||
moreThanOne = input<boolean>(false);
|
||||
|
||||
retry = output<void>();
|
||||
|
||||
infoText!: string;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.infoText = this.moreThanOne()?
|
||||
'Leider konnten Ihre Sitzplätze nicht storniert werden. Möglicherweise wurden die Tickets bereits bezahlt oder storniert.' :
|
||||
'Leider konnte Ihr Sitzplatz nicht storniert werden. Möglicherweise wurde das Ticket bereits bezahlt oder storniert.';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="bg-indigo-100 rounded-md shadow-sm w-full h-fit p-6 py-8 items-center justify-center flex flex-col space-y-2">
|
||||
<mat-icon class="material-symbols-outlined mb-5" style="font-size: 50px; width: 50px; height: 50px">
|
||||
task_alt
|
||||
</mat-icon>
|
||||
<h1 class="text-xl font-bold">Stornierung erfolgreich!</h1>
|
||||
<p class="text-center">{{ infoText }}</p>
|
||||
|
||||
<button routerLink="/schedule" type="button" mat-button matButton="filled" color="accent" class="w-80 mt-4">Zur Programmauswahl</button>
|
||||
<button type="button" mat-button matButton="outlined" class="w-80 mt-1" (click)="navigate()">Neue Tickets kaufen</button>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Component, inject, input } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-cancellation-success',
|
||||
standalone: false,
|
||||
templateUrl: './cancellation-success.component.html',
|
||||
styleUrl: './cancellation-success.component.css',
|
||||
})
|
||||
export class CancellationSuccessComponent {
|
||||
performanceId = input.required<number>();
|
||||
moreThanOne = input<boolean>(false);
|
||||
|
||||
router = inject(Router);
|
||||
|
||||
infoText!: string;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.infoText = this.moreThanOne()?
|
||||
'Ihre Sitzplätze wurden erfolgreich storniert und stehen wieder zur Buchung zur Verfügnug.' :
|
||||
'Ihr Sitzplatz wurden erfolgreich storniert und steht wieder zur Buchung zur Verfügnug.';
|
||||
}
|
||||
|
||||
navigate() {
|
||||
window.location.href = `/checkout/performance/${this.performanceId()}`;
|
||||
}
|
||||
}
|
||||
11
src/app/conversion-failed/conversion-failed.component.html
Normal file
11
src/app/conversion-failed/conversion-failed.component.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<div class="bg-red-100 text-red-500 rounded-md shadow-sm w-full h-fit p-6 py-8 items-center justify-center flex flex-col space-y-2">
|
||||
<mat-icon class="material-symbols-outlined mb-5" style="font-size: 50px; width: 50px; height: 50px">
|
||||
warning
|
||||
</mat-icon>
|
||||
<h1 class="text-xl font-bold">Kauf fehlgeschlagen!</h1>
|
||||
<p class="text-center">{{ infoText }}</p>
|
||||
|
||||
<button mat-button type="button" matButton="filled" class="error-button mt-4 w-80" (click)="retry.emit()">Erneut versuchen</button>
|
||||
<button routerLink="/order" type="button" mat-button matButton="outlined" color="accent" class="error-button w-80 mt-1">Zurück zur Codeeingabe</button>
|
||||
</div>
|
||||
|
||||
21
src/app/conversion-failed/conversion-failed.component.ts
Normal file
21
src/app/conversion-failed/conversion-failed.component.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Component, input, output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-conversion-failed',
|
||||
standalone: false,
|
||||
templateUrl: './conversion-failed.component.html',
|
||||
styleUrl: './conversion-failed.component.css',
|
||||
})
|
||||
export class ConversionFailedComponent {
|
||||
moreThanOne = input<boolean>(false);
|
||||
|
||||
retry = output<void>();
|
||||
|
||||
infoText!: string;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.infoText = this.moreThanOne()?
|
||||
'Leider konnten Ihre Sitzplätze nicht bezahlt werden. Möglicherweise wurden die Tickets bereits storniert.' :
|
||||
'Leider konnte Ihr Sitzplatz nicht bezahlt werden. Möglicherweise wurde das Ticket bereits storniert.';
|
||||
}
|
||||
}
|
||||
@@ -23,16 +23,6 @@ export class HttpService {
|
||||
|
||||
/* Bestellung APIs */
|
||||
|
||||
/* GET /api/bestellung/{id} */
|
||||
getAllOrder(id: number): Observable<Bestellung[]> {
|
||||
return this.http.get<Bestellung[]>(`${this.baseUrl}bestellung`);
|
||||
}
|
||||
|
||||
/* GET /api/bestellung/{id} */
|
||||
getOrderById(id: number): Observable<Bestellung> {
|
||||
return this.http.get<Bestellung>(`${this.baseUrl}bestellung/${id}`);
|
||||
}
|
||||
|
||||
/* POST /api/bestellung/filter */
|
||||
getOrdersByFilter(filter: string[]): Observable<Bestellung[]> {
|
||||
return this.http.post<Bestellung[]>(`${this.baseUrl}bestellung/filter`, filter);
|
||||
@@ -43,17 +33,11 @@ export class HttpService {
|
||||
return this.http.post<Bestellung>(`${this.baseUrl}bestellung`, order);
|
||||
}
|
||||
|
||||
/* PUT /api/bestellung/{id} */
|
||||
updateOrder(id: number, order: Partial<Bestellung>): Observable<Bestellung> {
|
||||
return this.http.put<Bestellung>(`${this.baseUrl}bestellung/${id}`, order);
|
||||
/* PUT /api/bestellung */
|
||||
updateOrder(order: Partial<Bestellung>): Observable<Bestellung> {
|
||||
return this.http.put<Bestellung>(`${this.baseUrl}bestellung`, order);
|
||||
}
|
||||
|
||||
/* DELETE /api/bestellung/{id} */
|
||||
deleteOrder(id: number): Observable<void> {
|
||||
return this.http.delete<void>(`${this.baseUrl}bestellung/${id}`);
|
||||
}
|
||||
|
||||
|
||||
/* 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);
|
||||
@@ -62,36 +46,11 @@ export class HttpService {
|
||||
|
||||
/* Eintrittskarte APIs */
|
||||
|
||||
/* GET /api/eintrittskarte/{id} */
|
||||
getAllTickets(id: number): Observable<Eintrittskarte[]> {
|
||||
return this.http.get<Eintrittskarte[]>(`${this.baseUrl}eintrittskarte`);
|
||||
}
|
||||
|
||||
/* GET /api/eintrittskarte/{id} */
|
||||
getTicketById(id: number): Observable<Eintrittskarte> {
|
||||
return this.http.get<Eintrittskarte>(`${this.baseUrl}eintrittskarte/${id}`);
|
||||
}
|
||||
|
||||
/* POST /api/eintrittskarte/filter */
|
||||
getTicketsByFilter(filter: string[]): Observable<Eintrittskarte[]> {
|
||||
return this.http.post<Eintrittskarte[]>(`${this.baseUrl}eintrittskarte/filter`, filter);
|
||||
}
|
||||
|
||||
/* POST /api/eintrittskarte */
|
||||
addTicket(order: Omit<Eintrittskarte, 'id'>): Observable<Eintrittskarte> {
|
||||
return this.http.post<Eintrittskarte>(`${this.baseUrl}eintrittskarte`, order);
|
||||
}
|
||||
|
||||
/* PUT /api/eintrittskarte/{id} */
|
||||
updateTicket(id: number, order: Partial<Eintrittskarte>): Observable<Eintrittskarte> {
|
||||
return this.http.put<Eintrittskarte>(`${this.baseUrl}eintrittskarte/${id}`, order);
|
||||
}
|
||||
|
||||
/* DELETE /api/eintrittskarte/{id} */
|
||||
deleteTicket(id: number): Observable<void> {
|
||||
return this.http.delete<void>(`${this.baseUrl}eintrittskarte/${id}`);
|
||||
}
|
||||
|
||||
|
||||
/* Kinosaal APIs */
|
||||
|
||||
@@ -100,34 +59,14 @@ export class HttpService {
|
||||
return this.http.get<Kinosaal[]>(`${this.baseUrl}kinosaal`);
|
||||
}
|
||||
|
||||
/* GET /api/kinosaal/{id} */
|
||||
getKinosaalById(id: number): Observable<Kinosaal> {
|
||||
return this.http.get<Kinosaal>(`${this.baseUrl}kinosaal/${id}`);
|
||||
}
|
||||
|
||||
/* POST /api/kinosaal */
|
||||
addKinosaal(kinosaal: Omit<Kinosaal, 'id'>): Observable<Kinosaal> {
|
||||
return this.http.post<Kinosaal>(`${this.baseUrl}kinosaal`, kinosaal);
|
||||
}
|
||||
|
||||
/* PUT /api/kinosaal/{id} */
|
||||
updateKinosaal(id: number, kinosaal: Partial<Kinosaal>): Observable<Kinosaal> {
|
||||
return this.http.put<Kinosaal>(`${this.baseUrl}kinosaal/${id}`, kinosaal);
|
||||
}
|
||||
|
||||
/* DELETE /api/kinosaal/{id} */
|
||||
deleteKinosaal(id: number): Observable<void> {
|
||||
return this.http.delete<void>(`${this.baseUrl}kinosaal/${id}`);
|
||||
}
|
||||
|
||||
|
||||
/* Vorstellung APIs */
|
||||
|
||||
/* GET /api/vorstellung */
|
||||
getAllPerformaces(): Observable<Vorstellung[]> {
|
||||
return this.http.get<Vorstellung[]>(`${this.baseUrl}vorstellung`);
|
||||
}
|
||||
|
||||
/* GET /api/vorstellung/{id} */
|
||||
getPerformaceById(id: number): Observable<Vorstellung> {
|
||||
return this.http.get<Vorstellung>(`${this.baseUrl}vorstellung/${id}`);
|
||||
@@ -138,21 +77,6 @@ export class HttpService {
|
||||
return this.http.post<Vorstellung[]>(`${this.baseUrl}vorstellung/filter`, filter);
|
||||
}
|
||||
|
||||
/* POST /api/vorstellung */
|
||||
addPerformace(vorstellung: Omit<Vorstellung, 'id'>): Observable<Vorstellung> {
|
||||
return this.http.post<Vorstellung>(`${this.baseUrl}vorstellung`, vorstellung);
|
||||
}
|
||||
|
||||
/* PUT /api/vorstellung/{id} */
|
||||
updatePerformace(id: number, vorstellung: Partial<Vorstellung>): Observable<Vorstellung> {
|
||||
return this.http.put<Vorstellung>(`${this.baseUrl}vorstellung/${id}`, vorstellung);
|
||||
}
|
||||
|
||||
/* DELETE /api/vorstellung/{id} */
|
||||
deletePerformace(id: number): Observable<void> {
|
||||
return this.http.delete<void>(`${this.baseUrl}vorstellung/${id}`);
|
||||
}
|
||||
|
||||
|
||||
/* Film APIs */
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { MatSnackBar, MatSnackBarRef } from '@angular/material/snack-bar';
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -13,6 +13,9 @@ export class LoadingService {
|
||||
public loading$ = this.loadingSubject.asObservable();
|
||||
public error$ = this.errorSubject.asObservable();
|
||||
|
||||
private currentSnackBarRef?: MatSnackBarRef<any>;
|
||||
private currentSubscription?: Subscription;
|
||||
|
||||
constructor(private snackBar: MatSnackBar) {}
|
||||
|
||||
show(): void {
|
||||
@@ -23,6 +26,7 @@ export class LoadingService {
|
||||
hide(): void {
|
||||
this.loadingSubject.next(false);
|
||||
this.errorSubject.next(false);
|
||||
this.currentSnackBarRef?.dismiss();
|
||||
}
|
||||
|
||||
showError(messageOrError?: string | HttpErrorResponse | any): void {
|
||||
@@ -35,15 +39,22 @@ export class LoadingService {
|
||||
|
||||
const message = this.getErrorMessage(messageOrError);
|
||||
|
||||
const snackBarRef = this.snackBar.open(message, 'Schließen', {
|
||||
if (this.currentSnackBarRef) {
|
||||
this.currentSubscription?.unsubscribe();
|
||||
this.currentSnackBarRef.dismiss();
|
||||
}
|
||||
|
||||
this.currentSnackBarRef = this.snackBar.open(message, 'Schließen', {
|
||||
duration: 0,
|
||||
panelClass: ['error-snackbar'],
|
||||
horizontalPosition: 'center',
|
||||
verticalPosition: 'bottom'
|
||||
});
|
||||
|
||||
snackBarRef.afterDismissed().subscribe(() => {
|
||||
this.currentSubscription = this.currentSnackBarRef.afterDismissed().subscribe(() => {
|
||||
if (!this.loadingSubject.value) {
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="flex items-center space-x-4">
|
||||
<mat-form-field class="w-full" subscriptSizing="dynamic">
|
||||
<mat-label>Film online suchen</mat-label>
|
||||
<input class="w-full" type="text" matInput [formControl]="formControl">
|
||||
<input class="w-full" type="text" matInput [formControl]="formControl"/>
|
||||
@if (formControl.hasError('noResults')) {
|
||||
<mat-error>Keine Suchergebnisse gefunden</mat-error>
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { LoadingService } from './../loading.service';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { Component, DestroyRef, inject } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { catchError, finalize, of, tap } from 'rxjs';
|
||||
import { HttpService } from '../http.service';
|
||||
import { OmdbMovie } from '@infinimotion/model-frontend';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Component({
|
||||
selector: 'app-movie-importer',
|
||||
@@ -21,13 +22,15 @@ export class MovieImporterComponent {
|
||||
|
||||
private httpService = inject(HttpService)
|
||||
public loadingService = inject(LoadingService)
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
|
||||
DoSubmit() {
|
||||
this.showAll = false;
|
||||
this.searchForMovies();
|
||||
}
|
||||
|
||||
searchForMovies() {
|
||||
private searchForMovies() {
|
||||
this.search_query = this.formControl.value?.trim() || '';
|
||||
if (this.search_query?.length == 0) return;
|
||||
|
||||
@@ -48,7 +51,8 @@ export class MovieImporterComponent {
|
||||
finalize(() => {
|
||||
this.isSearching = false;
|
||||
this.formControl.enable();
|
||||
})
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export class MoviePerformanceComponent implements OnInit {
|
||||
route: string = '';
|
||||
|
||||
ngOnInit() {
|
||||
this.route = `../performance/${this.id()}/checkout`;
|
||||
this.route = `../checkout/performance/${this.id()}`;
|
||||
}
|
||||
|
||||
startTime = computed(() =>
|
||||
|
||||
@@ -11,6 +11,7 @@ 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'},
|
||||
]
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
:host {
|
||||
min-width: 500px;
|
||||
}
|
||||
|
||||
mat-stepper {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
@@ -27,12 +27,16 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<mat-stepper orientation="horizontal" linear="true" [disableRipple]="true" (selectionChange)="onStepChange($event)" #stepper>
|
||||
<mat-step>
|
||||
<mat-stepper orientation="horizontal" linear="true" [selectedIndex]="isResuming()? 2 : 0" [disableRipple]="true" (selectionChange)="onStepChange($event)" #stepper>
|
||||
<mat-step [editable]="!isResuming() && !(isReservationSuccess() && !isSubmitting()) && !(isPurchaseSuccess() && !isSubmitting())">
|
||||
<ng-template matStepLabel>Warenkorb</ng-template>
|
||||
|
||||
<div class="performance-info-space"></div>
|
||||
|
||||
@if (selectedSeatsService.hadConflict()) {
|
||||
<app-selection-conflict-info></app-selection-conflict-info>
|
||||
}
|
||||
|
||||
<!-- Seat-Selection Overview -->
|
||||
<div class="mb-4 p-2">
|
||||
@for (seatCategory of seatCategories(); track $index) {
|
||||
@@ -63,7 +67,7 @@
|
||||
</div>
|
||||
</mat-step>
|
||||
|
||||
<mat-step [stepControl]="dataForm">
|
||||
<mat-step [editable]="!isResuming() && !(isPurchaseSuccess() && !isSubmitting())" [completed]="isResuming() || dataForm.valid || isPurchaseSuccess() || isSubmitting()" [stepControl]="dataForm">
|
||||
<form [formGroup]="dataForm">
|
||||
<ng-template matStepLabel>Anschrift</ng-template>
|
||||
|
||||
@@ -71,11 +75,11 @@
|
||||
|
||||
@if (isReservationSuccess() && !isSubmitting()) {
|
||||
<div class="h-4"></div>
|
||||
<app-reservation-success [order]="createdOrder()!"></app-reservation-success>
|
||||
<app-reservation-success [order]="createdOrder()!" [moreThanOne]="totalSeats() > 1"></app-reservation-success>
|
||||
}
|
||||
@else if (isReservationError() && !isSubmitting()) {
|
||||
@else if (isReservationError() && !isSubmitting() && performance()) {
|
||||
<div class="h-4"></div>
|
||||
<app-reservation-failed></app-reservation-failed>
|
||||
<app-reservation-failed (retry)="retryReservation()" [performanceId]="performance()!.id" [moreThanOne]="totalSeats() > 1"></app-reservation-failed>
|
||||
}
|
||||
@else {
|
||||
|
||||
@@ -119,11 +123,23 @@
|
||||
|
||||
@if (isPurchaseSuccess() && !isSubmitting()) {
|
||||
<div class="h-4"></div>
|
||||
<app-purchase-success [tickets]="createdTickets()"></app-purchase-success>
|
||||
<app-purchase-success [tickets]="createdTickets()" [moreThanOne]="totalSeats() > 1"></app-purchase-success>
|
||||
}
|
||||
@else if (isPurchaseError() && !isSubmitting()) {
|
||||
@else if (isPurchaseError() && !isSubmitting() && performance()) {
|
||||
<div class="h-4"></div>
|
||||
<app-purchase-failed></app-purchase-failed>
|
||||
<app-purchase-failed (retry)="retryPurchase()" [performanceId]="performance()!.id" [moreThanOne]="totalSeats() > 1"></app-purchase-failed>
|
||||
}
|
||||
@else if (isConversionError() && !isSubmitting()) {
|
||||
<div class="h-4"></div>
|
||||
<app-conversion-failed (retry)="retryConversion()" [moreThanOne]="totalSeats() > 1"></app-conversion-failed >
|
||||
}
|
||||
@else if (isCancellationSuccess() && !isSubmitting() && performance()) {
|
||||
<div class="h-4"></div>
|
||||
<app-cancellation-success [performanceId]="performance()!.id" [moreThanOne]="totalSeats() > 1"></app-cancellation-success>
|
||||
}
|
||||
@else if (isCancellationError() && !isSubmitting()) {
|
||||
<div class="h-4"></div>
|
||||
<app-cancellation-failed (retry)="retryCancellation()" [moreThanOne]="totalSeats() > 1"></app-cancellation-failed>
|
||||
}
|
||||
@else {
|
||||
|
||||
@@ -179,9 +195,17 @@
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex space-x-4 mt-8">
|
||||
|
||||
@if (isResuming()) {
|
||||
<button mat-stroked-button color="primary" type="button" [disabled]="isSubmitting()" class="w-1/3" (click)="cancelReservation()">
|
||||
Stornieren
|
||||
</button>
|
||||
} @else {
|
||||
<button mat-stroked-button color="primary" matStepperPrevious type="button" [disabled]="isSubmitting()" class="w-1/3">
|
||||
Zurück
|
||||
</button>
|
||||
}
|
||||
|
||||
<button mat-flat-button color="accent" matStepperNext type="submit" [disabled]="isSubmitting()" class="w-2/3">
|
||||
{{ getPriceDisplay(totalPrice()) }} jetzt bezahlen
|
||||
</button>
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
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, inject, input, output, signal } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { StepperSelectionEvent } from '@angular/cdk/stepper';
|
||||
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 { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { CancelOrderDialog } from '../cancel-order/cancel-order.dialog';
|
||||
|
||||
type OrderState =
|
||||
| { status: 'idle' }
|
||||
@@ -15,7 +17,10 @@ type OrderState =
|
||||
| { status: 'reservation-success'; order: Bestellung }
|
||||
| { status: 'reservation-error'; error: any }
|
||||
| { status: 'purchase-success'; tickets: Eintrittskarte[] }
|
||||
| { status: 'purchase-error'; error: any };
|
||||
| { status: 'purchase-error'; error: any }
|
||||
| { status: 'conversion-error'; error: any }
|
||||
| { status: 'cancellation-success'; }
|
||||
| { status: 'cancellation-error'; error: any };
|
||||
|
||||
type SubmissionMode = 'reservation' | 'purchase';
|
||||
|
||||
@@ -29,12 +34,20 @@ export class OrderComponent {
|
||||
private fb = inject(FormBuilder);
|
||||
private httpService = inject(HttpService);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
private dialog = inject(MatDialog);
|
||||
|
||||
readonly loadingService = inject(LoadingService);
|
||||
readonly selectedSeatsService = inject(SelectedSeatsService);
|
||||
|
||||
performance = input<Vorstellung>();
|
||||
seatCategories = input.required<Sitzkategorie[]>();
|
||||
|
||||
existingOrder = input<Bestellung>();
|
||||
existingTickets = input<Eintrittskarte[]>();
|
||||
resumeWithCancel = input<boolean>(true);
|
||||
|
||||
stepChanged = output<number>();
|
||||
|
||||
paymentForm!: FormGroup;
|
||||
dataForm!: FormGroup;
|
||||
|
||||
@@ -49,6 +62,16 @@ export class OrderComponent {
|
||||
|
||||
isSubmitting = computed(() => this.orderState().status === 'submitting');
|
||||
|
||||
isResuming = computed(() => {
|
||||
const order = this.existingOrder();
|
||||
const tickets = this.existingTickets();
|
||||
|
||||
if (!order || !tickets || tickets.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
secondPhaseButtonText = computed(() => {
|
||||
const mode = this.submissionMode();
|
||||
if (!mode) return 'Loading...';
|
||||
@@ -67,6 +90,10 @@ export class OrderComponent {
|
||||
this.orderState().status === 'purchase-success'
|
||||
);
|
||||
|
||||
isCancellationSuccess = computed(() =>
|
||||
this.orderState().status === 'cancellation-success'
|
||||
);
|
||||
|
||||
isReservationError = computed(() =>
|
||||
this.orderState().status === 'reservation-error'
|
||||
);
|
||||
@@ -75,6 +102,14 @@ export class OrderComponent {
|
||||
this.orderState().status === 'purchase-error'
|
||||
);
|
||||
|
||||
isConversionError = computed(() =>
|
||||
this.orderState().status === 'conversion-error'
|
||||
);
|
||||
|
||||
isCancellationError = computed(() =>
|
||||
this.orderState().status === 'cancellation-error'
|
||||
);
|
||||
|
||||
createdOrder = computed(() => {
|
||||
const state = this.orderState();
|
||||
return state.status === 'reservation-success' ? state.order : null;
|
||||
@@ -99,6 +134,10 @@ export class OrderComponent {
|
||||
cvv: ['', [Validators.required, Validators.pattern(/^\d{3,4}$/)]],
|
||||
});
|
||||
this.confetti = (await import('canvas-confetti')).default;
|
||||
|
||||
if (this.resumeWithCancel()) {
|
||||
this.cancelReservation();
|
||||
}
|
||||
}
|
||||
|
||||
get fData() { return this.dataForm.controls; }
|
||||
@@ -107,6 +146,8 @@ export class OrderComponent {
|
||||
onStepChange(event: StepperSelectionEvent) {
|
||||
this.submitted.set(false);
|
||||
this.selectedSeatsService.setSeatSelectable(event.selectedIndex === 0);
|
||||
|
||||
this.stepChanged.emit(event.selectedIndex);
|
||||
}
|
||||
|
||||
reservationClicked() {
|
||||
@@ -128,12 +169,12 @@ export class OrderComponent {
|
||||
this.makeReservation();
|
||||
} else if (this.submissionMode() === 'purchase') {
|
||||
stepper.next();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
makeReservation() {
|
||||
this.orderState.set({ status: 'submitting' });
|
||||
this.loadingService.show();
|
||||
this.disableForms();
|
||||
|
||||
const order = this.generateNewOrderObject(this.dataForm.value.email, false);
|
||||
@@ -152,15 +193,51 @@ export class OrderComponent {
|
||||
this.loadingService.show();
|
||||
this.disableForms();
|
||||
|
||||
if (this.isResuming()) {
|
||||
const order = this.existingOrder()!;
|
||||
order.booked = new Date();
|
||||
this.convertOrder(order, this.existingTickets()!);
|
||||
} else {
|
||||
const order = this.generateNewOrderObject(this.dataForm.value.email, true);
|
||||
const seats = this.selectedSeatsService.selectedSeats();
|
||||
const performance = this.performance()!;
|
||||
|
||||
this.submitOrder(order, seats, performance, 'purchase');
|
||||
}
|
||||
}
|
||||
|
||||
private convertOrder(order: Bestellung, tickets: Eintrittskarte[]) {
|
||||
this.loadingService.show();
|
||||
this.httpService.updateOrder(order).pipe(
|
||||
tap(() => {
|
||||
// Success Handling
|
||||
this.orderState.set({
|
||||
status: 'purchase-success',
|
||||
tickets: tickets
|
||||
});
|
||||
|
||||
submitOrder(order: Bestellung, seats: Sitzplatz[], performance: Vorstellung, mode: SubmissionMode) {
|
||||
this.selectedSeatsService.commit();
|
||||
this.loadingService.hide();
|
||||
this.showConfetti();
|
||||
}),
|
||||
catchError(err => {
|
||||
// Error handling
|
||||
this.selectedSeatsService.error();
|
||||
this.loadingService.showError(err);
|
||||
console.error('Fehler bei der Umwandlung der Bestellung:', err);
|
||||
this.orderState.set({status: 'conversion-error', error: err});
|
||||
|
||||
return EMPTY;
|
||||
}),
|
||||
finalize(() => {
|
||||
this.enableForms();
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
private submitOrder(order: Bestellung, seats: Sitzplatz[], performance: Vorstellung, mode: SubmissionMode) {
|
||||
this.loadingService.show();
|
||||
|
||||
// Tickets anlegen
|
||||
const tickets = seats.map(seat => {
|
||||
@@ -189,8 +266,9 @@ export class OrderComponent {
|
||||
}),
|
||||
catchError(err => {
|
||||
// Error handling
|
||||
console.error('Fehler beim Anlegen der Bestellung/Tickets:', err);
|
||||
this.selectedSeatsService.error();
|
||||
this.loadingService.showError(err);
|
||||
console.error('Fehler beim Anlegen der Bestellung/Tickets:', err);
|
||||
|
||||
if (mode === 'reservation') {
|
||||
this.orderState.set({ status: 'reservation-error', error: err });
|
||||
@@ -255,6 +333,50 @@ export class OrderComponent {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
cancelReservation() {
|
||||
const dialogRef = this.dialog.open(CancelOrderDialog, {
|
||||
width: '500px',
|
||||
disableClose: false,
|
||||
enterAnimationDuration: '200ms',
|
||||
exitAnimationDuration: '100ms'
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(confirmed => {
|
||||
if (confirmed) {
|
||||
this.performCancellation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private performCancellation() {
|
||||
const order = this.existingOrder()!;
|
||||
order.cancelled = new Date();
|
||||
|
||||
this.loadingService.show();
|
||||
this.httpService.updateOrder(order).pipe(
|
||||
tap(() => {
|
||||
// Success Handling
|
||||
this.orderState.set({
|
||||
status: 'cancellation-success'
|
||||
});
|
||||
|
||||
this.selectedSeatsService.cancel();
|
||||
this.loadingService.hide();
|
||||
}),
|
||||
catchError(err => {
|
||||
// Error handling
|
||||
this.selectedSeatsService.error();
|
||||
this.loadingService.showError(err);
|
||||
console.error('Fehler bei der Bezahlung der Bestellung:', err);
|
||||
this.orderState.set({status: 'cancellation-error', error: err});
|
||||
|
||||
return EMPTY;
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
private disableForms(): void {
|
||||
this.dataForm.disable();
|
||||
this.paymentForm.disable();
|
||||
@@ -268,4 +390,26 @@ export class OrderComponent {
|
||||
getPriceDisplay(price: number): string {
|
||||
return `${(price / 100).toFixed(2)} €`;
|
||||
}
|
||||
|
||||
retryPurchase() {
|
||||
this.orderState.set({ status: 'idle' });
|
||||
this.makePurchase();
|
||||
}
|
||||
|
||||
retryReservation() {
|
||||
this.orderState.set({ status: 'idle' });
|
||||
this.makeReservation();
|
||||
}
|
||||
|
||||
retryConversion() {
|
||||
this.orderState.set({ status: 'idle' });
|
||||
const order = this.existingOrder()!;
|
||||
order.booked = new Date();
|
||||
this.convertOrder(order, this.existingTickets()!);
|
||||
}
|
||||
|
||||
retryCancellation() {
|
||||
this.orderState.set({ status: 'idle' });
|
||||
this.cancelReservation();
|
||||
}
|
||||
}
|
||||
|
||||
7
src/app/pay-for-order/pay-for-order.component.css
Normal file
7
src/app/pay-for-order/pay-for-order.component.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.middle {
|
||||
position: relative;
|
||||
top: 40%;
|
||||
-webkit-transform: translateY(-50%);
|
||||
-ms-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
40
src/app/pay-for-order/pay-for-order.component.html
Normal file
40
src/app/pay-for-order/pay-for-order.component.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<app-menu-header label="Tickets bezahlen" icon="payment_card"></app-menu-header>
|
||||
|
||||
<div class="w-100 m-auto middle">
|
||||
<form class="order-search-form w-full" (ngSubmit)="DoSubmit()">
|
||||
<div class="flex items-center space-x-4">
|
||||
<mat-form-field class="w-full" subscriptSizing="dynamic">
|
||||
<mat-label>Reservierungsnummer eingeben</mat-label>
|
||||
<input class="w-full" type="text" matInput [formControl]="formControl" (input)="onInput($event)" placeholder="XXXXXX" maxlength="6" autocomplete="off">
|
||||
<mat-error>
|
||||
@if (formControl.hasError('invalid')) {
|
||||
Ungültiger Reservierungscode
|
||||
}
|
||||
@else if (formControl.hasError('completed')) {
|
||||
Diese Bestellung wurde bereits abgeschlossen
|
||||
}
|
||||
@else if (formControl.hasError('required')) {
|
||||
Bitte geben Sie Ihren Code ein
|
||||
}
|
||||
@else if (formControl.hasError('severalOrders')) {
|
||||
Mehrere Bestellungen gefunden - bitte kontaktieren Sie den Support
|
||||
}
|
||||
@else if (formControl.hasError('alreadyBooked')) {
|
||||
Diese Bestellung wurde bereits bezahlt
|
||||
}
|
||||
@else if (formControl.hasError('cancelled')) {
|
||||
Diese Reservierung wurde storniert
|
||||
}
|
||||
@else if (formControl.hasError('serverError')) {
|
||||
Fehler beim Laden der Reservierung
|
||||
}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
@if (formControl.valid || !formControl.touched) {
|
||||
<div class="h-6"></div>
|
||||
}
|
||||
<button mat-button class="w-100 mt-2" matButton="filled" color="accent" [disabled]="(loadingService.loading$ | async) || formControl.invalid" type="submit">Tickets jetzt online bezahlen</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
112
src/app/pay-for-order/pay-for-order.component.ts
Normal file
112
src/app/pay-for-order/pay-for-order.component.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { Component, DestroyRef, inject, OnInit } from '@angular/core';
|
||||
import { FormControl, Validators } from '@angular/forms';
|
||||
import { LoadingService } from '../loading.service';
|
||||
import { HttpService } from '../http.service';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { catchError, finalize, map, of, take } from 'rxjs';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pay-for-order',
|
||||
standalone: false,
|
||||
templateUrl: './pay-for-order.component.html',
|
||||
styleUrl: './pay-for-order.component.css',
|
||||
})
|
||||
export class PayForOrderComponent implements OnInit {
|
||||
private httpService = inject(HttpService);
|
||||
private router = inject(Router);
|
||||
private route = inject(ActivatedRoute);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
public loadingService = inject(LoadingService);
|
||||
|
||||
queryError?: string;
|
||||
|
||||
formControl = new FormControl('', {
|
||||
validators: [
|
||||
Validators.required,
|
||||
Validators.minLength(6),
|
||||
Validators.maxLength(6)
|
||||
]
|
||||
});
|
||||
|
||||
ngOnInit() {
|
||||
const error = this.route.snapshot.queryParamMap.get('error');
|
||||
const code = this.route.snapshot.queryParamMap.get('code');
|
||||
|
||||
if (code) {
|
||||
this.formControl.setValue(code);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
// Warte einen Tick, damit Angular das FormControl initialisiert hat
|
||||
setTimeout(() => {
|
||||
this.formControl.clearValidators();
|
||||
this.formControl.setErrors({ [error]: true });
|
||||
this.formControl.markAsTouched();
|
||||
});
|
||||
|
||||
// Bei erster Änderung: Validatoren wieder aktivieren
|
||||
this.formControl.valueChanges.pipe(
|
||||
take(1),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe(() => {
|
||||
this.formControl.setValidators([
|
||||
Validators.required,
|
||||
Validators.minLength(6),
|
||||
Validators.maxLength(6)
|
||||
]);
|
||||
this.formControl.updateValueAndValidity();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onInput(event: Event) {
|
||||
this.queryError = undefined;
|
||||
const input = event.target as HTMLInputElement;
|
||||
const filtered = input.value.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
||||
this.formControl.setValue(filtered, { emitEvent: false });
|
||||
}
|
||||
|
||||
DoSubmit() {
|
||||
this.formControl.markAsTouched();
|
||||
if (this.formControl.invalid) return;
|
||||
|
||||
const code = this.formControl.value?.trim();
|
||||
if (!code || code.length !== 6) return;
|
||||
|
||||
this.loadingService.show();
|
||||
const orderFilter = [`eq;code;string;${code}`];
|
||||
|
||||
this.httpService.getOrdersByFilter(orderFilter).pipe(
|
||||
map(orders => {
|
||||
this.loadingService.hide();
|
||||
if (orders.length === 0) {
|
||||
this.formControl.setErrors({ invalid: true });
|
||||
return
|
||||
}
|
||||
|
||||
if (orders.length > 1) {
|
||||
this.formControl.setErrors({ severalOrders: true });
|
||||
return;
|
||||
}
|
||||
const order = orders[0];
|
||||
if (order.booked) {
|
||||
this.formControl.setErrors({ alreadyBooked: true });
|
||||
return;
|
||||
}
|
||||
if (order.cancelled) {
|
||||
this.formControl.setErrors({ cancelled: true });
|
||||
return;
|
||||
}
|
||||
this.router.navigate(['/checkout/order', order.code]);
|
||||
}),
|
||||
catchError(err => {
|
||||
this.loadingService.hide();
|
||||
this.loadingService.showError(err);
|
||||
this.formControl.setErrors({ serverError: true });
|
||||
return of(null);
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe();
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,9 @@
|
||||
warning
|
||||
</mat-icon>
|
||||
<h1 class="text-xl font-bold">Kauf fehlgeschlagen!</h1>
|
||||
<p class="text-center">Leider konnten Ihre Sitzplätze nicht gekauft werden. Dies kann passieren, wenn andere Nutzer zeitgleich versucht haben, dieselben Sitzplätze zu kaufen.</p>
|
||||
<p class="text-center">{{ infoText }}</p>
|
||||
|
||||
<button mat-button matButton="filled" class="error-button mt-4 w-80">Erneut versuchen</button>
|
||||
<button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="error-button w-80 mt-1">Zurück zur Programmauswahl</button>
|
||||
<button mat-button type="button" matButton="filled" class="error-button mt-4 w-80" (click)="retry.emit()">Erneut versuchen</button>
|
||||
<button mat-button type="button" matButton="outlined" color="accent" class="error-button w-80 mt-1" (click)="navigate()">Zurück zur Sitzplatzauswahl</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, inject, input, output } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-purchase-failed',
|
||||
@@ -7,5 +8,22 @@ import { Component } from '@angular/core';
|
||||
styleUrl: './purchase-failed.component.css',
|
||||
})
|
||||
export class PurchaseFailedComponent {
|
||||
performanceId = input.required<number>();
|
||||
moreThanOne = input<boolean>(false);
|
||||
|
||||
retry = output<void>();
|
||||
|
||||
private router = inject(Router);
|
||||
|
||||
infoText!: string;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.infoText = this.moreThanOne()?
|
||||
'Leider konnten Ihre Sitzplätze nicht gekauft werden. Dies kann passieren, wenn andere Nutzer zeitgleich versucht haben, dieselben Sitzplätze zu kaufen.' :
|
||||
'Leider konnte Ihr Sitzplatz nicht gekauft werden. Dies kann passieren, wenn andere Nutzer zeitgleich versucht haben, denselben Sitzplatz zu kaufen.';
|
||||
}
|
||||
|
||||
navigate() {
|
||||
window.location.href = `/checkout/performance/${this.performanceId()}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<div class="bg-green-200 rounded-md shadow-sm w-full h-fit p-6 py-8 items-center justify-center flex flex-col space-y-2">
|
||||
<h1 class="text-xl font-bold">Vielen Dank für Ihren Einkauf!</h1>
|
||||
<p class="text-center">Ihre Sitzplätze wurden erfolgreich gebucht.</p>
|
||||
<p class="text-center">{{ infoText }}</p>
|
||||
|
||||
<app-ticket-list [tickets]="tickets()" class="w-8/10 my-4"></app-ticket-list>
|
||||
|
||||
<button mat-button disabled="true" matButton="filled" class="success-button w-80 mt-4">Tickets herunterladen</button>
|
||||
<button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="success-button w-80 mt-1">Zurück zur Programmauswahl</button>
|
||||
<button mat-button disabled="true" matButton="filled" class="success-button w-80 mt-4">{{ buttonText }}</button>
|
||||
<button routerLink="/schedule" type="button" mat-button matButton="outlined" color="accent" class="success-button w-80 mt-1">Zur Programmauswahl</button>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,4 +9,18 @@ import { Component, input } from '@angular/core';
|
||||
})
|
||||
export class PurchaseSuccessComponent {
|
||||
tickets = input.required<Eintrittskarte[]>();
|
||||
moreThanOne = input<boolean>(false);
|
||||
|
||||
infoText!: string;
|
||||
buttonText!: string;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.infoText = this.moreThanOne()?
|
||||
'Ihre Sitzplätze wurden erfolgreich gebucht.' :
|
||||
'Ihr Sitzplatz wurden erfolgreich gebucht.';
|
||||
|
||||
this.buttonText = this.moreThanOne()?
|
||||
'Tickets herunterladen' :
|
||||
'Ticket herunterladen';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
warning
|
||||
</mat-icon>
|
||||
<h1 class="text-xl font-bold">Reservierung fehlgeschlagen!</h1>
|
||||
<p class="text-center">Leider konnten Ihre Sitzplätze nicht reserviert werden. Dies kann passieren, wenn andere Nutzer gleichzeitig versucht haben, dieselben Sitzplätze zu reservieren.</p>
|
||||
<p class="text-center">{{ infoText }}</p>
|
||||
|
||||
<button mat-button matButton="filled" class="error-button mt-4 w-80">Erneut versuchen</button>
|
||||
<button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="error-button w-80 mt-1">Zurück zur Programmauswahl</button>
|
||||
<button mat-button type="button" matButton="filled" class="error-button mt-4 w-80" (click)="retry.emit()">Erneut versuchen</button>
|
||||
<button mat-button type="button" matButton="outlined" color="accent" class="error-button w-80 mt-1" (click)="navigate()">Zurück zur Sitzplatzauswahl</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Component, inject, input, output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-reservation-failed',
|
||||
@@ -7,5 +8,22 @@ import { Component } from '@angular/core';
|
||||
styleUrl: './reservation-failed.component.css',
|
||||
})
|
||||
export class ReservationFailedComponent {
|
||||
performanceId = input.required<number>();
|
||||
moreThanOne = input<boolean>(false);
|
||||
|
||||
retry = output<void>();
|
||||
|
||||
router = inject(Router)
|
||||
|
||||
infoText!: string;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.infoText = this.moreThanOne()?
|
||||
'Leider konnten Ihre Sitzplätze nicht reserviert werden. Dies kann passieren, wenn andere Nutzer gleichzeitig versucht haben, dieselben Sitzplätze zu reservieren.' :
|
||||
'Leider konnte Ihr Sitzplatz nicht reserviert werden. Dies kann passieren, wenn andere Nutzer gleichzeitig versucht haben, denselben Sitzplatz zu reservieren.';
|
||||
}
|
||||
|
||||
navigate() {
|
||||
window.location.href = `/checkout/performance/${this.performanceId()}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<div class="bg-green-200 rounded-md shadow-sm w-full h-fit p-6 py-8 items-center justify-center flex flex-col space-y-2">
|
||||
<h1 class="text-xl font-bold">Reservierung erfolgreich!</h1>
|
||||
|
||||
<p class="text-center">Ihre Sitzplätze wurden erfolgreich reserviert. Bitte nennen sie den folgenden Code an der Kasse, um Ihre Reservierung in eine Buchung umzuwandeln.</p>
|
||||
<p class="text-center" style="white-space: pre-line;">{{ infoText }}</p>
|
||||
<div class="bg-white text-5xl font-mono rounded-md shadow-sm w-fit h-fit p-4 py-2 my-4">
|
||||
<strong>{{ order().code }}</strong>
|
||||
</div>
|
||||
|
||||
<button [disabled]="true" mat-button matButton="filled" color="accent" class="success-button mt-2 w-80">Tickets jetzt online bezahlen</button>
|
||||
<button routerLink="/schedule" mat-button matButton="outlined" class="success-button mb-4 w-80 mt-1">Zurück zur Programmauswahl</button>
|
||||
<div class="text-green-500 cursor-pointer w-fit mt-2" (click)="cancelReservation()">
|
||||
<button routerLink="/checkout/order/{{ order().code }}" type="button" mat-button matButton="filled" color="accent" class="success-button mt-2 w-80">{{ buttonText }}</button>
|
||||
<button routerLink="/schedule" type="button" mat-button matButton="outlined" class="success-button mb-4 w-80 mt-1">Zurück zur Programmauswahl</button>
|
||||
<div [routerLink]="['/checkout/order', order().code]" [queryParams]="{ action: 'cancel' }" class="text-green-500 cursor-pointer w-fit mt-2">
|
||||
Reservierung stornieren
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Bestellung } from '@infinimotion/model-frontend';
|
||||
import { Component, input } from '@angular/core';
|
||||
import { Component, input, OnInit, output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-reservation-success',
|
||||
@@ -7,10 +7,20 @@ import { Component, input } from '@angular/core';
|
||||
templateUrl: './reservation-success.component.html',
|
||||
styleUrl: './reservation-success.component.css',
|
||||
})
|
||||
export class ReservationSuccessComponent {
|
||||
export class ReservationSuccessComponent implements OnInit {
|
||||
order = input.required<Bestellung>();
|
||||
moreThanOne = input<boolean>(false);
|
||||
|
||||
cancelReservation() {
|
||||
// Logic to cancel the reservation
|
||||
infoText!: string;
|
||||
buttonText!: string;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.infoText = this.moreThanOne()?
|
||||
'Ihre Sitzplätze wurden erfolgreich reserviert.\nBitte nennen sie den folgenden Code an der Kasse, um Ihre Reservierung in eine Buchung umzuwandeln' :
|
||||
'Ihr Sitzplatz wurde erfolgreich reserviert.\nBitte nennen sie den folgenden Code an der Kasse, um Ihre Reservierung in eine Buchung umzuwandeln';
|
||||
|
||||
this.buttonText = this.moreThanOne()?
|
||||
'Tickets jetzt online bezahlen' :
|
||||
'Ticket jetzt online bezahlen';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<button (click)="updateSelectedSeats(this.seat())" [disabled]="state() == TheaterSeatState.BOOKED || state() == TheaterSeatState.RESERVED || !seatService.getSeatIsSelectable()" class="mx-0.5">
|
||||
<button (click)="updateSelectedSeats(this.seat())" [disabled]="!isClickable()" class="mx-0.5">
|
||||
<mat-icon
|
||||
[class]="isHoverable()? 'hover:opacity-50' : ''"
|
||||
[ngStyle]="{color : getSeatStateColor() }"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Component, inject, input} from '@angular/core';
|
||||
import { Component, computed, inject, input } from '@angular/core';
|
||||
import { TheaterSeatState } from '../model/theater-seat-state.model';
|
||||
import { Sitzplatz } from '@infinimotion/model-frontend';
|
||||
import { SelectedSeatsService } from '../selected-seats.service';
|
||||
@@ -13,14 +13,17 @@ export class SeatComponent{
|
||||
seat = input.required<Sitzplatz>();
|
||||
state = input.required<TheaterSeatState>()
|
||||
|
||||
selected: boolean = false;
|
||||
selected = computed(() => this.seatService.isSeatSelected(this.seat().id));
|
||||
|
||||
protected seatService = inject(SelectedSeatsService)
|
||||
protected readonly TheaterSeatState = TheaterSeatState;
|
||||
|
||||
getSeatStateColor(): string {
|
||||
if (this.isSelectedAndAvaliable()) {
|
||||
return this.seatService.committed()? '#00c951' : '#6366f1';
|
||||
if (this.selected()) {
|
||||
if(this.seatService.errored()) return '#f01d05';
|
||||
if(this.seatService.committed()) return '#00c951';
|
||||
if(this.seatService.cancelled()) return '#c0c0c0';
|
||||
return '#6366f1';
|
||||
}
|
||||
|
||||
if (!this.seatService.getSeatIsSelectable()) {
|
||||
@@ -50,16 +53,18 @@ export class SeatComponent{
|
||||
}
|
||||
|
||||
updateSelectedSeats(selectedSeat: Sitzplatz) : void {
|
||||
if(!this.selected){
|
||||
if (!this.selected()){
|
||||
this.seatService.pushSelectedSeat(selectedSeat);
|
||||
} else {
|
||||
this.seatService.removeSelectedSeat(selectedSeat);
|
||||
}
|
||||
|
||||
this.selected = !this.selected;
|
||||
}
|
||||
|
||||
isSelectedAndAvaliable(): boolean {
|
||||
return this.selected && this.state() == TheaterSeatState.AVAILABLE;
|
||||
isClickable(): boolean {
|
||||
if (this.state() === TheaterSeatState.BOOKED ||
|
||||
this.state() === TheaterSeatState.RESERVED) {
|
||||
return false;
|
||||
}
|
||||
return this.seatService.getSeatIsSelectable();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,22 +9,30 @@ export class SelectedSeatsService {
|
||||
private selectedSeatsSignal = signal<Sitzplatz[]>([]);
|
||||
private seatIsSelectableSignal = signal(true);
|
||||
private committedSignal = signal(false);
|
||||
private erroredSignal = signal(false);
|
||||
private cancelledSignal = signal(false);
|
||||
private debugSignal = signal(false);
|
||||
private hadConflictSignal = signal(false);
|
||||
|
||||
readonly selectedSeats = this.selectedSeatsSignal.asReadonly();
|
||||
readonly seatIsSelectable = this.seatIsSelectableSignal.asReadonly();
|
||||
readonly committed = this.committedSignal.asReadonly();
|
||||
readonly errored = this.erroredSignal.asReadonly();
|
||||
readonly cancelled = this.cancelledSignal.asReadonly();
|
||||
readonly debug = this.debugSignal.asReadonly();
|
||||
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));
|
||||
|
||||
pushSelectedSeat(selectedSeat: Sitzplatz): void {
|
||||
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.hadConflictSignal.set(false);
|
||||
}
|
||||
|
||||
getSeatsByCategory(categoryId: number): Sitzplatz[] {
|
||||
@@ -34,6 +42,9 @@ export class SelectedSeatsService {
|
||||
clearSelection(): void {
|
||||
this.selectedSeatsSignal.set([]);
|
||||
this.committedSignal.set(false);
|
||||
this.cancelledSignal.set(false);
|
||||
this.erroredSignal.set(false);
|
||||
this.hadConflictSignal.set(false);
|
||||
}
|
||||
|
||||
getSeatIsSelectable(): boolean{
|
||||
@@ -44,13 +55,25 @@ export class SelectedSeatsService {
|
||||
this.seatIsSelectableSignal.set(selectable);
|
||||
if (selectable) {
|
||||
this.committedSignal.set(false);
|
||||
this.cancelledSignal.set(false);
|
||||
this.erroredSignal.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
commit(): void {
|
||||
this.erroredSignal.set(false);
|
||||
this.committedSignal.set(true);
|
||||
}
|
||||
|
||||
error(): void {
|
||||
this.erroredSignal.set(true);
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.erroredSignal.set(false);
|
||||
this.cancelledSignal.set(true);
|
||||
}
|
||||
|
||||
toggleDebug(): void {
|
||||
this.debugSignal.update(debug => !debug);
|
||||
}
|
||||
@@ -58,4 +81,8 @@ export class SelectedSeatsService {
|
||||
isSeatSelected(seatId: number): boolean {
|
||||
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 {
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="m-auto w-200 h-10 bg-gray-200 mb-22" style="clip-path: polygon(0% 0%,100% 0%,90% 100%,10% 100%);" (click)="selectedSeatsService.toggleDebug()">
|
||||
<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%);" (click)="selectedSeatsService.toggleDebug()">
|
||||
<p class="flex justify-center text-lg font-bold p-1.5">
|
||||
Leinwand
|
||||
</p>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<div class="w-7/10 p-10 h-fit">
|
||||
<div>
|
||||
@if (!performance && (loading.loading$ | async)){
|
||||
@if (!performance() && (loading.loading$ | async)){
|
||||
<div class="w-full h-full flex items-center justify-center mt-70">
|
||||
<mat-progress-spinner
|
||||
mode="indeterminate"
|
||||
@@ -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" [existingOrder]="isResuming? order : undefined" [existingTickets]="isResuming? tickets : undefined" [resumeWithCancel] = "resumeWithCancel"></app-order>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
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 { 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} from '@angular/router';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { SelectedSeatsService } from '../selected-seats.service';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
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',
|
||||
@@ -19,79 +21,294 @@ const POLLING_INTERVAL_MS = 5000;
|
||||
export class TheaterOverlayComponent implements OnInit, OnDestroy {
|
||||
private http = inject(HttpService);
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
private selectedSeatService = inject(SelectedSeatsService);
|
||||
|
||||
readonly loading = inject(LoadingService);
|
||||
|
||||
showId!: number;
|
||||
orderId?: string;
|
||||
seatsPerRow = signal<{ seat: Sitzplatz | null, state: TheaterSeatState | null }[][]>([]);
|
||||
performance: Vorstellung | undefined;
|
||||
showId?: number | null;
|
||||
orderId?: number;
|
||||
orderCode?: string | null;
|
||||
isResuming = false;
|
||||
resumeWithCancel = false;
|
||||
tickets: Eintrittskarte[] | undefined;
|
||||
order: Bestellung | undefined;
|
||||
blockedSeats: Sitzplatz[] | undefined;
|
||||
seatCategories: Sitzkategorie[] = [];
|
||||
snackBarRef: MatSnackBarRef<TextOnlySnackBar> | undefined
|
||||
|
||||
performance = signal<Vorstellung | undefined>(undefined);
|
||||
seatsPerRow = signal<{ seat: Sitzplatz | null, state: TheaterSeatState | null }[][]>([]);
|
||||
|
||||
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')!);
|
||||
this.orderId = this.route.snapshot.queryParams['paramName'];
|
||||
this.orderCode = this.route.snapshot.paramMap.get('orderId');
|
||||
|
||||
if (this.orderCode) {
|
||||
// Checkout fortsetzen
|
||||
this.isResuming = true;
|
||||
|
||||
this.loadExistingOrder(this.orderCode);
|
||||
|
||||
this.route.queryParams.subscribe(params => {
|
||||
if (params['action'] === 'cancel') {
|
||||
this.resumeWithCancel = true;
|
||||
}
|
||||
});
|
||||
} else if (this.showId) {
|
||||
// Neuer Checkout
|
||||
this.isResuming = false;
|
||||
|
||||
this.selectedSeatService.clearSelection();
|
||||
this.selectedSeatService.setSeatSelectable(true);
|
||||
|
||||
this.setupActivityTracking();
|
||||
this.startAutoRefresh();
|
||||
this.startInactivityCheck();
|
||||
|
||||
} else {
|
||||
// Fallback
|
||||
console.error('Ungültige Checkout-Route');
|
||||
this.router.navigate(['/performances']);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private loadPerformanceAndSeats() {
|
||||
if (this.isInitialLoad()) {
|
||||
const isInitial = this.isInitialLoad();
|
||||
|
||||
if (isInitial) {
|
||||
this.loading.show();
|
||||
}
|
||||
|
||||
this.isRequestInProgress.set(true);
|
||||
|
||||
return forkJoin({
|
||||
performance: this.http.getPerformaceById(this.showId),
|
||||
seats: this.http.getSeatsByShowId(this.showId)
|
||||
performance: this.http.getPerformaceById(this.showId!),
|
||||
seats: this.http.getSeatsByShowId(this.showId!)
|
||||
}).pipe(
|
||||
tap(({ performance, seats }) => {
|
||||
this.performance = performance;
|
||||
this.performance.set(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));
|
||||
|
||||
if (this.isInitialLoad()) {
|
||||
if (isInitial) {
|
||||
this.loading.hide();
|
||||
this.isInitialLoad.set(false);
|
||||
}
|
||||
}),
|
||||
catchError(err => {
|
||||
if (this.isInitialLoad()) {
|
||||
if (isInitial) {
|
||||
this.loading.showError(err);
|
||||
} else {
|
||||
console.warn('Fehler beim Aktualisieren der Sitze:', err);
|
||||
}
|
||||
|
||||
if (this.isInitialLoad()) {
|
||||
this.loading.hide();
|
||||
return of({ performance: null, seats: { seats: [], reserved: [], booked: [] } });
|
||||
}),
|
||||
finalize(() => {
|
||||
if (isInitial) {
|
||||
this.isInitialLoad.set(false);
|
||||
}
|
||||
|
||||
return of({ performance: null, seats: { seats: [], reserved: [], booked: [] } });
|
||||
this.isRequestInProgress.set(false);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
converter(resp: { seats: Sitzplatz[], reserved: Sitzplatz[], booked: Sitzplatz[] }): {
|
||||
private loadExistingOrder(orderCode: string) {
|
||||
this.loading.show();
|
||||
const ticketFilter = [`eq;order.code;string;${orderCode}`];
|
||||
|
||||
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 } }
|
||||
));
|
||||
}
|
||||
|
||||
this.order = tickets[0].order;
|
||||
|
||||
if (this.order.booked || this.order.cancelled) {
|
||||
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.selectedSeatService.setSeatSelectable(false);
|
||||
|
||||
return this.loadPerformanceAndSeats();
|
||||
}),
|
||||
catchError(err => {
|
||||
console.error('Fehler beim Laden der Bestellung', err);
|
||||
|
||||
return from(this.router.navigate(
|
||||
['/checkout/order'],
|
||||
{ queryParams: { error: 'invalid', code: orderCode } }
|
||||
));
|
||||
}),
|
||||
finalize(() => {
|
||||
this.loading.hide();
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
|
||||
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[] }): {
|
||||
seat: Sitzplatz | null,
|
||||
state: TheaterSeatState | null
|
||||
}[][] {
|
||||
@@ -163,17 +380,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -42,17 +42,25 @@ html.dark {
|
||||
background: linear-gradient(to right, #6366f1, #db2777) !important;
|
||||
}
|
||||
|
||||
|
||||
.error-snackbar {
|
||||
@include mat.snack-bar-overrides((
|
||||
container-color: red,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
.timeout-snackbar {
|
||||
@include mat.snack-bar-overrides((
|
||||
container-color: #ff9900
|
||||
));
|
||||
}
|
||||
|
||||
.error-snackbar .mat-mdc-snack-bar-label {
|
||||
.mat-mdc-snack-bar-label {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.error-snackbar .mat-mdc-button {
|
||||
.mat-mdc-button {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user