Add reservation and purchase functionality
Introduces ReservationSuccess, ReservationFailed, PurchaseSuccess, PurchaseFailed, TicketSmall, and TicketList components for handling and displaying reservation and purchase outcomes. Updates order flow logic in OrderComponent to support reservation and purchase states, disables/enables form inputs during submission, and integrates new UI feedback. Also adds angularx-qrcode dependency and updates @infinimotion/model-frontend version.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<div class="w-full h-full relative">
|
||||
|
||||
@if (loadingService.loading$ | async){
|
||||
@if (!performance() && (loadingService.loading$ | async)){
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<mat-progress-spinner
|
||||
mode="indeterminate"
|
||||
@@ -17,6 +17,16 @@
|
||||
></app-performance-info>
|
||||
</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>
|
||||
}
|
||||
|
||||
<mat-stepper orientation="horizontal" linear="true" [disableRipple]="true" (selectionChange)="onStepChange($event)" #stepper>
|
||||
<mat-step>
|
||||
<ng-template matStepLabel>Warenkorb</ng-template>
|
||||
@@ -48,8 +58,8 @@
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex space-x-5 mt-10">
|
||||
<button mat-button matButton="outlined" matStepperNext class="w-1/2" [disabled]="totalSeats()==0">Reservieren</button>
|
||||
<button mat-button matButton="filled" matStepperNext class="w-1/2" [disabled]="totalSeats()==0">Buchen</button>
|
||||
<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>
|
||||
|
||||
@@ -59,32 +69,45 @@
|
||||
|
||||
<div class="performance-info-space"></div>
|
||||
|
||||
<!-- 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>
|
||||
@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 {
|
||||
|
||||
<!-- 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>
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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>
|
||||
<!-- 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>
|
||||
|
||||
}
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex space-x-5 mt-10">
|
||||
<button type="button" mat-button matButton="outlined" (click)="stepper.reset()" class="w-1/3">Zurück</button>
|
||||
<button type="submit" mat-button matButton="filled" matStepperNext (click)="stupidCheckboxWorkaround()" class="w-2/3">Sitzplätze reservieren</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
<mat-step [stepControl]="paymentForm">
|
||||
@@ -93,66 +116,77 @@
|
||||
|
||||
<div class="performance-info-space"></div>
|
||||
|
||||
<!-- 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> }
|
||||
@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 {
|
||||
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Card Name -->
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Kartenname</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>
|
||||
<!-- Card Number -->
|
||||
<mat-form-field class="w-full mt-8">
|
||||
<mat-label>Kartennummer</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="expiry"
|
||||
mask="00/00"
|
||||
placeholder="MM/YY"
|
||||
[dropSpecialCharacters]="false"
|
||||
formControlName="cardNumber"
|
||||
mask="0000 0000 0000 0000"
|
||||
placeholder="1111 2222 3333 4444"
|
||||
/>
|
||||
@if (fPayment['expiry'].hasError('pattern')) { <mat-error>Ungültiges Format</mat-error> }
|
||||
@if (fPayment['cardNumber'].hasError('pattern')) { <mat-error>Ungültige Kartennummer</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> }
|
||||
<!-- Card Name -->
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Kartenname</mat-label>
|
||||
<input matInput formControlName="cardName" />
|
||||
@if (fPayment['cardName'].hasError('minlength')) { <mat-error>Mindestens 3 Zeichen</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>
|
||||
<!-- 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>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex space-x-4 mt-8">
|
||||
<button mat-stroked-button color="primary" matStepperPrevious type="button" class="w-1/3">
|
||||
Zurück
|
||||
</button>
|
||||
<button mat-flat-button color="accent" class="w-2/3" matStepperNext type="submit">
|
||||
{{ getPriceDisplay(totalPrice()) }} jetzt bezahlen
|
||||
</button>
|
||||
</div>
|
||||
<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" (click)="makePurchase()" class="w-2/3">
|
||||
{{ getPriceDisplay(totalPrice()) }} jetzt bezahlen
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
</mat-step>
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { SelectedSeatsService } from './../selected-seats.service';
|
||||
import { LoadingService } from './../loading.service';
|
||||
import { Sitzkategorie, Vorstellung } from '@infinimotion/model-frontend';
|
||||
import { Bestellung, Eintrittskarte, Sitzkategorie, Sitzplatz, Vorstellung } from '@infinimotion/model-frontend';
|
||||
import { Component, computed, inject, input } 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 { MatStepper } from '@angular/material/stepper';
|
||||
|
||||
@Component({
|
||||
selector: 'app-order',
|
||||
@@ -17,8 +20,17 @@ export class OrderComponent {
|
||||
|
||||
submitted = false;
|
||||
|
||||
performance = input<Vorstellung>();
|
||||
seatCategories = input.required<Sitzkategorie[]>();
|
||||
|
||||
loadingService = inject(LoadingService);
|
||||
private httpService = inject(HttpService)
|
||||
private selectedSeatsService = inject(SelectedSeatsService);
|
||||
|
||||
constructor(private fb: FormBuilder) {}
|
||||
|
||||
// Form-Validation
|
||||
|
||||
ngOnInit(): void {
|
||||
this.paymentForm = this.fb.group({
|
||||
cardNumber: ['', [Validators.required, Validators.pattern(/^\d{16}$/)]],
|
||||
@@ -37,23 +49,31 @@ export class OrderComponent {
|
||||
get fPayment() { return this.paymentForm.controls; }
|
||||
|
||||
onSubmit() {
|
||||
if (this.dataForm.invalid) return;
|
||||
if (this.paymentForm.invalid) return;
|
||||
console.log('Zahlungsdaten:', this.paymentForm.value);
|
||||
}
|
||||
|
||||
onStepChange(event: StepperSelectionEvent) {
|
||||
this.submitted = false;
|
||||
|
||||
if(event.selectedIndex != 0) {
|
||||
this.selectedSeatsService.setSeatIsSelectableFalse()
|
||||
} else {
|
||||
this.selectedSeatsService.setSeatIsSelectableTrue()
|
||||
}
|
||||
}
|
||||
|
||||
stupidCheckboxWorkaround() {
|
||||
nextPhaseButtonClicked(stepper: MatStepper) {
|
||||
this.submitted = true;
|
||||
if (this.dataForm.invalid) return;
|
||||
|
||||
if (this.submissionMode === "reservation") {
|
||||
this.makeReservation();
|
||||
} else if (this.submissionMode === "purchase") {
|
||||
stepper.next();
|
||||
}
|
||||
}
|
||||
|
||||
performance = input<Vorstellung>();
|
||||
seatCategories = input.required<Sitzkategorie[]>();
|
||||
|
||||
loadingService = inject(LoadingService);
|
||||
private selectedSeatsService = inject(SelectedSeatsService);
|
||||
|
||||
totalPrice = computed(() =>
|
||||
this.selectedSeatsService.getSelectedSeatsList().reduce((sum, seat) => sum + seat.row.category.price, 0)
|
||||
@@ -67,4 +87,138 @@ export class OrderComponent {
|
||||
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.loadingService.show();
|
||||
this.disableInputs()
|
||||
|
||||
const order = this.generateNewOrderObject(this.dataForm.value.email, false);
|
||||
const seats = this.selectedSeatsService.getSelectedSeatsList();
|
||||
const performance = this.performance()!;
|
||||
this.successful = true;
|
||||
this.sendToBackend(order, seats, performance);
|
||||
this.seatsReserved = true;
|
||||
}
|
||||
|
||||
makePurchase() {
|
||||
this.loadingService.show();
|
||||
this.disableInputs()
|
||||
|
||||
const order = this.generateNewOrderObject(this.dataForm.value.email, true);
|
||||
const seats = this.selectedSeatsService.getSelectedSeatsList();
|
||||
const performance = this.performance()!;
|
||||
this.successful = true;
|
||||
this.sendToBackend(order, seats, performance);
|
||||
this.seatsPurchased = true;
|
||||
}
|
||||
|
||||
createdOrder!: Bestellung;
|
||||
createdTickets!: Eintrittskarte[];
|
||||
|
||||
sendToBackend(order: Bestellung, seats: Sitzplatz[], performance: Vorstellung) {
|
||||
this.httpService.addOrder(order).pipe(
|
||||
tap(createdOrder => {
|
||||
this.createdOrder = createdOrder;
|
||||
|
||||
const ticketCreations = seats.map(seat => {
|
||||
const ticket = this.generateNewTicketObject(performance, seat, createdOrder);
|
||||
return this.httpService.addTicket(ticket);
|
||||
});
|
||||
|
||||
Promise.all(ticketCreations.map(obs => obs.toPromise()))
|
||||
.then(createdTickets => {
|
||||
this.createdTickets = createdTickets.filter(
|
||||
(ticket): ticket is Eintrittskarte => ticket !== undefined
|
||||
);
|
||||
|
||||
this.loadingService.hide();
|
||||
this.enableInputs();
|
||||
})
|
||||
.catch(err => {
|
||||
this.loadingService.showError(err);
|
||||
this.successful = false;
|
||||
console.error('Fehler beim Anlegen der Eintrittskarten', err);
|
||||
});
|
||||
}),
|
||||
catchError(err => {
|
||||
this.loadingService.showError(err);
|
||||
this.successful = false;
|
||||
console.error('Fehler beim Anlegen der Bestellung', err);
|
||||
return [];
|
||||
}),
|
||||
finalize(() => {
|
||||
this.enableInputs();
|
||||
})
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private generateCode(length: number = 6): string {
|
||||
const chars = "ABCDEFGHJKLMNPQRSUVWXYZ23456789";
|
||||
let result = "";
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const randomIndex = Math.floor(Math.random() * chars.length);
|
||||
result += chars[randomIndex];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private generateNewOrderObject(mail: string, isBooked: boolean): Bestellung {
|
||||
return{
|
||||
id: 0, // Wird durch Backend gesetzt
|
||||
mail: mail,
|
||||
code: this.generateCode(length=6),
|
||||
reserved: new Date(),
|
||||
booked: isBooked ? new Date() : null,
|
||||
cancelled: null,
|
||||
};
|
||||
}
|
||||
|
||||
private generateNewTicketObject(show: Vorstellung, seat: Sitzplatz, order: Bestellung): Eintrittskarte {
|
||||
return {
|
||||
id: 0, // Wird durch Backend gesetzt
|
||||
code: 'T' + this.generateCode(length=7),
|
||||
show: show,
|
||||
seat: seat,
|
||||
order: order
|
||||
};
|
||||
}
|
||||
|
||||
private disableInputs() {
|
||||
this.dataForm.disable();
|
||||
this.paymentForm.disable();
|
||||
this.isSubmitting = true;
|
||||
}
|
||||
|
||||
private enableInputs() {
|
||||
this.dataForm.enable();
|
||||
this.paymentForm.enable();
|
||||
this.isSubmitting = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user