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 { MovieImportSearchInfoComponent } from './movie-import-search-info/movie-import-search-info.component';
|
||||||
import { LoginDialog } from './login/login.dialog';
|
import { LoginDialog } from './login/login.dialog';
|
||||||
import { PerformanceInfoComponent } from './performance-info/performance-info.component';
|
import { PerformanceInfoComponent } from './performance-info/performance-info.component';
|
||||||
import { ShoppingCartComponent } from './shopping-cart/shopping-cart.component';
|
|
||||||
import { OrderComponent } from './order/order.component';
|
import { OrderComponent } from './order/order.component';
|
||||||
import { SeatSelectionComponent } from './seat-selection/seat-selection.component';
|
import { SeatSelectionComponent } from './seat-selection/seat-selection.component';
|
||||||
import { NoSeatsInHallComponent } from './no-seats-in-hall/no-seats-in-hall.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 { TicketListComponent } from './ticket-list/ticket-list.component';
|
||||||
import { StatisticsComponent } from './statistics/statistics.component';
|
import { StatisticsComponent } from './statistics/statistics.component';
|
||||||
import { ZoomWarningComponent } from './zoom-warning/zoom-warning.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';
|
import { PricelistComponent } from './pricelist/pricelist.component';
|
||||||
|
|
||||||
|
|
||||||
@@ -104,7 +109,6 @@ import { PricelistComponent } from './pricelist/pricelist.component';
|
|||||||
MovieImportSearchInfoComponent,
|
MovieImportSearchInfoComponent,
|
||||||
LoginDialog,
|
LoginDialog,
|
||||||
PerformanceInfoComponent,
|
PerformanceInfoComponent,
|
||||||
ShoppingCartComponent,
|
|
||||||
OrderComponent,
|
OrderComponent,
|
||||||
SeatSelectionComponent,
|
SeatSelectionComponent,
|
||||||
NoSeatsInHallComponent,
|
NoSeatsInHallComponent,
|
||||||
@@ -116,6 +120,12 @@ import { PricelistComponent } from './pricelist/pricelist.component';
|
|||||||
TicketListComponent,
|
TicketListComponent,
|
||||||
StatisticsComponent,
|
StatisticsComponent,
|
||||||
ZoomWarningComponent,
|
ZoomWarningComponent,
|
||||||
|
SelectionConflictInfoComponent,
|
||||||
|
CancellationSuccessComponent,
|
||||||
|
CancellationFailedComponent,
|
||||||
|
ConversionFailedComponent,
|
||||||
|
PayForOrderComponent,
|
||||||
|
CancelOrderDialog,
|
||||||
PricelistComponent,
|
PricelistComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import { ScheduleComponent } from './schedule/schedule.component';
|
|||||||
import { TheaterOverlayComponent} from './theater-overlay/theater-overlay.component';
|
import { TheaterOverlayComponent} from './theater-overlay/theater-overlay.component';
|
||||||
import { MovieImporterComponent } from './movie-importer/movie-importer.component';
|
import { MovieImporterComponent } from './movie-importer/movie-importer.component';
|
||||||
import { AuthGuard } from './auth.guard';
|
import { AuthGuard } from './auth.guard';
|
||||||
import {StatisticsComponent} from './statistics/statistics.component';
|
import { PayForOrderComponent } from './pay-for-order/pay-for-order.component';
|
||||||
|
import { StatisticsComponent } from './statistics/statistics.component';
|
||||||
import { PricelistComponent } from './pricelist/pricelist.component';
|
import { PricelistComponent } from './pricelist/pricelist.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
@@ -29,7 +30,9 @@ const routes: Routes = [
|
|||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard],
|
||||||
data: { roles: ['admin'] }, // Array von erlaubten Rollen. Derzeit gäbe es 'admin' und 'employee'
|
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',
|
path: 'admin/statistics',
|
||||||
component: StatisticsComponent,
|
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 */
|
/* 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 */
|
/* POST /api/bestellung/filter */
|
||||||
getOrdersByFilter(filter: string[]): Observable<Bestellung[]> {
|
getOrdersByFilter(filter: string[]): Observable<Bestellung[]> {
|
||||||
return this.http.post<Bestellung[]>(`${this.baseUrl}bestellung/filter`, filter);
|
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);
|
return this.http.post<Bestellung>(`${this.baseUrl}bestellung`, order);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* PUT /api/bestellung/{id} */
|
/* PUT /api/bestellung */
|
||||||
updateOrder(id: number, order: Partial<Bestellung>): Observable<Bestellung> {
|
updateOrder(order: Partial<Bestellung>): Observable<Bestellung> {
|
||||||
return this.http.put<Bestellung>(`${this.baseUrl}bestellung/${id}`, order);
|
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 */
|
/* POST /api/order-transaction/create */
|
||||||
saveAddOrder(req: {order:Bestellung, tickets:Eintrittskarte[]}): Observable<{order:Bestellung, tickets:Eintrittskarte[]}> {
|
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);
|
return this.http.post<{order: Bestellung, tickets: Eintrittskarte[]}>(`${this.baseUrl}order-transaction/create`, req);
|
||||||
@@ -62,36 +46,11 @@ export class HttpService {
|
|||||||
|
|
||||||
/* Eintrittskarte APIs */
|
/* 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 */
|
/* POST /api/eintrittskarte/filter */
|
||||||
getTicketsByFilter(filter: string[]): Observable<Eintrittskarte[]> {
|
getTicketsByFilter(filter: string[]): Observable<Eintrittskarte[]> {
|
||||||
return this.http.post<Eintrittskarte[]>(`${this.baseUrl}eintrittskarte/filter`, filter);
|
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 */
|
/* Kinosaal APIs */
|
||||||
|
|
||||||
@@ -100,34 +59,14 @@ export class HttpService {
|
|||||||
return this.http.get<Kinosaal[]>(`${this.baseUrl}kinosaal`);
|
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 */
|
/* POST /api/kinosaal */
|
||||||
addKinosaal(kinosaal: Omit<Kinosaal, 'id'>): Observable<Kinosaal> {
|
addKinosaal(kinosaal: Omit<Kinosaal, 'id'>): Observable<Kinosaal> {
|
||||||
return this.http.post<Kinosaal>(`${this.baseUrl}kinosaal`, 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 */
|
/* Vorstellung APIs */
|
||||||
|
|
||||||
/* GET /api/vorstellung */
|
|
||||||
getAllPerformaces(): Observable<Vorstellung[]> {
|
|
||||||
return this.http.get<Vorstellung[]>(`${this.baseUrl}vorstellung`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* GET /api/vorstellung/{id} */
|
/* GET /api/vorstellung/{id} */
|
||||||
getPerformaceById(id: number): Observable<Vorstellung> {
|
getPerformaceById(id: number): Observable<Vorstellung> {
|
||||||
return this.http.get<Vorstellung>(`${this.baseUrl}vorstellung/${id}`);
|
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);
|
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 */
|
/* Film APIs */
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { HttpErrorResponse } from '@angular/common/http';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar, MatSnackBarRef } from '@angular/material/snack-bar';
|
||||||
import { BehaviorSubject } from 'rxjs';
|
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -13,6 +13,9 @@ export class LoadingService {
|
|||||||
public loading$ = this.loadingSubject.asObservable();
|
public loading$ = this.loadingSubject.asObservable();
|
||||||
public error$ = this.errorSubject.asObservable();
|
public error$ = this.errorSubject.asObservable();
|
||||||
|
|
||||||
|
private currentSnackBarRef?: MatSnackBarRef<any>;
|
||||||
|
private currentSubscription?: Subscription;
|
||||||
|
|
||||||
constructor(private snackBar: MatSnackBar) {}
|
constructor(private snackBar: MatSnackBar) {}
|
||||||
|
|
||||||
show(): void {
|
show(): void {
|
||||||
@@ -23,6 +26,7 @@ export class LoadingService {
|
|||||||
hide(): void {
|
hide(): void {
|
||||||
this.loadingSubject.next(false);
|
this.loadingSubject.next(false);
|
||||||
this.errorSubject.next(false);
|
this.errorSubject.next(false);
|
||||||
|
this.currentSnackBarRef?.dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
showError(messageOrError?: string | HttpErrorResponse | any): void {
|
showError(messageOrError?: string | HttpErrorResponse | any): void {
|
||||||
@@ -35,15 +39,22 @@ export class LoadingService {
|
|||||||
|
|
||||||
const message = this.getErrorMessage(messageOrError);
|
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,
|
duration: 0,
|
||||||
panelClass: ['error-snackbar'],
|
panelClass: ['error-snackbar'],
|
||||||
horizontalPosition: 'center',
|
horizontalPosition: 'center',
|
||||||
verticalPosition: 'bottom'
|
verticalPosition: 'bottom'
|
||||||
});
|
});
|
||||||
|
|
||||||
snackBarRef.afterDismissed().subscribe(() => {
|
this.currentSubscription = this.currentSnackBarRef.afterDismissed().subscribe(() => {
|
||||||
this.hide();
|
if (!this.loadingSubject.value) {
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
<mat-form-field class="w-full" subscriptSizing="dynamic">
|
<mat-form-field class="w-full" subscriptSizing="dynamic">
|
||||||
<mat-label>Film online suchen</mat-label>
|
<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')) {
|
@if (formControl.hasError('noResults')) {
|
||||||
<mat-error>Keine Suchergebnisse gefunden</mat-error>
|
<mat-error>Keine Suchergebnisse gefunden</mat-error>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { LoadingService } from './../loading.service';
|
import { LoadingService } from './../loading.service';
|
||||||
import { Component, inject } from '@angular/core';
|
import { Component, DestroyRef, inject } from '@angular/core';
|
||||||
import { FormControl } from '@angular/forms';
|
import { FormControl } from '@angular/forms';
|
||||||
import { catchError, finalize, of, tap } from 'rxjs';
|
import { catchError, finalize, of, tap } from 'rxjs';
|
||||||
import { HttpService } from '../http.service';
|
import { HttpService } from '../http.service';
|
||||||
import { OmdbMovie } from '@infinimotion/model-frontend';
|
import { OmdbMovie } from '@infinimotion/model-frontend';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-movie-importer',
|
selector: 'app-movie-importer',
|
||||||
@@ -21,13 +22,15 @@ export class MovieImporterComponent {
|
|||||||
|
|
||||||
private httpService = inject(HttpService)
|
private httpService = inject(HttpService)
|
||||||
public loadingService = inject(LoadingService)
|
public loadingService = inject(LoadingService)
|
||||||
|
private destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
|
|
||||||
DoSubmit() {
|
DoSubmit() {
|
||||||
this.showAll = false;
|
this.showAll = false;
|
||||||
this.searchForMovies();
|
this.searchForMovies();
|
||||||
}
|
}
|
||||||
|
|
||||||
searchForMovies() {
|
private searchForMovies() {
|
||||||
this.search_query = this.formControl.value?.trim() || '';
|
this.search_query = this.formControl.value?.trim() || '';
|
||||||
if (this.search_query?.length == 0) return;
|
if (this.search_query?.length == 0) return;
|
||||||
|
|
||||||
@@ -48,7 +51,8 @@ export class MovieImporterComponent {
|
|||||||
finalize(() => {
|
finalize(() => {
|
||||||
this.isSearching = false;
|
this.isSearching = false;
|
||||||
this.formControl.enable();
|
this.formControl.enable();
|
||||||
})
|
}),
|
||||||
|
takeUntilDestroyed(this.destroyRef)
|
||||||
).subscribe();
|
).subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export class MoviePerformanceComponent implements OnInit {
|
|||||||
route: string = '';
|
route: string = '';
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.route = `../performance/${this.id()}/checkout`;
|
this.route = `../checkout/performance/${this.id()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
startTime = computed(() =>
|
startTime = computed(() =>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export class NavbarComponent {
|
|||||||
navItems: { label:string, path:string }[] = [
|
navItems: { label:string, path:string }[] = [
|
||||||
{label: 'Programm', path: '/schedule'},
|
{label: 'Programm', path: '/schedule'},
|
||||||
{label: 'Preise', path: '/prices'},
|
{label: 'Preise', path: '/prices'},
|
||||||
|
{label: 'Bezahlen', path: '/checkout/order'},
|
||||||
{label: 'Film importieren', path: '/admin/movie-importer'},
|
{label: 'Film importieren', path: '/admin/movie-importer'},
|
||||||
{label: 'Statistiken', path: '/admin/statistics'},
|
{label: 'Statistiken', path: '/admin/statistics'},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
:host {
|
||||||
|
min-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
mat-stepper {
|
mat-stepper {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,43 +27,47 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<mat-stepper orientation="horizontal" linear="true" [disableRipple]="true" (selectionChange)="onStepChange($event)" #stepper>
|
<mat-stepper orientation="horizontal" linear="true" [selectedIndex]="isResuming()? 2 : 0" [disableRipple]="true" (selectionChange)="onStepChange($event)" #stepper>
|
||||||
<mat-step>
|
<mat-step [editable]="!isResuming() && !(isReservationSuccess() && !isSubmitting()) && !(isPurchaseSuccess() && !isSubmitting())">
|
||||||
<ng-template matStepLabel>Warenkorb</ng-template>
|
<ng-template matStepLabel>Warenkorb</ng-template>
|
||||||
|
|
||||||
<div class="performance-info-space"></div>
|
<div class="performance-info-space"></div>
|
||||||
|
|
||||||
<!-- Seat-Selection Overview -->
|
@if (selectedSeatsService.hadConflict()) {
|
||||||
<div class="mb-4 p-2">
|
<app-selection-conflict-info></app-selection-conflict-info>
|
||||||
@for (seatCategory of seatCategories(); track $index) {
|
}
|
||||||
<div class="h-2"></div>
|
|
||||||
<app-seat-selection [seatCategory]="seatCategory"></app-seat-selection>
|
|
||||||
}
|
|
||||||
@empty {
|
|
||||||
<app-no-seats-in-hall></app-no-seats-in-hall>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<mat-divider></mat-divider>
|
<!-- Seat-Selection Overview -->
|
||||||
|
<div class="mb-4 p-2">
|
||||||
|
@for (seatCategory of seatCategories(); track $index) {
|
||||||
|
<div class="h-2"></div>
|
||||||
|
<app-seat-selection [seatCategory]="seatCategory"></app-seat-selection>
|
||||||
|
}
|
||||||
|
@empty {
|
||||||
|
<app-no-seats-in-hall></app-no-seats-in-hall>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Total Price -->
|
<mat-divider></mat-divider>
|
||||||
<div class="flex justify-between p-2 mt-1 items-baseline">
|
|
||||||
<p class="font-semibold text-lg">
|
|
||||||
Tickets gesamt:
|
|
||||||
</p>
|
|
||||||
<p class="font-semibold text-2xl bg-linear-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent">
|
|
||||||
{{ getPriceDisplay(totalPrice()) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Buttons -->
|
<!-- Total Price -->
|
||||||
<div class="flex space-x-5 mt-10">
|
<div class="flex justify-between p-2 mt-1 items-baseline">
|
||||||
<button mat-button matButton="outlined" matStepperNext class="w-1/2" [disabled]="totalSeats()==0" (click)="reservationClicked()">Reservieren</button>
|
<p class="font-semibold text-lg">
|
||||||
<button mat-button matButton="filled" matStepperNext class="w-1/2" [disabled]="totalSeats()==0" (click)="purchaseClicked()">Kaufen</button>
|
Tickets gesamt:
|
||||||
</div>
|
</p>
|
||||||
|
<p class="font-semibold text-2xl bg-linear-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent">
|
||||||
|
{{ getPriceDisplay(totalPrice()) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Buttons -->
|
||||||
|
<div class="flex space-x-5 mt-10">
|
||||||
|
<button mat-button matButton="outlined" matStepperNext class="w-1/2" [disabled]="totalSeats()==0" (click)="reservationClicked()">Reservieren</button>
|
||||||
|
<button mat-button matButton="filled" matStepperNext class="w-1/2" [disabled]="totalSeats()==0" (click)="purchaseClicked()">Kaufen</button>
|
||||||
|
</div>
|
||||||
</mat-step>
|
</mat-step>
|
||||||
|
|
||||||
<mat-step [stepControl]="dataForm">
|
<mat-step [editable]="!isResuming() && !(isPurchaseSuccess() && !isSubmitting())" [completed]="isResuming() || dataForm.valid || isPurchaseSuccess() || isSubmitting()" [stepControl]="dataForm">
|
||||||
<form [formGroup]="dataForm">
|
<form [formGroup]="dataForm">
|
||||||
<ng-template matStepLabel>Anschrift</ng-template>
|
<ng-template matStepLabel>Anschrift</ng-template>
|
||||||
|
|
||||||
@@ -71,11 +75,11 @@
|
|||||||
|
|
||||||
@if (isReservationSuccess() && !isSubmitting()) {
|
@if (isReservationSuccess() && !isSubmitting()) {
|
||||||
<div class="h-4"></div>
|
<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>
|
<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 {
|
@else {
|
||||||
|
|
||||||
@@ -119,11 +123,23 @@
|
|||||||
|
|
||||||
@if (isPurchaseSuccess() && !isSubmitting()) {
|
@if (isPurchaseSuccess() && !isSubmitting()) {
|
||||||
<div class="h-4"></div>
|
<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>
|
<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 {
|
@else {
|
||||||
|
|
||||||
@@ -179,9 +195,17 @@
|
|||||||
|
|
||||||
<!-- Buttons -->
|
<!-- Buttons -->
|
||||||
<div class="flex space-x-4 mt-8">
|
<div class="flex space-x-4 mt-8">
|
||||||
<button mat-stroked-button color="primary" matStepperPrevious type="button" [disabled]="isSubmitting()" class="w-1/3">
|
|
||||||
Zurück
|
@if (isResuming()) {
|
||||||
</button>
|
<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">
|
<button mat-flat-button color="accent" matStepperNext type="submit" [disabled]="isSubmitting()" class="w-2/3">
|
||||||
{{ getPriceDisplay(totalPrice()) }} jetzt bezahlen
|
{{ getPriceDisplay(totalPrice()) }} jetzt bezahlen
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { SelectedSeatsService } from './../selected-seats.service';
|
import { SelectedSeatsService } from './../selected-seats.service';
|
||||||
import { LoadingService } from './../loading.service';
|
import { LoadingService } from './../loading.service';
|
||||||
import { Bestellung, Eintrittskarte, Sitzkategorie, Sitzplatz, Vorstellung } from '@infinimotion/model-frontend';
|
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 { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||||
import { StepperSelectionEvent } from '@angular/cdk/stepper';
|
import { StepperSelectionEvent } from '@angular/cdk/stepper';
|
||||||
import { HttpService } from '../http.service';
|
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 { MatStepper } from '@angular/material/stepper';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { CancelOrderDialog } from '../cancel-order/cancel-order.dialog';
|
||||||
|
|
||||||
type OrderState =
|
type OrderState =
|
||||||
| { status: 'idle' }
|
| { status: 'idle' }
|
||||||
@@ -15,7 +17,10 @@ type OrderState =
|
|||||||
| { status: 'reservation-success'; order: Bestellung }
|
| { status: 'reservation-success'; order: Bestellung }
|
||||||
| { status: 'reservation-error'; error: any }
|
| { status: 'reservation-error'; error: any }
|
||||||
| { status: 'purchase-success'; tickets: Eintrittskarte[] }
|
| { 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';
|
type SubmissionMode = 'reservation' | 'purchase';
|
||||||
|
|
||||||
@@ -29,12 +34,20 @@ export class OrderComponent {
|
|||||||
private fb = inject(FormBuilder);
|
private fb = inject(FormBuilder);
|
||||||
private httpService = inject(HttpService);
|
private httpService = inject(HttpService);
|
||||||
private destroyRef = inject(DestroyRef);
|
private destroyRef = inject(DestroyRef);
|
||||||
|
private dialog = inject(MatDialog);
|
||||||
|
|
||||||
readonly loadingService = inject(LoadingService);
|
readonly loadingService = inject(LoadingService);
|
||||||
readonly selectedSeatsService = inject(SelectedSeatsService);
|
readonly selectedSeatsService = inject(SelectedSeatsService);
|
||||||
|
|
||||||
performance = input<Vorstellung>();
|
performance = input<Vorstellung>();
|
||||||
seatCategories = input.required<Sitzkategorie[]>();
|
seatCategories = input.required<Sitzkategorie[]>();
|
||||||
|
|
||||||
|
existingOrder = input<Bestellung>();
|
||||||
|
existingTickets = input<Eintrittskarte[]>();
|
||||||
|
resumeWithCancel = input<boolean>(true);
|
||||||
|
|
||||||
|
stepChanged = output<number>();
|
||||||
|
|
||||||
paymentForm!: FormGroup;
|
paymentForm!: FormGroup;
|
||||||
dataForm!: FormGroup;
|
dataForm!: FormGroup;
|
||||||
|
|
||||||
@@ -49,6 +62,16 @@ export class OrderComponent {
|
|||||||
|
|
||||||
isSubmitting = computed(() => this.orderState().status === 'submitting');
|
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(() => {
|
secondPhaseButtonText = computed(() => {
|
||||||
const mode = this.submissionMode();
|
const mode = this.submissionMode();
|
||||||
if (!mode) return 'Loading...';
|
if (!mode) return 'Loading...';
|
||||||
@@ -67,6 +90,10 @@ export class OrderComponent {
|
|||||||
this.orderState().status === 'purchase-success'
|
this.orderState().status === 'purchase-success'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
isCancellationSuccess = computed(() =>
|
||||||
|
this.orderState().status === 'cancellation-success'
|
||||||
|
);
|
||||||
|
|
||||||
isReservationError = computed(() =>
|
isReservationError = computed(() =>
|
||||||
this.orderState().status === 'reservation-error'
|
this.orderState().status === 'reservation-error'
|
||||||
);
|
);
|
||||||
@@ -75,6 +102,14 @@ export class OrderComponent {
|
|||||||
this.orderState().status === 'purchase-error'
|
this.orderState().status === 'purchase-error'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
isConversionError = computed(() =>
|
||||||
|
this.orderState().status === 'conversion-error'
|
||||||
|
);
|
||||||
|
|
||||||
|
isCancellationError = computed(() =>
|
||||||
|
this.orderState().status === 'cancellation-error'
|
||||||
|
);
|
||||||
|
|
||||||
createdOrder = computed(() => {
|
createdOrder = computed(() => {
|
||||||
const state = this.orderState();
|
const state = this.orderState();
|
||||||
return state.status === 'reservation-success' ? state.order : null;
|
return state.status === 'reservation-success' ? state.order : null;
|
||||||
@@ -99,6 +134,10 @@ export class OrderComponent {
|
|||||||
cvv: ['', [Validators.required, Validators.pattern(/^\d{3,4}$/)]],
|
cvv: ['', [Validators.required, Validators.pattern(/^\d{3,4}$/)]],
|
||||||
});
|
});
|
||||||
this.confetti = (await import('canvas-confetti')).default;
|
this.confetti = (await import('canvas-confetti')).default;
|
||||||
|
|
||||||
|
if (this.resumeWithCancel()) {
|
||||||
|
this.cancelReservation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get fData() { return this.dataForm.controls; }
|
get fData() { return this.dataForm.controls; }
|
||||||
@@ -107,6 +146,8 @@ export class OrderComponent {
|
|||||||
onStepChange(event: StepperSelectionEvent) {
|
onStepChange(event: StepperSelectionEvent) {
|
||||||
this.submitted.set(false);
|
this.submitted.set(false);
|
||||||
this.selectedSeatsService.setSeatSelectable(event.selectedIndex === 0);
|
this.selectedSeatsService.setSeatSelectable(event.selectedIndex === 0);
|
||||||
|
|
||||||
|
this.stepChanged.emit(event.selectedIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
reservationClicked() {
|
reservationClicked() {
|
||||||
@@ -128,12 +169,12 @@ export class OrderComponent {
|
|||||||
this.makeReservation();
|
this.makeReservation();
|
||||||
} else if (this.submissionMode() === 'purchase') {
|
} else if (this.submissionMode() === 'purchase') {
|
||||||
stepper.next();
|
stepper.next();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
makeReservation() {
|
makeReservation() {
|
||||||
this.orderState.set({ status: 'submitting' });
|
this.orderState.set({ status: 'submitting' });
|
||||||
this.loadingService.show();
|
|
||||||
this.disableForms();
|
this.disableForms();
|
||||||
|
|
||||||
const order = this.generateNewOrderObject(this.dataForm.value.email, false);
|
const order = this.generateNewOrderObject(this.dataForm.value.email, false);
|
||||||
@@ -152,15 +193,51 @@ export class OrderComponent {
|
|||||||
this.loadingService.show();
|
this.loadingService.show();
|
||||||
this.disableForms();
|
this.disableForms();
|
||||||
|
|
||||||
const order = this.generateNewOrderObject(this.dataForm.value.email, true);
|
if (this.isResuming()) {
|
||||||
const seats = this.selectedSeatsService.selectedSeats();
|
const order = this.existingOrder()!;
|
||||||
const performance = this.performance()!;
|
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');
|
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
|
// Tickets anlegen
|
||||||
const tickets = seats.map(seat => {
|
const tickets = seats.map(seat => {
|
||||||
@@ -189,8 +266,9 @@ export class OrderComponent {
|
|||||||
}),
|
}),
|
||||||
catchError(err => {
|
catchError(err => {
|
||||||
// Error handling
|
// Error handling
|
||||||
console.error('Fehler beim Anlegen der Bestellung/Tickets:', err);
|
this.selectedSeatsService.error();
|
||||||
this.loadingService.showError(err);
|
this.loadingService.showError(err);
|
||||||
|
console.error('Fehler beim Anlegen der Bestellung/Tickets:', err);
|
||||||
|
|
||||||
if (mode === 'reservation') {
|
if (mode === 'reservation') {
|
||||||
this.orderState.set({ status: 'reservation-error', error: err });
|
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 {
|
private disableForms(): void {
|
||||||
this.dataForm.disable();
|
this.dataForm.disable();
|
||||||
this.paymentForm.disable();
|
this.paymentForm.disable();
|
||||||
@@ -268,4 +390,26 @@ export class OrderComponent {
|
|||||||
getPriceDisplay(price: number): string {
|
getPriceDisplay(price: number): string {
|
||||||
return `${(price / 100).toFixed(2)} €`;
|
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
|
warning
|
||||||
</mat-icon>
|
</mat-icon>
|
||||||
<h1 class="text-xl font-bold">Kauf fehlgeschlagen!</h1>
|
<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 mat-button type="button" matButton="filled" class="error-button mt-4 w-80" (click)="retry.emit()">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="outlined" color="accent" class="error-button w-80 mt-1" (click)="navigate()">Zurück zur Sitzplatzauswahl</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, inject, input, output } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-purchase-failed',
|
selector: 'app-purchase-failed',
|
||||||
@@ -7,5 +8,22 @@ import { Component } from '@angular/core';
|
|||||||
styleUrl: './purchase-failed.component.css',
|
styleUrl: './purchase-failed.component.css',
|
||||||
})
|
})
|
||||||
export class PurchaseFailedComponent {
|
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">
|
<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>
|
<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>
|
<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 mat-button disabled="true" matButton="filled" class="success-button w-80 mt-4">{{ buttonText }}</button>
|
||||||
<button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="success-button w-80 mt-1">Zurück zur Programmauswahl</button>
|
<button routerLink="/schedule" type="button" mat-button matButton="outlined" color="accent" class="success-button w-80 mt-1">Zur Programmauswahl</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -9,4 +9,18 @@ import { Component, input } from '@angular/core';
|
|||||||
})
|
})
|
||||||
export class PurchaseSuccessComponent {
|
export class PurchaseSuccessComponent {
|
||||||
tickets = input.required<Eintrittskarte[]>();
|
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
|
warning
|
||||||
</mat-icon>
|
</mat-icon>
|
||||||
<h1 class="text-xl font-bold">Reservierung fehlgeschlagen!</h1>
|
<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 mat-button type="button" matButton="filled" class="error-button mt-4 w-80" (click)="retry.emit()">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="outlined" color="accent" class="error-button w-80 mt-1" (click)="navigate()">Zurück zur Sitzplatzauswahl</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Router } from '@angular/router';
|
||||||
|
import { Component, inject, input, output } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-reservation-failed',
|
selector: 'app-reservation-failed',
|
||||||
@@ -7,5 +8,22 @@ import { Component } from '@angular/core';
|
|||||||
styleUrl: './reservation-failed.component.css',
|
styleUrl: './reservation-failed.component.css',
|
||||||
})
|
})
|
||||||
export class ReservationFailedComponent {
|
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">
|
<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>
|
<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">
|
<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>
|
<strong>{{ order().code }}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button [disabled]="true" mat-button matButton="filled" color="accent" class="success-button mt-2 w-80">Tickets jetzt online bezahlen</button>
|
<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" mat-button matButton="outlined" class="success-button mb-4 w-80 mt-1">Zurück zur Programmauswahl</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 class="text-green-500 cursor-pointer w-fit mt-2" (click)="cancelReservation()">
|
<div [routerLink]="['/checkout/order', order().code]" [queryParams]="{ action: 'cancel' }" class="text-green-500 cursor-pointer w-fit mt-2">
|
||||||
Reservierung stornieren
|
Reservierung stornieren
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Bestellung } from '@infinimotion/model-frontend';
|
import { Bestellung } from '@infinimotion/model-frontend';
|
||||||
import { Component, input } from '@angular/core';
|
import { Component, input, OnInit, output } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-reservation-success',
|
selector: 'app-reservation-success',
|
||||||
@@ -7,10 +7,20 @@ import { Component, input } from '@angular/core';
|
|||||||
templateUrl: './reservation-success.component.html',
|
templateUrl: './reservation-success.component.html',
|
||||||
styleUrl: './reservation-success.component.css',
|
styleUrl: './reservation-success.component.css',
|
||||||
})
|
})
|
||||||
export class ReservationSuccessComponent {
|
export class ReservationSuccessComponent implements OnInit {
|
||||||
order = input.required<Bestellung>();
|
order = input.required<Bestellung>();
|
||||||
|
moreThanOne = input<boolean>(false);
|
||||||
|
|
||||||
cancelReservation() {
|
infoText!: string;
|
||||||
// Logic to cancel the reservation
|
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
|
<mat-icon
|
||||||
[class]="isHoverable()? 'hover:opacity-50' : ''"
|
[class]="isHoverable()? 'hover:opacity-50' : ''"
|
||||||
[ngStyle]="{color : getSeatStateColor() }"
|
[ngStyle]="{color : getSeatStateColor() }"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {Component, inject, input} from '@angular/core';
|
import { Component, computed, inject, input } from '@angular/core';
|
||||||
import {TheaterSeatState} from '../model/theater-seat-state.model';
|
import { TheaterSeatState } from '../model/theater-seat-state.model';
|
||||||
import {Sitzplatz} from '@infinimotion/model-frontend';
|
import { Sitzplatz } from '@infinimotion/model-frontend';
|
||||||
import {SelectedSeatsService} from '../selected-seats.service';
|
import { SelectedSeatsService } from '../selected-seats.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-seat',
|
selector: 'app-seat',
|
||||||
@@ -13,14 +13,17 @@ export class SeatComponent{
|
|||||||
seat = input.required<Sitzplatz>();
|
seat = input.required<Sitzplatz>();
|
||||||
state = input.required<TheaterSeatState>()
|
state = input.required<TheaterSeatState>()
|
||||||
|
|
||||||
selected: boolean = false;
|
selected = computed(() => this.seatService.isSeatSelected(this.seat().id));
|
||||||
|
|
||||||
protected seatService = inject(SelectedSeatsService)
|
protected seatService = inject(SelectedSeatsService)
|
||||||
protected readonly TheaterSeatState = TheaterSeatState;
|
protected readonly TheaterSeatState = TheaterSeatState;
|
||||||
|
|
||||||
getSeatStateColor(): string {
|
getSeatStateColor(): string {
|
||||||
if (this.isSelectedAndAvaliable()) {
|
if (this.selected()) {
|
||||||
return this.seatService.committed()? '#00c951' : '#6366f1';
|
if(this.seatService.errored()) return '#f01d05';
|
||||||
|
if(this.seatService.committed()) return '#00c951';
|
||||||
|
if(this.seatService.cancelled()) return '#c0c0c0';
|
||||||
|
return '#6366f1';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.seatService.getSeatIsSelectable()) {
|
if (!this.seatService.getSeatIsSelectable()) {
|
||||||
@@ -50,16 +53,18 @@ export class SeatComponent{
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateSelectedSeats(selectedSeat: Sitzplatz) : void {
|
updateSelectedSeats(selectedSeat: Sitzplatz) : void {
|
||||||
if(!this.selected){
|
if (!this.selected()){
|
||||||
this.seatService.pushSelectedSeat(selectedSeat);
|
this.seatService.pushSelectedSeat(selectedSeat);
|
||||||
} else {
|
} else {
|
||||||
this.seatService.removeSelectedSeat(selectedSeat);
|
this.seatService.removeSelectedSeat(selectedSeat);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.selected = !this.selected;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isSelectedAndAvaliable(): boolean {
|
isClickable(): boolean {
|
||||||
return this.selected && this.state() == TheaterSeatState.AVAILABLE;
|
if (this.state() === TheaterSeatState.BOOKED ||
|
||||||
|
this.state() === TheaterSeatState.RESERVED) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.seatService.getSeatIsSelectable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { computed, Injectable, signal } from '@angular/core';
|
import { computed, Injectable, signal } from '@angular/core';
|
||||||
import {Sitzplatz} from '@infinimotion/model-frontend';
|
import { Sitzplatz } from '@infinimotion/model-frontend';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -9,22 +9,30 @@ export class SelectedSeatsService {
|
|||||||
private selectedSeatsSignal = signal<Sitzplatz[]>([]);
|
private selectedSeatsSignal = signal<Sitzplatz[]>([]);
|
||||||
private seatIsSelectableSignal = signal(true);
|
private seatIsSelectableSignal = signal(true);
|
||||||
private committedSignal = signal(false);
|
private committedSignal = signal(false);
|
||||||
|
private erroredSignal = signal(false);
|
||||||
|
private cancelledSignal = signal(false);
|
||||||
private debugSignal = signal(false);
|
private debugSignal = signal(false);
|
||||||
|
private hadConflictSignal = signal(false);
|
||||||
|
|
||||||
readonly selectedSeats = this.selectedSeatsSignal.asReadonly();
|
readonly selectedSeats = this.selectedSeatsSignal.asReadonly();
|
||||||
readonly seatIsSelectable = this.seatIsSelectableSignal.asReadonly();
|
readonly seatIsSelectable = this.seatIsSelectableSignal.asReadonly();
|
||||||
readonly committed = this.committedSignal.asReadonly();
|
readonly committed = this.committedSignal.asReadonly();
|
||||||
|
readonly errored = this.erroredSignal.asReadonly();
|
||||||
|
readonly cancelled = this.cancelledSignal.asReadonly();
|
||||||
readonly debug = this.debugSignal.asReadonly();
|
readonly debug = this.debugSignal.asReadonly();
|
||||||
|
readonly hadConflict = this.hadConflictSignal.asReadonly();
|
||||||
|
|
||||||
readonly totalSeats = computed(() => this.selectedSeats().length);
|
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 {
|
pushSelectedSeat(selectedSeat: Sitzplatz): void {
|
||||||
this.selectedSeatsSignal.update(seats => [...seats, selectedSeat]);
|
this.selectedSeatsSignal.update(seats => [...seats, selectedSeat]);
|
||||||
|
this.hadConflictSignal.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeSelectedSeat(selectedSeat: Sitzplatz): void {
|
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[] {
|
getSeatsByCategory(categoryId: number): Sitzplatz[] {
|
||||||
@@ -34,6 +42,9 @@ export class SelectedSeatsService {
|
|||||||
clearSelection(): void {
|
clearSelection(): void {
|
||||||
this.selectedSeatsSignal.set([]);
|
this.selectedSeatsSignal.set([]);
|
||||||
this.committedSignal.set(false);
|
this.committedSignal.set(false);
|
||||||
|
this.cancelledSignal.set(false);
|
||||||
|
this.erroredSignal.set(false);
|
||||||
|
this.hadConflictSignal.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSeatIsSelectable(): boolean{
|
getSeatIsSelectable(): boolean{
|
||||||
@@ -44,13 +55,25 @@ export class SelectedSeatsService {
|
|||||||
this.seatIsSelectableSignal.set(selectable);
|
this.seatIsSelectableSignal.set(selectable);
|
||||||
if (selectable) {
|
if (selectable) {
|
||||||
this.committedSignal.set(false);
|
this.committedSignal.set(false);
|
||||||
|
this.cancelledSignal.set(false);
|
||||||
|
this.erroredSignal.set(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commit(): void {
|
commit(): void {
|
||||||
|
this.erroredSignal.set(false);
|
||||||
this.committedSignal.set(true);
|
this.committedSignal.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
error(): void {
|
||||||
|
this.erroredSignal.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(): void {
|
||||||
|
this.erroredSignal.set(false);
|
||||||
|
this.cancelledSignal.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
toggleDebug(): void {
|
toggleDebug(): void {
|
||||||
this.debugSignal.update(debug => !debug);
|
this.debugSignal.update(debug => !debug);
|
||||||
}
|
}
|
||||||
@@ -58,4 +81,8 @@ export class SelectedSeatsService {
|
|||||||
isSeatSelected(seatId: number): boolean {
|
isSeatSelected(seatId: number): boolean {
|
||||||
return this.selectedSeats().some(seat => seat.id === seatId);
|
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">
|
<p class="flex justify-center text-lg font-bold p-1.5">
|
||||||
Leinwand
|
Leinwand
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<div class="w-7/10 p-10 h-fit">
|
<div class="w-7/10 p-10 h-fit">
|
||||||
<div>
|
<div>
|
||||||
@if (!performance && (loading.loading$ | async)){
|
@if (!performance() && (loading.loading$ | async)){
|
||||||
<div class="w-full h-full flex items-center justify-center mt-70">
|
<div class="w-full h-full flex items-center justify-center mt-70">
|
||||||
<mat-progress-spinner
|
<mat-progress-spinner
|
||||||
mode="indeterminate"
|
mode="indeterminate"
|
||||||
@@ -18,6 +18,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import {Component, DestroyRef, inject, OnDestroy, OnInit, signal} from '@angular/core';
|
import { Component, DestroyRef, inject, OnDestroy, OnInit, signal } from '@angular/core';
|
||||||
import {HttpService} from '../http.service';
|
import { HttpService } from '../http.service';
|
||||||
import {LoadingService} from '../loading.service';
|
import { LoadingService } from '../loading.service';
|
||||||
import {catchError, filter, forkJoin, interval, of, startWith, switchMap, tap} from 'rxjs';
|
import { catchError, filter, finalize, forkJoin, from, fromEvent, interval, merge, of, startWith, switchMap, tap } from 'rxjs';
|
||||||
import {Sitzkategorie, Sitzplatz, Vorstellung} from '@infinimotion/model-frontend';
|
import { Bestellung, Eintrittskarte, Sitzkategorie, Sitzplatz, Vorstellung } from '@infinimotion/model-frontend';
|
||||||
import {TheaterSeatState} from '../model/theater-seat-state.model';
|
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 { SelectedSeatsService } from '../selected-seats.service';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
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({
|
@Component({
|
||||||
selector: 'app-theater-overlay',
|
selector: 'app-theater-overlay',
|
||||||
@@ -19,79 +21,294 @@ const POLLING_INTERVAL_MS = 5000;
|
|||||||
export class TheaterOverlayComponent implements OnInit, OnDestroy {
|
export class TheaterOverlayComponent implements OnInit, OnDestroy {
|
||||||
private http = inject(HttpService);
|
private http = inject(HttpService);
|
||||||
private route = inject(ActivatedRoute);
|
private route = inject(ActivatedRoute);
|
||||||
|
private router = inject(Router);
|
||||||
private destroyRef = inject(DestroyRef);
|
private destroyRef = inject(DestroyRef);
|
||||||
private selectedSeatService = inject(SelectedSeatsService);
|
private selectedSeatService = inject(SelectedSeatsService);
|
||||||
|
|
||||||
readonly loading = inject(LoadingService);
|
readonly loading = inject(LoadingService);
|
||||||
|
|
||||||
showId!: number;
|
showId?: number | null;
|
||||||
orderId?: string;
|
orderId?: number;
|
||||||
seatsPerRow = signal<{ seat: Sitzplatz | null, state: TheaterSeatState | null }[][]>([]);
|
orderCode?: string | null;
|
||||||
performance: Vorstellung | undefined;
|
isResuming = false;
|
||||||
|
resumeWithCancel = false;
|
||||||
|
tickets: Eintrittskarte[] | undefined;
|
||||||
|
order: Bestellung | undefined;
|
||||||
|
blockedSeats: Sitzplatz[] | undefined;
|
||||||
seatCategories: Sitzkategorie[] = [];
|
seatCategories: Sitzkategorie[] = [];
|
||||||
|
snackBarRef: MatSnackBarRef<TextOnlySnackBar> | undefined
|
||||||
|
|
||||||
|
performance = signal<Vorstellung | undefined>(undefined);
|
||||||
|
seatsPerRow = signal<{ seat: Sitzplatz | null, state: TheaterSeatState | null }[][]>([]);
|
||||||
|
|
||||||
private isPollingEnabled = signal(true);
|
private isPollingEnabled = signal(true);
|
||||||
private isInitialLoad = 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() {
|
ngOnInit() {
|
||||||
this.showId = Number(this.route.snapshot.paramMap.get('performanceId')!);
|
this.showId = Number(this.route.snapshot.paramMap.get('performanceId')!);
|
||||||
this.orderId = this.route.snapshot.queryParams['paramName'];
|
this.orderCode = this.route.snapshot.paramMap.get('orderId');
|
||||||
this.selectedSeatService.clearSelection();
|
|
||||||
this.selectedSeatService.setSeatSelectable(true);
|
|
||||||
|
|
||||||
this.startAutoRefresh();
|
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() {
|
ngOnDestroy() {
|
||||||
this.isPollingEnabled.set(false);
|
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() {
|
private startAutoRefresh() {
|
||||||
|
console.info('[TheaterOverlay] Starting auto-refresh polling');
|
||||||
interval(POLLING_INTERVAL_MS).pipe(
|
interval(POLLING_INTERVAL_MS).pipe(
|
||||||
startWith(0),
|
startWith(POLLING_INTERVAL_MS),
|
||||||
filter(() => this.isPollingEnabled()),
|
filter(() => this.isPollingEnabled()),
|
||||||
filter(() => !this.selectedSeatService.committed()),
|
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()),
|
switchMap(() => this.loadPerformanceAndSeats()),
|
||||||
takeUntilDestroyed(this.destroyRef)
|
takeUntilDestroyed(this.destroyRef)
|
||||||
).subscribe();
|
).subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadPerformanceAndSeats() {
|
private loadPerformanceAndSeats() {
|
||||||
if (this.isInitialLoad()) {
|
const isInitial = this.isInitialLoad();
|
||||||
|
|
||||||
|
if (isInitial) {
|
||||||
this.loading.show();
|
this.loading.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.isRequestInProgress.set(true);
|
||||||
|
|
||||||
return forkJoin({
|
return forkJoin({
|
||||||
performance: this.http.getPerformaceById(this.showId),
|
performance: this.http.getPerformaceById(this.showId!),
|
||||||
seats: this.http.getSeatsByShowId(this.showId)
|
seats: this.http.getSeatsByShowId(this.showId!)
|
||||||
}).pipe(
|
}).pipe(
|
||||||
tap(({ performance, seats }) => {
|
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));
|
this.seatsPerRow.set(this.converter(seats));
|
||||||
|
|
||||||
if (this.isInitialLoad()) {
|
if (isInitial) {
|
||||||
this.loading.hide();
|
this.loading.hide();
|
||||||
this.isInitialLoad.set(false);
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
catchError(err => {
|
catchError(err => {
|
||||||
if (this.isInitialLoad()) {
|
if (isInitial) {
|
||||||
this.loading.showError(err);
|
this.loading.showError(err);
|
||||||
} else {
|
} else {
|
||||||
console.warn('Fehler beim Aktualisieren der Sitze:', err);
|
console.warn('Fehler beim Aktualisieren der Sitze:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isInitialLoad()) {
|
return of({ performance: null, seats: { seats: [], reserved: [], booked: [] } });
|
||||||
this.loading.hide();
|
}),
|
||||||
|
finalize(() => {
|
||||||
|
if (isInitial) {
|
||||||
this.isInitialLoad.set(false);
|
this.isInitialLoad.set(false);
|
||||||
}
|
}
|
||||||
|
this.isRequestInProgress.set(false);
|
||||||
return of({ performance: null, seats: { seats: [], reserved: [], booked: [] } });
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
converter(resp: { seats: Sitzplatz[], reserved: Sitzplatz[], booked: Sitzplatz[] }): {
|
private 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,
|
seat: Sitzplatz | null,
|
||||||
state: TheaterSeatState | null
|
state: TheaterSeatState | null
|
||||||
}[][] {
|
}[][] {
|
||||||
@@ -163,16 +380,36 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
|
|||||||
return filledRows;
|
return filledRows;
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshSeats(): void {
|
private refreshSeats(): void {
|
||||||
|
console.info('[TheaterOverlay] Manual refresh triggered');
|
||||||
this.loadPerformanceAndSeats().subscribe();
|
this.loadPerformanceAndSeats().subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
pausePolling(): void { //TODO: Ab Stepper Schritt 2 Polling pausieren
|
private pausePolling(): void {
|
||||||
|
console.info('[TheaterOverlay] Polling paused');
|
||||||
this.isPollingEnabled.set(false);
|
this.isPollingEnabled.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
resumePolling(): void {
|
private resumePolling(): void {
|
||||||
this.isPollingEnabled.set(true);
|
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;
|
background: linear-gradient(to right, #6366f1, #db2777) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include mat.snack-bar-overrides((
|
|
||||||
container-color: red,
|
.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;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-snackbar .mat-mdc-button {
|
.mat-mdc-button {
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user