-
- encrypted
-
-
- Ihre Zahlung wird sicher über unsere Partner verarbeitet.
Wir speichern keine Zahlungsinformationen.
+
+
+
+ Tickets gesamt:
+
+
+ {{ getPriceDisplay(totalPrice()) }}
-
-
-
+
+
+
- }
-
-
+
-
-}
+
+
+
+
+
+
+
+
+
+ }
diff --git a/src/app/order/order.component.ts b/src/app/order/order.component.ts
index 79dbc5f..8b13379 100644
--- a/src/app/order/order.component.ts
+++ b/src/app/order/order.component.ts
@@ -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
();
seatCategories = input.required();
- loadingService = inject(LoadingService);
- private httpService = inject(HttpService)
- private selectedSeatsService = inject(SelectedSeatsService);
+ paymentForm!: FormGroup;
+ dataForm!: FormGroup;
+
+ orderState = signal({ status: 'idle' });
+ submissionMode = signal(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)} €`;
+ }
}
diff --git a/src/app/purchase-failed/purchase-failed.component.html b/src/app/purchase-failed/purchase-failed.component.html
index 564513a..6cc5888 100644
--- a/src/app/purchase-failed/purchase-failed.component.html
+++ b/src/app/purchase-failed/purchase-failed.component.html
@@ -3,7 +3,7 @@
warning
Kauf fehlgeschlagen!
- Leider konnten Ihre Sitzplätze nicht gebucht werden.
Dies kann passieren, wenn andere Nutzer gleichzeitig versucht haben, dieselben Sitzplätze zu kaufen.
+ Leider konnten Ihre Sitzplätze nicht gekauft werden. Dies kann passieren, wenn andere Nutzer zeitgleich versucht haben, dieselben Sitzplätze zu kaufen.
diff --git a/src/app/reservation-failed/reservation-failed.component.html b/src/app/reservation-failed/reservation-failed.component.html
index 792a731..a8913c0 100644
--- a/src/app/reservation-failed/reservation-failed.component.html
+++ b/src/app/reservation-failed/reservation-failed.component.html
@@ -3,7 +3,7 @@
warning
Reservierung fehlgeschlagen!
- Leider konnten Ihre Sitzplätze nicht reserviert werden.
Dies kann passieren, wenn andere Nutzer gleichzeitig versucht haben, dieselben Sitzplätze zu reservieren.
+ Leider konnten Ihre Sitzplätze nicht reserviert werden. Dies kann passieren, wenn andere Nutzer gleichzeitig versucht haben, dieselben Sitzplätze zu reservieren.
diff --git a/src/app/seat-selection/seat-selection.component.ts b/src/app/seat-selection/seat-selection.component.ts
index 9c5ca14..6bab062 100644
--- a/src/app/seat-selection/seat-selection.component.ts
+++ b/src/app/seat-selection/seat-selection.component.ts
@@ -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(() =>
diff --git a/src/app/seat/seat.component.ts b/src/app/seat/seat.component.ts
index 5b48e2b..1a8dd58 100644
--- a/src/app/seat/seat.component.ts
+++ b/src/app/seat/seat.component.ts
@@ -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';
diff --git a/src/app/selected-seats.service.ts b/src/app/selected-seats.service.ts
index 07eb3a3..18a84ff 100644
--- a/src/app/selected-seats.service.ts
+++ b/src/app/selected-seats.service.ts
@@ -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([]);
- private seatIsSelectable: boolean = true;
- private commited = false;
- private debug = false;
- get selectedSeats() {
- return this.selectedSeatsSignal;
- }
+ private selectedSeatsSignal = signal([]);
+ 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);
+ }
}
diff --git a/src/app/theater-overlay/theater-overlay.component.ts b/src/app/theater-overlay/theater-overlay.component.ts
index dce8dcc..9050e31 100644
--- a/src/app/theater-overlay/theater-overlay.component.ts
+++ b/src/app/theater-overlay/theater-overlay.component.ts
@@ -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();
}