Refactor order process to fix flicker bug

This commit is contained in:
2025-11-15 01:53:13 +01:00
parent 4ec3795697
commit e5707709bf
8 changed files with 366 additions and 337 deletions

View File

@@ -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>34 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>34 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>

View File

@@ -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)}`;
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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(() =>

View File

@@ -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';

View File

@@ -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);
}
}

View File

@@ -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();
}