From 3bc5b9cd3ac0d3ca7f60a9feead5ff49316470c2 Mon Sep 17 00:00:00 2001 From: Piet Ostendorp Date: Thu, 20 Nov 2025 23:19:16 +0100 Subject: [PATCH] Add cancellation and conversion error/success flows Introduces new components for cancellation and conversion error/success states, updates routing and UI to support resuming and cancelling orders, and refactors seat selection and order handling logic to accommodate these new flows. Also improves seat selection feedback and error handling throughout the checkout process. --- src/app/app-module.ts | 6 + src/app/app-routing-module.ts | 3 +- .../cancellation-failed.component.css | 0 .../cancellation-failed.component.html | 11 ++ .../cancellation-failed.component.ts | 11 ++ .../cancellation-success.component.css | 0 .../cancellation-success.component.html | 11 ++ .../cancellation-success.component.ts | 11 ++ .../conversion-failed.component.css | 0 .../conversion-failed.component.html | 11 ++ .../conversion-failed.component.ts | 11 ++ src/app/http.service.ts | 6 +- .../movie-performance.component.ts | 2 +- src/app/order/order.component.html | 92 +++++++------ src/app/order/order.component.ts | 108 ++++++++++++++-- .../reservation-success.component.html | 2 +- src/app/seat/seat.component.html | 2 +- src/app/seat/seat.component.ts | 29 +++-- src/app/selected-seats.service.ts | 16 +++ .../theater-overlay.component.html | 4 +- .../theater-overlay.component.ts | 122 ++++++++++++++---- 21 files changed, 365 insertions(+), 93 deletions(-) create mode 100644 src/app/cancellation-failed/cancellation-failed.component.css create mode 100644 src/app/cancellation-failed/cancellation-failed.component.html create mode 100644 src/app/cancellation-failed/cancellation-failed.component.ts create mode 100644 src/app/cancellation-success/cancellation-success.component.css create mode 100644 src/app/cancellation-success/cancellation-success.component.html create mode 100644 src/app/cancellation-success/cancellation-success.component.ts create mode 100644 src/app/conversion-failed/conversion-failed.component.css create mode 100644 src/app/conversion-failed/conversion-failed.component.html create mode 100644 src/app/conversion-failed/conversion-failed.component.ts diff --git a/src/app/app-module.ts b/src/app/app-module.ts index a8554ec..c625000 100644 --- a/src/app/app-module.ts +++ b/src/app/app-module.ts @@ -65,6 +65,9 @@ import { TicketSmallComponent } from './ticket-small/ticket-small.component'; import { TicketListComponent } from './ticket-list/ticket-list.component'; import { ZoomWarningComponent } from './zoom-warning/zoom-warning.component'; import { SelectionConflictInfoComponent } from './selection-conflict-info/selection-conflict-info.component'; +import { CancellationSuccessComponent } from './cancellation-success/cancellation-success.component'; +import { CancellationFailedComponent } from './cancellation-failed/cancellation-failed.component'; +import { ConversionFailedComponent } from './conversion-failed/conversion-failed.component'; @NgModule({ @@ -109,6 +112,9 @@ import { SelectionConflictInfoComponent } from './selection-conflict-info/select TicketListComponent, ZoomWarningComponent, SelectionConflictInfoComponent, + CancellationSuccessComponent, + CancellationFailedComponent, + ConversionFailedComponent, ], imports: [ AppRoutingModule, diff --git a/src/app/app-routing-module.ts b/src/app/app-routing-module.ts index e5692a4..1678c5f 100644 --- a/src/app/app-routing-module.ts +++ b/src/app/app-routing-module.ts @@ -27,7 +27,8 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { roles: ['admin'] }, // Array von erlaubten Rollen. Derzeit gäbe es 'admin' und 'employee' }, - { path: 'performance/:performanceId/checkout', component: TheaterOverlayComponent}, + { path: 'checkout/performance/:performanceId', component: TheaterOverlayComponent}, + { path: 'checkout/order/:orderId', component: TheaterOverlayComponent}, ], }, diff --git a/src/app/cancellation-failed/cancellation-failed.component.css b/src/app/cancellation-failed/cancellation-failed.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/cancellation-failed/cancellation-failed.component.html b/src/app/cancellation-failed/cancellation-failed.component.html new file mode 100644 index 0000000..0bd5a1d --- /dev/null +++ b/src/app/cancellation-failed/cancellation-failed.component.html @@ -0,0 +1,11 @@ +
+ + warning + +

Stornierung fehlgeschlagen!

+

Leider konnten Ihre Sitzplätze nicht storniert werden. Möglicherweise wurden die Tickets bereits bezahlt oder storniert.

+ + + +
+ diff --git a/src/app/cancellation-failed/cancellation-failed.component.ts b/src/app/cancellation-failed/cancellation-failed.component.ts new file mode 100644 index 0000000..a6e7519 --- /dev/null +++ b/src/app/cancellation-failed/cancellation-failed.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-cancellation-failed', + standalone: false, + templateUrl: './cancellation-failed.component.html', + styleUrl: './cancellation-failed.component.css', +}) +export class CancellationFailedComponent { + +} diff --git a/src/app/cancellation-success/cancellation-success.component.css b/src/app/cancellation-success/cancellation-success.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/cancellation-success/cancellation-success.component.html b/src/app/cancellation-success/cancellation-success.component.html new file mode 100644 index 0000000..d7d1c22 --- /dev/null +++ b/src/app/cancellation-success/cancellation-success.component.html @@ -0,0 +1,11 @@ +
+ + task_alt + +

Stornierung erfolgreich!

+

Ihre Sitzplätze wurden erfolgreich storniert und stehen wieder zur Buchung zur Verfügnug.

+ + + + +
diff --git a/src/app/cancellation-success/cancellation-success.component.ts b/src/app/cancellation-success/cancellation-success.component.ts new file mode 100644 index 0000000..75e645f --- /dev/null +++ b/src/app/cancellation-success/cancellation-success.component.ts @@ -0,0 +1,11 @@ +import { Component, input } from '@angular/core'; + +@Component({ + selector: 'app-cancellation-success', + standalone: false, + templateUrl: './cancellation-success.component.html', + styleUrl: './cancellation-success.component.css', +}) +export class CancellationSuccessComponent { + performanceId = input.required(); +} diff --git a/src/app/conversion-failed/conversion-failed.component.css b/src/app/conversion-failed/conversion-failed.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/conversion-failed/conversion-failed.component.html b/src/app/conversion-failed/conversion-failed.component.html new file mode 100644 index 0000000..d822ae3 --- /dev/null +++ b/src/app/conversion-failed/conversion-failed.component.html @@ -0,0 +1,11 @@ +
+ + warning + +

Kauf fehlgeschlagen!

+

Leider konnten Ihre Sitzplätze nicht bezahlt werden. Möglicherweise wurden die Tickets bereits storniert.

+ + + +
+ diff --git a/src/app/conversion-failed/conversion-failed.component.ts b/src/app/conversion-failed/conversion-failed.component.ts new file mode 100644 index 0000000..9ba4c68 --- /dev/null +++ b/src/app/conversion-failed/conversion-failed.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-conversion-failed', + standalone: false, + templateUrl: './conversion-failed.component.html', + styleUrl: './conversion-failed.component.css', +}) +export class ConversionFailedComponent { + +} diff --git a/src/app/http.service.ts b/src/app/http.service.ts index ede6c37..3db7819 100644 --- a/src/app/http.service.ts +++ b/src/app/http.service.ts @@ -33,9 +33,9 @@ export class HttpService { return this.http.post(`${this.baseUrl}bestellung`, order); } - /* PUT /api/bestellung/{id} */ - updateOrder(id: number, order: Partial): Observable { - return this.http.put(`${this.baseUrl}bestellung/${id}`, order); + /* PUT /api/bestellung */ + updateOrder(order: Partial): Observable { + return this.http.put(`${this.baseUrl}bestellung`, order); } /* DELETE /api/bestellung/{id} */ diff --git a/src/app/movie-performance/movie-performance.component.ts b/src/app/movie-performance/movie-performance.component.ts index 6f98ff9..217cec6 100644 --- a/src/app/movie-performance/movie-performance.component.ts +++ b/src/app/movie-performance/movie-performance.component.ts @@ -15,7 +15,7 @@ export class MoviePerformanceComponent implements OnInit { route: string = ''; ngOnInit() { - this.route = `../performance/${this.id()}/checkout`; + this.route = `../checkout/performance/${this.id()}`; } startTime = computed(() => diff --git a/src/app/order/order.component.html b/src/app/order/order.component.html index 5e8dffd..f9a2b58 100644 --- a/src/app/order/order.component.html +++ b/src/app/order/order.component.html @@ -27,47 +27,47 @@ } - - - Warenkorb + + + Warenkorb -
+
- @if (selectedSeatsService.hadConflict()) { - + @if (selectedSeatsService.hadConflict()) { + + } + + +
+ @for (seatCategory of seatCategories(); track $index) { +
+ } + @empty { + + } +
- -
- @for (seatCategory of seatCategories(); track $index) { -
- - } - @empty { - - } -
+ - + +
+

+ Tickets gesamt: +

+

+ {{ getPriceDisplay(totalPrice()) }} +

+
- -
-

- Tickets gesamt: -

-

- {{ getPriceDisplay(totalPrice()) }} -

-
- - -
- - -
+ +
+ + +
- +
Anschrift @@ -129,6 +129,18 @@
} + @else if (isConversionError() && !isSubmitting()) { +
+ + } + @else if (isCancellationSuccess() && !isSubmitting() && performance()) { +
+ + } + @else if (isCancellationError() && !isSubmitting()) { +
+ + } @else { @@ -183,9 +195,17 @@
- + + @if (isResuming()) { + + } @else { + + } + diff --git a/src/app/order/order.component.ts b/src/app/order/order.component.ts index 4d2665d..e3aa851 100644 --- a/src/app/order/order.component.ts +++ b/src/app/order/order.component.ts @@ -15,7 +15,10 @@ type OrderState = | { status: 'reservation-success'; order: Bestellung } | { status: 'reservation-error'; error: any } | { status: 'purchase-success'; tickets: Eintrittskarte[] } - | { status: 'purchase-error'; error: any }; + | { status: 'purchase-error'; error: any } + | { status: 'conversion-error'; error: any } + | { status: 'cancellation-success'; } + | { status: 'cancellation-error'; error: any }; type SubmissionMode = 'reservation' | 'purchase'; @@ -36,6 +39,9 @@ export class OrderComponent { performance = input(); seatCategories = input.required(); + existingOrder = input(); + existingTickets = input(); + stepChanged = output(); paymentForm!: FormGroup; @@ -52,6 +58,16 @@ export class OrderComponent { isSubmitting = computed(() => this.orderState().status === 'submitting'); + isResuming = computed(() => { + const order = this.existingOrder(); + const tickets = this.existingTickets(); + + if (!order || !tickets || tickets.length === 0) { + return false; + } + return true; + }); + secondPhaseButtonText = computed(() => { const mode = this.submissionMode(); if (!mode) return 'Loading...'; @@ -70,6 +86,10 @@ export class OrderComponent { this.orderState().status === 'purchase-success' ); + isCancellationSuccess = computed(() => + this.orderState().status === 'cancellation-success' + ); + isReservationError = computed(() => this.orderState().status === 'reservation-error' ); @@ -78,6 +98,14 @@ export class OrderComponent { this.orderState().status === 'purchase-error' ); + isConversionError = computed(() => + this.orderState().status === 'conversion-error' + ); + + isCancellationError = computed(() => + this.orderState().status === 'cancellation-error' + ); + createdOrder = computed(() => { const state = this.orderState(); return state.status === 'reservation-success' ? state.order : null; @@ -138,7 +166,6 @@ export class OrderComponent { makeReservation() { this.orderState.set({ status: 'submitting' }); - this.loadingService.show(); this.disableForms(); const order = this.generateNewOrderObject(this.dataForm.value.email, false); @@ -157,15 +184,51 @@ export class OrderComponent { this.loadingService.show(); this.disableForms(); - const order = this.generateNewOrderObject(this.dataForm.value.email, true); - const seats = this.selectedSeatsService.selectedSeats(); - const performance = this.performance()!; + if (this.isResuming()) { + const order = this.existingOrder()!; + order.booked = new Date(); + this.convertOrder(order, this.existingTickets()!); + } else { + const order = this.generateNewOrderObject(this.dataForm.value.email, true); + const seats = this.selectedSeatsService.selectedSeats(); + const performance = this.performance()!; - this.submitOrder(order, seats, performance, 'purchase'); + this.submitOrder(order, seats, performance, 'purchase'); + } } + private convertOrder(order: Bestellung, tickets: Eintrittskarte[]) { + this.loadingService.show(); + this.httpService.updateOrder(order).pipe( + tap(() => { + // Success Handling + this.orderState.set({ + status: 'purchase-success', + tickets: tickets + }); - submitOrder(order: Bestellung, seats: Sitzplatz[], performance: Vorstellung, mode: SubmissionMode) { + this.selectedSeatsService.commit(); + this.loadingService.hide(); + this.showConfetti(); + }), + catchError(err => { + // Error handling + this.selectedSeatsService.error(); + this.loadingService.showError(err); + console.error('Fehler bei der Umwandlung der Bestellung:', err); + this.orderState.set({status: 'conversion-error', error: err}); + + return EMPTY; + }), + finalize(() => { + this.enableForms(); + }), + takeUntilDestroyed(this.destroyRef) + ).subscribe(); + } + + private submitOrder(order: Bestellung, seats: Sitzplatz[], performance: Vorstellung, mode: SubmissionMode) { + this.loadingService.hide(); // Tickets anlegen const tickets = seats.map(seat => { @@ -194,8 +257,9 @@ export class OrderComponent { }), catchError(err => { // Error handling - console.error('Fehler beim Anlegen der Bestellung/Tickets:', err); + this.selectedSeatsService.error(); this.loadingService.showError(err); + console.error('Fehler beim Anlegen der Bestellung/Tickets:', err); if (mode === 'reservation') { this.orderState.set({ status: 'reservation-error', error: err }); @@ -260,6 +324,34 @@ export class OrderComponent { }; } + cancelReservation() { + const order = this.existingOrder()!; + order.cancelled = new Date(); + + this.loadingService.show(); + this.httpService.updateOrder(order).pipe( + tap(() => { + // Success Handling + this.orderState.set({ + status: 'cancellation-success' + }); + + this.selectedSeatsService.cancel(); + this.loadingService.hide(); + }), + catchError(err => { + // Error handling + this.selectedSeatsService.error(); + this.loadingService.showError(err); + console.error('Fehler bei der Bezahlung der Bestellung:', err); + this.orderState.set({status: 'cancellation-error', error: err}); + + return EMPTY; + }), + takeUntilDestroyed(this.destroyRef) + ).subscribe(); + } + private disableForms(): void { this.dataForm.disable(); this.paymentForm.disable(); diff --git a/src/app/reservation-success/reservation-success.component.html b/src/app/reservation-success/reservation-success.component.html index 8d28e59..a8c961c 100644 --- a/src/app/reservation-success/reservation-success.component.html +++ b/src/app/reservation-success/reservation-success.component.html @@ -6,7 +6,7 @@ {{ order().code }}
- +
Reservierung stornieren diff --git a/src/app/seat/seat.component.html b/src/app/seat/seat.component.html index 975c099..f4a1015 100644 --- a/src/app/seat/seat.component.html +++ b/src/app/seat/seat.component.html @@ -1,4 +1,4 @@ -