Refactor order process to fix flicker bug
This commit is contained in:
@@ -1,195 +1,195 @@
|
||||
<div class="w-full h-full relative">
|
||||
|
||||
@if (!performance() && (loadingService.loading$ | async)){
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<mat-progress-spinner
|
||||
mode="indeterminate"
|
||||
diameter="50"
|
||||
></mat-progress-spinner>
|
||||
</div>
|
||||
}
|
||||
@else if (performance()) {
|
||||
|
||||
<div class="absolute top-18 z-20 w-full px-6">
|
||||
<app-performance-info
|
||||
class="w-full h-10"
|
||||
[performance]="performance()!"
|
||||
></app-performance-info>
|
||||
</div>
|
||||
|
||||
@if(isSubmitting) {
|
||||
<div class="absolute top-55 z-25 w-full px-6 my-auto">
|
||||
@if (!performance() && (loadingService.loading$ | async)){
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<mat-progress-spinner
|
||||
class="m-auto"
|
||||
mode="indeterminate"
|
||||
diameter="100"
|
||||
diameter="50"
|
||||
></mat-progress-spinner>
|
||||
</div>
|
||||
}
|
||||
@else if (performance()) {
|
||||
|
||||
<mat-stepper orientation="horizontal" linear="true" [disableRipple]="true" (selectionChange)="onStepChange($event)" #stepper>
|
||||
<mat-step>
|
||||
<ng-template matStepLabel>Warenkorb</ng-template>
|
||||
<div class="absolute top-18 z-20 w-full px-6">
|
||||
<app-performance-info
|
||||
class="w-full h-10"
|
||||
[performance]="performance()!"
|
||||
></app-performance-info>
|
||||
</div>
|
||||
|
||||
<div class="performance-info-space"></div>
|
||||
@if(isSubmitting()) {
|
||||
<div class="absolute top-55 z-25 w-full px-6 my-auto">
|
||||
<mat-progress-spinner
|
||||
class="m-auto"
|
||||
mode="indeterminate"
|
||||
diameter="100"
|
||||
></mat-progress-spinner>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- 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>
|
||||
<mat-stepper orientation="horizontal" linear="true" [disableRipple]="true" (selectionChange)="onStepChange($event)" #stepper>
|
||||
<mat-step>
|
||||
<ng-template matStepLabel>Warenkorb</ng-template>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
<div class="performance-info-space"></div>
|
||||
|
||||
<!-- Total Price -->
|
||||
<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 -->
|
||||
<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 [stepControl]="dataForm">
|
||||
<form [formGroup]="dataForm">
|
||||
<ng-template matStepLabel>Anschrift</ng-template>
|
||||
|
||||
<div class="performance-info-space"></div>
|
||||
|
||||
@if (seatsReserved && !isSubmitting) {
|
||||
<div class="h-4"></div>
|
||||
@if (successful) {
|
||||
<app-reservation-success [order]="this.createdOrder"></app-reservation-success>
|
||||
} @else {
|
||||
<app-reservation-failed></app-reservation-failed>
|
||||
}
|
||||
}
|
||||
@else {
|
||||
|
||||
<!-- Name -->
|
||||
<mat-form-field class="w-full mt-8">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput formControlName="name" placeholder="Max Mustermann" />
|
||||
@if (fData['name'].hasError('minlength')) { <mat-error>Mindestens 3 Zeichen</mat-error> }
|
||||
</mat-form-field>
|
||||
|
||||
<!-- E-Mail -->
|
||||
<mat-form-field class="w-full mt-2">
|
||||
<mat-label>E-Mail Adresse</mat-label>
|
||||
<input matInput formControlName="email" placeholder="max.mustermann@edu.fhdw.de" />
|
||||
@if (fData['email'].hasError('email')) { <mat-error>Ungültige E-Mail-Adresse</mat-error> }
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Checkbox -->
|
||||
<div class="w-full my-4">
|
||||
<mat-checkbox required formControlName="accept" class="checkbox-invalid" [class]="{ 'checkbox-invalid': submitted && fData['accept'].hasError('required') }">
|
||||
Ich akzeptiere die AGB und die Datenbestimmung
|
||||
</mat-checkbox>
|
||||
<!-- 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>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex space-x-5 mt-10">
|
||||
<button type="button" mat-button matButton="outlined" (click)="stepper.reset()" class="w-1/3" [disabled]="isSubmitting">Zurück</button>
|
||||
<button type="submit" mat-button matButton="filled" (click)="nextPhaseButtonClicked(stepper)" class="w-2/3" [disabled]="isSubmitting">{{ secondPhaseButtonText }}</button>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
}
|
||||
|
||||
</form>
|
||||
</mat-step>
|
||||
<mat-step [stepControl]="paymentForm">
|
||||
<form [formGroup]="paymentForm" (ngSubmit)="onSubmit()">
|
||||
<ng-template matStepLabel>Zahlung</ng-template>
|
||||
|
||||
<div class="performance-info-space"></div>
|
||||
|
||||
@if (seatsPurchased && !isSubmitting) {
|
||||
<div class="h-4"></div>
|
||||
@if (successful) {
|
||||
<app-purchase-success [tickets]="createdTickets"></app-purchase-success>
|
||||
} @else {
|
||||
<app-purchase-failed></app-purchase-failed>
|
||||
}
|
||||
}
|
||||
@else {
|
||||
|
||||
<!-- Card Number -->
|
||||
<mat-form-field class="w-full mt-8">
|
||||
<mat-label>Kartennummer</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="cardNumber"
|
||||
mask="0000 0000 0000 0000"
|
||||
placeholder="1111 2222 3333 4444"
|
||||
/>
|
||||
@if (fPayment['cardNumber'].hasError('pattern')) { <mat-error>Ungültige Kartennummer</mat-error> }
|
||||
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Card Name -->
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Name des Besitzers</mat-label>
|
||||
<input matInput formControlName="cardName" />
|
||||
@if (fPayment['cardName'].hasError('minlength')) { <mat-error>Mindestens 3 Zeichen</mat-error> }
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Expiry & CVV -->
|
||||
<div class="flex space-x-4">
|
||||
<mat-form-field class="flex-1">
|
||||
<mat-label>Gültig bis (MM/YY)</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="expiry"
|
||||
mask="00/00"
|
||||
placeholder="MM/YY"
|
||||
[dropSpecialCharacters]="false"
|
||||
/>
|
||||
@if (fPayment['expiry'].hasError('pattern')) { <mat-error>Ungültiges Format</mat-error> }
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-1">
|
||||
<mat-label>CVV</mat-label>
|
||||
<input matInput type="password" maxlength="4" formControlName="cvv" />
|
||||
@if (fPayment['cvv'].hasError('pattern')) { <mat-error>3–4 Ziffernt</mat-error> }
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="flex w-full space-x-2 mt-2 items-center">
|
||||
<mat-icon class="material-symbols-outlined opacity-50" style="font-size: 32px; width: 32px; height: 32px">
|
||||
encrypted
|
||||
</mat-icon>
|
||||
<p class="text-sm opacity-75">
|
||||
Ihre Zahlung wird sicher über unsere Partner verarbeitet.<br>Wir speichern keine Zahlungsinformationen.
|
||||
<!-- Total Price -->
|
||||
<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 -->
|
||||
<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
|
||||
</button>
|
||||
<button mat-flat-button color="accent" matStepperNext type="submit" [disabled]="isSubmitting" (click)="makePurchase()" class="w-2/3">
|
||||
{{ getPriceDisplay(totalPrice()) }} jetzt bezahlen
|
||||
</button>
|
||||
<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>
|
||||
}
|
||||
</form>
|
||||
</mat-step>
|
||||
</mat-step>
|
||||
|
||||
</mat-stepper>
|
||||
}
|
||||
<mat-step [stepControl]="dataForm">
|
||||
<form [formGroup]="dataForm">
|
||||
<ng-template matStepLabel>Anschrift</ng-template>
|
||||
|
||||
<div class="performance-info-space"></div>
|
||||
|
||||
@if (isReservationSuccess() && !isSubmitting()) {
|
||||
<div class="h-4"></div>
|
||||
<app-reservation-success [order]="createdOrder()!"></app-reservation-success>
|
||||
}
|
||||
@else if (isReservationError() && !isSubmitting()) {
|
||||
<div class="h-4"></div>
|
||||
<app-reservation-failed></app-reservation-failed>
|
||||
}
|
||||
@else {
|
||||
|
||||
<!-- Name -->
|
||||
<mat-form-field class="w-full mt-8">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput formControlName="name" placeholder="Max Mustermann" />
|
||||
@if (fData['name'].hasError('minlength')) { <mat-error>Mindestens 3 Zeichen</mat-error> }
|
||||
</mat-form-field>
|
||||
|
||||
<!-- E-Mail -->
|
||||
<mat-form-field class="w-full mt-2">
|
||||
<mat-label>E-Mail Adresse</mat-label>
|
||||
<input matInput formControlName="email" placeholder="max.mustermann@edu.fhdw.de" />
|
||||
@if (fData['email'].hasError('email')) { <mat-error>Ungültige E-Mail-Adresse</mat-error> }
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Checkbox -->
|
||||
<div class="w-full my-4">
|
||||
<mat-checkbox required formControlName="accept" class="checkbox-invalid" [class.checkbox-invalid]="submitted() && fData['accept'].hasError('required')">
|
||||
Ich akzeptiere die AGB und die Datenbestimmung
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex space-x-5 mt-10">
|
||||
<button type="button" mat-button matButton="outlined" (click)="stepper.reset()" class="w-1/3" [disabled]="isSubmitting()">Zurück</button>
|
||||
<button type="submit" mat-button matButton="filled" (click)="nextPhaseButtonClicked(stepper)" class="w-2/3" [disabled]="isSubmitting()">{{ secondPhaseButtonText() }}</button>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
</form>
|
||||
</mat-step>
|
||||
|
||||
<mat-step [stepControl]="paymentForm">
|
||||
<form [formGroup]="paymentForm" (ngSubmit)="makePurchase()">
|
||||
<ng-template matStepLabel>Zahlung</ng-template>
|
||||
|
||||
<div class="performance-info-space"></div>
|
||||
|
||||
@if (isPurchaseSuccess() && !isSubmitting()) {
|
||||
<div class="h-4"></div>
|
||||
<app-purchase-success [tickets]="createdTickets()"></app-purchase-success>
|
||||
}
|
||||
@else if (isPurchaseError() && !isSubmitting()) {
|
||||
<div class="h-4"></div>
|
||||
<app-purchase-failed></app-purchase-failed>
|
||||
}
|
||||
@else {
|
||||
|
||||
<!-- Card Number -->
|
||||
<mat-form-field class="w-full mt-8">
|
||||
<mat-label>Kartennummer</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="cardNumber"
|
||||
mask="0000 0000 0000 0000"
|
||||
placeholder="1111 2222 3333 4444"
|
||||
/>
|
||||
@if (fPayment['cardNumber'].hasError('pattern')) { <mat-error>Ungültige Kartennummer</mat-error> }
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Card Name -->
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Name des Besitzers</mat-label>
|
||||
<input matInput formControlName="cardName" />
|
||||
@if (fPayment['cardName'].hasError('minlength')) { <mat-error>Mindestens 3 Zeichen</mat-error> }
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Expiry & CVV -->
|
||||
<div class="flex space-x-4">
|
||||
<mat-form-field class="flex-1">
|
||||
<mat-label>Gültig bis (MM/YY)</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="expiry"
|
||||
mask="00/00"
|
||||
placeholder="MM/YY"
|
||||
[dropSpecialCharacters]="false"
|
||||
/>
|
||||
@if (fPayment['expiry'].hasError('pattern')) { <mat-error>Ungültiges Format</mat-error> }
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-1">
|
||||
<mat-label>CVV</mat-label>
|
||||
<input matInput type="password" maxlength="4" formControlName="cvv" />
|
||||
@if (fPayment['cvv'].hasError('pattern')) { <mat-error>3–4 Ziffernt</mat-error> }
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="flex w-full space-x-2 mt-2 items-center">
|
||||
<mat-icon class="material-symbols-outlined opacity-50" style="font-size: 32px; width: 32px; height: 32px">
|
||||
encrypted
|
||||
</mat-icon>
|
||||
<p class="text-sm opacity-75">
|
||||
Ihre Zahlung wird sicher über unsere Partner verarbeitet.<br>Wir speichern keine Zahlungsinformationen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<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
|
||||
</button>
|
||||
<button mat-flat-button color="accent" matStepperNext type="submit" [disabled]="isSubmitting()" class="w-2/3">
|
||||
{{ getPriceDisplay(totalPrice()) }} jetzt bezahlen
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
</mat-step>
|
||||
|
||||
</mat-stepper>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
import { SelectedSeatsService } from './../selected-seats.service';
|
||||
import { LoadingService } from './../loading.service';
|
||||
import { Bestellung, Eintrittskarte, Sitzkategorie, Sitzplatz, Vorstellung } from '@infinimotion/model-frontend';
|
||||
import { Component, computed, inject, input } from '@angular/core';
|
||||
import { Component, computed, DestroyRef, inject, input, signal } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { StepperSelectionEvent } from '@angular/cdk/stepper';
|
||||
import { HttpService } from '../http.service';
|
||||
import { catchError, tap, finalize } from 'rxjs';
|
||||
import { catchError, tap, finalize, switchMap, map, EMPTY, forkJoin } from 'rxjs';
|
||||
import { MatStepper } from '@angular/material/stepper';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
type OrderState =
|
||||
| { status: 'idle' }
|
||||
| { status: 'submitting' }
|
||||
| { status: 'reservation-success'; order: Bestellung }
|
||||
| { status: 'reservation-error'; error: any }
|
||||
| { status: 'purchase-success'; tickets: Eintrittskarte[] }
|
||||
| { status: 'purchase-error'; error: any };
|
||||
|
||||
type SubmissionMode = 'reservation' | 'purchase';
|
||||
|
||||
@Component({
|
||||
selector: 'app-order',
|
||||
@@ -15,182 +26,207 @@ import { MatStepper } from '@angular/material/stepper';
|
||||
styleUrl: './order.component.css'
|
||||
})
|
||||
export class OrderComponent {
|
||||
paymentForm!: FormGroup;
|
||||
dataForm!: FormGroup;
|
||||
|
||||
submitted = false;
|
||||
private fb = inject(FormBuilder);
|
||||
private httpService = inject(HttpService);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
readonly loadingService = inject(LoadingService);
|
||||
readonly selectedSeatsService = inject(SelectedSeatsService);
|
||||
|
||||
performance = input<Vorstellung>();
|
||||
seatCategories = input.required<Sitzkategorie[]>();
|
||||
|
||||
loadingService = inject(LoadingService);
|
||||
private httpService = inject(HttpService)
|
||||
private selectedSeatsService = inject(SelectedSeatsService);
|
||||
paymentForm!: FormGroup;
|
||||
dataForm!: FormGroup;
|
||||
|
||||
orderState = signal<OrderState>({ status: 'idle' });
|
||||
submissionMode = signal<SubmissionMode | null>(null);
|
||||
submitted = signal(false);
|
||||
|
||||
confetti: any;
|
||||
|
||||
constructor(private fb: FormBuilder) {}
|
||||
totalPrice = this.selectedSeatsService.totalPrice;
|
||||
totalSeats = this.selectedSeatsService.totalSeats;
|
||||
|
||||
isSubmitting = computed(() => this.orderState().status === 'submitting');
|
||||
|
||||
secondPhaseButtonText = computed(() => {
|
||||
const mode = this.submissionMode();
|
||||
if (!mode) return 'Loading...';
|
||||
|
||||
if (mode === 'reservation') {
|
||||
return this.totalSeats() > 1 ? 'Sitzplätze reservieren' : 'Sitzplatz reservieren';
|
||||
}
|
||||
return 'Weiter zur Zahlung';
|
||||
});
|
||||
|
||||
isReservationSuccess = computed(() =>
|
||||
this.orderState().status === 'reservation-success'
|
||||
);
|
||||
|
||||
isPurchaseSuccess = computed(() =>
|
||||
this.orderState().status === 'purchase-success'
|
||||
);
|
||||
|
||||
isReservationError = computed(() =>
|
||||
this.orderState().status === 'reservation-error'
|
||||
);
|
||||
|
||||
isPurchaseError = computed(() =>
|
||||
this.orderState().status === 'purchase-error'
|
||||
);
|
||||
|
||||
createdOrder = computed(() => {
|
||||
const state = this.orderState();
|
||||
return state.status === 'reservation-success' ? state.order : null;
|
||||
});
|
||||
|
||||
createdTickets = computed(() => {
|
||||
const state = this.orderState();
|
||||
return state.status === 'purchase-success' ? state.tickets : [];
|
||||
});
|
||||
|
||||
// Form-Validation
|
||||
|
||||
async ngOnInit() {
|
||||
this.dataForm = this.fb.group({
|
||||
name: ['', [Validators.required, Validators.minLength(3)]],
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
accept: ['', Validators.requiredTrue],
|
||||
});
|
||||
this.paymentForm = this.fb.group({
|
||||
cardNumber: ['', [Validators.required, Validators.pattern(/^\d{16}$/)]],
|
||||
cardName: ['', [Validators.required, Validators.minLength(3)]],
|
||||
expiry: ['', [Validators.required, Validators.pattern(/^(0[1-9]|1[0-2])\/\d{2}$/)]],
|
||||
cvv: ['', [Validators.required, Validators.pattern(/^\d{3,4}$/)]],
|
||||
});
|
||||
this.dataForm = this.fb.group({
|
||||
name: ['', [Validators.required, Validators.minLength(3)]],
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
accept: ['', Validators.requiredTrue],
|
||||
});
|
||||
this.confetti = (await import('canvas-confetti')).default;
|
||||
}
|
||||
|
||||
get fData() { return this.dataForm.controls; }
|
||||
get fPayment() { return this.paymentForm.controls; }
|
||||
|
||||
onSubmit() {
|
||||
if (this.dataForm.invalid) return;
|
||||
if (this.paymentForm.invalid) return;
|
||||
onStepChange(event: StepperSelectionEvent) {
|
||||
this.submitted.set(false);
|
||||
this.selectedSeatsService.setSeatSelectable(event.selectedIndex === 0);
|
||||
}
|
||||
|
||||
onStepChange(event: StepperSelectionEvent) {
|
||||
this.submitted = false;
|
||||
reservationClicked() {
|
||||
this.submissionMode.set('reservation');
|
||||
}
|
||||
|
||||
if(event.selectedIndex != 0) {
|
||||
this.selectedSeatsService.setSeatIsSelectableFalse()
|
||||
} else {
|
||||
this.selectedSeatsService.setSeatIsSelectableTrue()
|
||||
}
|
||||
purchaseClicked() {
|
||||
this.submissionMode.set('purchase');
|
||||
}
|
||||
|
||||
nextPhaseButtonClicked(stepper: MatStepper) {
|
||||
this.submitted = true;
|
||||
if (this.dataForm.invalid) return;
|
||||
this.submitted.set(true);
|
||||
|
||||
if (this.submissionMode === "reservation") {
|
||||
if (this.dataForm.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.submissionMode() === 'reservation') {
|
||||
this.makeReservation();
|
||||
} else if (this.submissionMode === "purchase") {
|
||||
} else if (this.submissionMode() === 'purchase') {
|
||||
stepper.next();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
totalPrice = computed(() =>
|
||||
this.selectedSeatsService.getSelectedSeatsList().reduce((sum, seat) => sum + seat.row.category.price, 0)
|
||||
);
|
||||
|
||||
totalSeats = computed(() =>
|
||||
this.selectedSeatsService.getSelectedSeatsList().length
|
||||
);
|
||||
|
||||
getPriceDisplay(price: number): string {
|
||||
return `${(price / 100).toFixed(2)} €`;
|
||||
}
|
||||
|
||||
isSubmitting: boolean = false;
|
||||
secondPhaseButtonText: string = "Loading..."
|
||||
submissionMode!: 'reservation' | 'purchase';
|
||||
|
||||
seatsReserved: boolean = false;
|
||||
seatsPurchased: boolean = false;
|
||||
successful: boolean = false;
|
||||
|
||||
reservationClicked() {
|
||||
this.submissionMode = "reservation";
|
||||
if (this.totalSeats() > 1) {
|
||||
this.secondPhaseButtonText = "Sitzplätze reservieren"
|
||||
} else {
|
||||
this.secondPhaseButtonText = "Sitzplatz reservieren"
|
||||
}
|
||||
}
|
||||
|
||||
purchaseClicked() {
|
||||
this.submissionMode = "purchase"
|
||||
this.secondPhaseButtonText = "Weiter zur Zahlung"
|
||||
}
|
||||
|
||||
makeReservation() {
|
||||
this.orderState.set({ status: 'submitting' });
|
||||
this.loadingService.show();
|
||||
this.disableInputs()
|
||||
this.disableForms();
|
||||
|
||||
const order = this.generateNewOrderObject(this.dataForm.value.email, false);
|
||||
const seats = this.selectedSeatsService.getSelectedSeatsList();
|
||||
const seats = this.selectedSeatsService.selectedSeats();
|
||||
const performance = this.performance()!;
|
||||
this.sendToBackend(order, seats, performance);
|
||||
this.seatsReserved = true;
|
||||
|
||||
this.submitOrder(order, seats, performance, 'reservation');
|
||||
}
|
||||
|
||||
makePurchase() {
|
||||
if (this.paymentForm.invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.orderState.set({ status: 'submitting' });
|
||||
this.loadingService.show();
|
||||
this.disableInputs()
|
||||
this.disableForms();
|
||||
|
||||
const order = this.generateNewOrderObject(this.dataForm.value.email, true);
|
||||
const seats = this.selectedSeatsService.getSelectedSeatsList();
|
||||
const seats = this.selectedSeatsService.selectedSeats();
|
||||
const performance = this.performance()!;
|
||||
this.sendToBackend(order, seats, performance);
|
||||
this.seatsPurchased = true;
|
||||
|
||||
this.submitOrder(order, seats, performance, 'purchase');
|
||||
}
|
||||
|
||||
createdOrder!: Bestellung;
|
||||
createdTickets!: Eintrittskarte[];
|
||||
|
||||
sendToBackend(order: Bestellung, seats: Sitzplatz[], performance: Vorstellung) {
|
||||
submitOrder(order: Bestellung, seats: Sitzplatz[], performance: Vorstellung, mode: SubmissionMode) {
|
||||
this.httpService.addOrder(order).pipe(
|
||||
tap(createdOrder => {
|
||||
this.createdOrder = createdOrder;
|
||||
// Order erstellen
|
||||
switchMap(createdOrder => {
|
||||
|
||||
const ticketCreations = seats.map(seat => {
|
||||
// Tickets parallel erstellen
|
||||
const ticketObservables = seats.map(seat => {
|
||||
const ticket = this.generateNewTicketObject(performance, seat, createdOrder);
|
||||
return this.httpService.addTicket(ticket);
|
||||
});
|
||||
|
||||
Promise.all(ticketCreations.map(obs => obs.toPromise()))
|
||||
.then(async createdTickets => {
|
||||
this.createdTickets = createdTickets.filter(
|
||||
(ticket): ticket is Eintrittskarte => ticket !== undefined
|
||||
);
|
||||
// Warten bis alles fertig sind
|
||||
return forkJoin(ticketObservables).pipe(
|
||||
tap(createdTickets => {
|
||||
// Success Handling
|
||||
if (mode === 'reservation') {
|
||||
this.orderState.set({
|
||||
status: 'reservation-success',
|
||||
order: createdOrder
|
||||
});
|
||||
} else {
|
||||
this.orderState.set({
|
||||
status: 'purchase-success',
|
||||
tickets: createdTickets
|
||||
});
|
||||
}
|
||||
|
||||
this.successful = true;
|
||||
this.selectedSeatsService.setCommitedTrue();
|
||||
this.selectedSeatsService.commit();
|
||||
this.loadingService.hide();
|
||||
|
||||
this.confetti({
|
||||
particleCount: 100,
|
||||
angle: 0,
|
||||
spread: 180,
|
||||
origin: { x: -0.1, y: 0.75 }
|
||||
});
|
||||
this.confetti({
|
||||
particleCount: 100,
|
||||
angle: 180,
|
||||
spread: 180,
|
||||
origin: { x: 1.1, y: 0.75 }
|
||||
});
|
||||
|
||||
this.showConfetti();
|
||||
})
|
||||
.catch(err => {
|
||||
this.loadingService.showError(err);
|
||||
this.successful = false;
|
||||
console.error('Fehler beim Anlegen der Eintrittskarten', err);
|
||||
});
|
||||
);
|
||||
}),
|
||||
catchError(err => {
|
||||
// Error handling
|
||||
console.error('Fehler beim Anlegen der Bestellung/Tickets:', err);
|
||||
this.loadingService.showError(err);
|
||||
this.successful = false;
|
||||
console.error('Fehler beim Anlegen der Bestellung', err);
|
||||
return [];
|
||||
|
||||
if (mode === 'reservation') {
|
||||
this.orderState.set({ status: 'reservation-error', error: err });
|
||||
} else {
|
||||
this.orderState.set({ status: 'purchase-error', error: err });
|
||||
}
|
||||
|
||||
return EMPTY;
|
||||
}),
|
||||
finalize(() => {
|
||||
this.enableInputs();
|
||||
})
|
||||
this.enableForms();
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
|
||||
|
||||
private showConfetti() {
|
||||
this.confetti({
|
||||
particleCount: 100,
|
||||
angle: 0,
|
||||
spread: 180,
|
||||
origin: { x: -0.1, y: 0.75 }
|
||||
});
|
||||
this.confetti({
|
||||
particleCount: 100,
|
||||
angle: 180,
|
||||
spread: 180,
|
||||
origin: { x: 1.1, y: 0.75 }
|
||||
});
|
||||
}
|
||||
|
||||
private generateCode(length: number = 6): string {
|
||||
const chars = "ABCDEFGHJKLMNPQRSUVWXYZ23456789";
|
||||
@@ -206,7 +242,7 @@ export class OrderComponent {
|
||||
|
||||
private generateNewOrderObject(mail: string, isBooked: boolean): Bestellung {
|
||||
return{
|
||||
id: 0, // Wird durch Backend gesetzt
|
||||
id: 0, // Wird durch das Backend gesetzt
|
||||
mail: mail,
|
||||
code: this.generateCode(length=6),
|
||||
reserved: new Date(),
|
||||
@@ -217,7 +253,7 @@ export class OrderComponent {
|
||||
|
||||
private generateNewTicketObject(show: Vorstellung, seat: Sitzplatz, order: Bestellung): Eintrittskarte {
|
||||
return {
|
||||
id: 0, // Wird durch Backend gesetzt
|
||||
id: 0, // Wird durch das Backend gesetzt
|
||||
code: 'T' + this.generateCode(length=7),
|
||||
show: show,
|
||||
seat: seat,
|
||||
@@ -225,16 +261,17 @@ export class OrderComponent {
|
||||
};
|
||||
}
|
||||
|
||||
private disableInputs() {
|
||||
private disableForms(): void {
|
||||
this.dataForm.disable();
|
||||
this.paymentForm.disable();
|
||||
this.isSubmitting = true;
|
||||
}
|
||||
|
||||
private enableInputs() {
|
||||
private enableForms(): void {
|
||||
this.dataForm.enable();
|
||||
this.paymentForm.enable();
|
||||
this.isSubmitting = false;
|
||||
}
|
||||
|
||||
getPriceDisplay(price: number): string {
|
||||
return `${(price / 100).toFixed(2)} €`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
warning
|
||||
</mat-icon>
|
||||
<h1 class="text-xl font-bold">Kauf fehlgeschlagen!</h1>
|
||||
<p class="text-center">Leider konnten Ihre Sitzplätze nicht gebucht werden.<br>Dies kann passieren, wenn andere Nutzer gleichzeitig versucht haben, dieselben Sitzplätze zu kaufen.</p>
|
||||
<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>
|
||||
|
||||
<button mat-button matButton="filled" class="error-button mt-4 w-80">Erneut versuchen</button>
|
||||
<button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="error-button w-80">Zurück zur Programmauswahl</button>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
warning
|
||||
</mat-icon>
|
||||
<h1 class="text-xl font-bold">Reservierung fehlgeschlagen!</h1>
|
||||
<p class="text-center">Leider konnten Ihre Sitzplätze nicht reserviert werden.<br>Dies kann passieren, wenn andere Nutzer gleichzeitig versucht haben, dieselben Sitzplätze zu reservieren.</p>
|
||||
<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>
|
||||
|
||||
<button mat-button matButton="filled" class="error-button mt-4 w-80">Erneut versuchen</button>
|
||||
<button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="error-button w-80">Zurück zur Programmauswahl</button>
|
||||
|
||||
@@ -14,7 +14,7 @@ export class SeatSelectionComponent {
|
||||
SelectedSeatsService = inject(SelectedSeatsService);
|
||||
|
||||
selectedSeatsByCategory = computed(() =>
|
||||
this.SelectedSeatsService.getSelectedSeatsByCategory(this.seatCategory().id).length
|
||||
this.SelectedSeatsService.getSeatsByCategory(this.seatCategory().id).length
|
||||
);
|
||||
|
||||
totalCategoryPrice = computed(() =>
|
||||
|
||||
@@ -20,7 +20,7 @@ export class SeatComponent{
|
||||
|
||||
getSeatStateColor(): string {
|
||||
if (this.isSelectedAndAvaliable()) {
|
||||
return this.seatService.getCommited()? '#00c951' : '#6366f1';
|
||||
return this.seatService.committed()? '#00c951' : '#6366f1';
|
||||
}
|
||||
|
||||
if (!this.seatService.getSeatIsSelectable()) {
|
||||
@@ -29,9 +29,9 @@ export class SeatComponent{
|
||||
|
||||
switch (this.state()) {
|
||||
case TheaterSeatState.RESERVED:
|
||||
return this.seatService.getDebug()? '#f7e8c3' : '#c0c0c0';
|
||||
return this.seatService.debug()? '#f7e8c3' : '#c0c0c0';
|
||||
case TheaterSeatState.BOOKED:
|
||||
return this.seatService.getDebug()? '#ffc9c9' : '#c0c0c0';
|
||||
return this.seatService.debug()? '#ffc9c9' : '#c0c0c0';
|
||||
default:
|
||||
case TheaterSeatState.AVAILABLE:
|
||||
return '#1B1B23';
|
||||
|
||||
@@ -1,69 +1,61 @@
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
import { computed, Injectable, signal } from '@angular/core';
|
||||
import {Sitzplatz} from '@infinimotion/model-frontend';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SelectedSeatsService {
|
||||
private selectedSeatsSignal = signal<Sitzplatz[]>([]);
|
||||
private seatIsSelectable: boolean = true;
|
||||
private commited = false;
|
||||
private debug = false;
|
||||
|
||||
get selectedSeats() {
|
||||
return this.selectedSeatsSignal;
|
||||
}
|
||||
private selectedSeatsSignal = signal<Sitzplatz[]>([]);
|
||||
private seatIsSelectableSignal = signal(true);
|
||||
private committedSignal = signal(false);
|
||||
private debugSignal = signal(false);
|
||||
|
||||
readonly selectedSeats = this.selectedSeatsSignal.asReadonly();
|
||||
readonly seatIsSelectable = this.seatIsSelectableSignal.asReadonly();
|
||||
readonly committed = this.committedSignal.asReadonly();
|
||||
readonly debug = this.debugSignal.asReadonly();
|
||||
|
||||
readonly totalSeats = computed(() => this.selectedSeats().length);
|
||||
readonly totalPrice = computed(() => this.selectedSeats().reduce((sum, seat) => sum + seat.row.category.price, 0));
|
||||
|
||||
pushSelectedSeat(selectedSeat: Sitzplatz): void {
|
||||
this.selectedSeatsSignal.update(seats => [...seats, selectedSeat]);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
getSelectedSeatsList(): Sitzplatz[] {
|
||||
return this.selectedSeatsSignal();
|
||||
getSeatsByCategory(categoryId: number): Sitzplatz[] {
|
||||
return this.selectedSeats().filter(seat => seat.row.category.id === categoryId);
|
||||
}
|
||||
|
||||
getSelectedSeatsByCategory(categoryId: number): Sitzplatz[] {
|
||||
return this.selectedSeatsSignal().filter(seat => seat.row.category.id === categoryId);
|
||||
}
|
||||
|
||||
clearSelectedSeatsList(): void {
|
||||
clearSelection(): void {
|
||||
this.selectedSeatsSignal.set([]);
|
||||
this.commited = false;
|
||||
this.committedSignal.set(false);
|
||||
}
|
||||
|
||||
getSeatIsSelectable(): boolean{
|
||||
return this.seatIsSelectable;
|
||||
return this.seatIsSelectable();
|
||||
}
|
||||
|
||||
setSeatIsSelectableTrue(): void {
|
||||
this.seatIsSelectable = true;
|
||||
this.commited = false;
|
||||
setSeatSelectable(selectable: boolean): void {
|
||||
this.seatIsSelectableSignal.set(selectable);
|
||||
if (selectable) {
|
||||
this.committedSignal.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
setSeatIsSelectableFalse(): void {
|
||||
this.seatIsSelectable = false;
|
||||
}
|
||||
|
||||
getCommited(): boolean {
|
||||
return this.commited;
|
||||
}
|
||||
|
||||
setCommitedTrue(): void {
|
||||
this.commited = true;
|
||||
}
|
||||
|
||||
getDebug(): boolean {
|
||||
return this.debug;
|
||||
commit(): void {
|
||||
this.committedSignal.set(true);
|
||||
}
|
||||
|
||||
toggleDebug(): void {
|
||||
this.debug = !this.debug;
|
||||
this.debugSignal.update(debug => !debug);
|
||||
}
|
||||
|
||||
isSeatSelected(seatId: number): boolean {
|
||||
return this.selectedSeats().some(seat => seat.id === seatId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ export class TheaterOverlayComponent implements OnInit {
|
||||
|
||||
ngOnInit() {
|
||||
this.showId = Number(this.route.snapshot.paramMap.get('id')!);
|
||||
this.selectedSeatService.clearSelectedSeatsList();
|
||||
this.selectedSeatService.setSeatIsSelectableTrue();
|
||||
this.selectedSeatService.clearSelection();
|
||||
this.selectedSeatService.setSeatSelectable(true);
|
||||
this.loadPerformanceAndSeats();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user