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:
2025-11-14 17:56:33 +01:00
parent f165a91e3c
commit 50cac8ac24
30 changed files with 821 additions and 107 deletions

View File

@@ -7,6 +7,7 @@ import { AppRoutingModule } from './app-routing-module';
import { App } from './app';
import { NgxMaskDirective, NgxMaskPipe, provideNgxMask } from 'ngx-mask';
import { QRCodeComponent } from 'angularx-qrcode';
import { MatIconModule } from '@angular/material/icon';
import { MatTabsModule } from '@angular/material/tabs';
@@ -55,6 +56,12 @@ import { ShoppingCartComponent } from './shopping-cart/shopping-cart.component';
import { OrderComponent } from './order/order.component';
import { SeatSelectionComponent } from './seat-selection/seat-selection.component';
import { NoSeatsInHallComponent } from './no-seats-in-hall/no-seats-in-hall.component';
import { ReservationSuccessComponent } from './reservation-success/reservation-success.component';
import { ReservationFailedComponent } from './reservation-failed/reservation-failed.component';
import { PurchaseSuccessComponent } from './purchase-success/purchase-success.component';
import { PurchaseFailedComponent } from './purchase-failed/purchase-failed.component';
import { TicketSmallComponent } from './ticket-small/ticket-small.component';
import { TicketListComponent } from './ticket-list/ticket-list.component';
@NgModule({
@@ -92,6 +99,12 @@ import { NoSeatsInHallComponent } from './no-seats-in-hall/no-seats-in-hall.comp
OrderComponent,
SeatSelectionComponent,
NoSeatsInHallComponent,
ReservationSuccessComponent,
ReservationFailedComponent,
PurchaseSuccessComponent,
PurchaseFailedComponent,
TicketSmallComponent,
TicketListComponent,
],
imports: [
AppRoutingModule,
@@ -119,6 +132,7 @@ import { NoSeatsInHallComponent } from './no-seats-in-hall/no-seats-in-hall.comp
MatStepperModule,
NgxMaskDirective,
NgxMaskPipe,
QRCodeComponent,
],
providers: [
provideBrowserGlobalErrorListeners(),

View File

@@ -13,7 +13,7 @@
Erscheinungsjahr: {{ movie().year }}
</h2>
<button matFab extended class="mb-3" (click)="importMovie(movie().imdbID, movie().title)" [disabled]="this.buttonDisabled">
<button matFab extended class="mb-3" (click)="importMovie(movie().imdbID!, movie().title!)" [disabled]="this.buttonDisabled">
<mat-icon>{{ buttonIcon }}</mat-icon>
{{ buttonText }}
</button>

View File

@@ -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>34 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>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" (click)="makePurchase()" class="w-2/3">
{{ getPriceDisplay(totalPrice()) }} jetzt bezahlen
</button>
</div>
}
</form>
</mat-step>

View File

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

View File

@@ -0,0 +1,11 @@
<div class="bg-red-100 text-red-500 rounded-md shadow-sm w-full h-fit p-6 py-8 items-center justify-center flex flex-col space-y-2">
<mat-icon class="material-symbols-outlined mb-5" style="font-size: 50px; width: 50px; height: 50px">
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>
<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>
</div>

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-purchase-failed',
standalone: false,
templateUrl: './purchase-failed.component.html',
styleUrl: './purchase-failed.component.css',
})
export class PurchaseFailedComponent {
}

View File

@@ -0,0 +1,11 @@
<div class="bg-green-200 rounded-md shadow-sm w-full h-fit p-6 py-8 items-center justify-center flex flex-col space-y-2">
<h1 class="text-xl font-bold">Vielen Dank für Ihren Einkauf!</h1>
<p class="text-center">Ihre Sitzplätze wurden erfolgreich gebucht.</p>
<app-ticket-list [tickets]="tickets()" class="w-8/10"></app-ticket-list>
<button mat-button disabled="true" matButton="filled" class="success-button w-80 mt-2">Tickets herunterladen</button>
<button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="success-button w-80">Zurück zur Programmauswahl</button>
</div>

View File

@@ -0,0 +1,12 @@
import { Eintrittskarte } from '@infinimotion/model-frontend';
import { Component, input } from '@angular/core';
@Component({
selector: 'app-purchase-success',
standalone: false,
templateUrl: './purchase-success.component.html',
styleUrl: './purchase-success.component.css',
})
export class PurchaseSuccessComponent {
tickets = input.required<Eintrittskarte[]>();
}

View File

@@ -0,0 +1,11 @@
<div class="bg-red-100 text-red-500 rounded-md shadow-sm w-full h-fit p-6 py-8 items-center justify-center flex flex-col space-y-2">
<mat-icon class="material-symbols-outlined mb-5" style="font-size: 50px; width: 50px; height: 50px">
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>
<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>
</div>

View File

@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-reservation-failed',
standalone: false,
templateUrl: './reservation-failed.component.html',
styleUrl: './reservation-failed.component.css',
})
export class ReservationFailedComponent {
}

View File

@@ -0,0 +1,17 @@
<div class="bg-green-200 rounded-md shadow-sm w-full h-fit p-6 py-8 items-center justify-center flex flex-col space-y-2">
<h1 class="text-xl font-bold">Reservierung erfolgreich!</h1>
<!-- <p class="text-center">Ihre Sitzplätze wurden reserviert. </p> -->
<p class="text-center">Ihre Sitzplätze wurden erfolgreich reserviert. Bitte nennen sie den folgenden Code an der Kasse, um Ihre Reservierung in eine Buchung umzuwandeln.</p>
<div class="bg-white text-5xl font-mono rounded-md shadow-sm w-fit h-fit p-4 py-2 my-4">
<strong>{{ order().code }}</strong>
</div>
<button routerLink="/schedule" mat-button matButton="filled" class="success-button mt-4 w-80">Zurück zur Programmauswahl</button>
<button [disabled]="true" mat-button matButton="outlined" color="accent" class="success-button w-80">Tickets jetzt online bezahlen</button>
<div class="text-green-500 cursor-pointer w-fit mt-1" (click)="cancelReservation()">
Reservierung stornieren
</div>
</div>

View File

@@ -0,0 +1,16 @@
import { Bestellung } from '@infinimotion/model-frontend';
import { Component, input } from '@angular/core';
@Component({
selector: 'app-reservation-success',
standalone: false,
templateUrl: './reservation-success.component.html',
styleUrl: './reservation-success.component.css',
})
export class ReservationSuccessComponent {
order = input.required<Bestellung>();
cancelReservation() {
// Logic to cancel the reservation
}
}

View File

@@ -0,0 +1,15 @@
@keyframes blink {
0% {
color: #6366f1;
}
50% {
color: #ffde05;
}
100% {
color: #6366f1;
}
}
.blink {
animation: blink 1s ease-in-out infinite;
}

View File

@@ -1,6 +1,11 @@
<button (click)="updateSelectedSeats(this.seat())" [disabled]="state() == TheaterSeatState.BOOKED || state() == TheaterSeatState.RESERVED || !seatService.getSeatIsSelected()" class="mx-0.5 hover:opacity-50">
<mat-icon [ngStyle]="{color: isSelectedAndAvaliable() ? '#6366f1': getSeatStateColor() }" style="font-size: 30px; width: 30px; height: 30px">
<button (click)="updateSelectedSeats(this.seat())" [disabled]="state() == TheaterSeatState.BOOKED || state() == TheaterSeatState.RESERVED || !seatService.getSeatIsSelectable()" class="mx-0.5">
<mat-icon
[class]="isHoverable()? 'hover:opacity-50' : ''"
[ngStyle]="{color: isSelectedAndAvaliable() ? '#6366f1': getSeatStateColor() }"
[style]="!seatService.getSeatIsSelectable()? 'transition: color 0.5s ease, transform 0.3s ease-in-out;' : ''"
style="font-size: 30px; width: 30px; height: 30px;">
{{ seat().row.category.icon }}
</mat-icon>
<!-- [ngClass]="{'blink': isSelectedAndAvaliable() && !seatService.getSeatIsSelectable()}" -->
</button>

View File

@@ -19,17 +19,29 @@ export class SeatComponent{
protected readonly TheaterSeatState = TheaterSeatState;
getSeatStateColor(): string {
if (!this.seatService.getSeatIsSelectable()) return 'gray'
switch (this.state()) {
case TheaterSeatState.RESERVED:
return 'orange';
return '#d6c9a9';
case TheaterSeatState.BOOKED:
return 'red';
return '#d9abab';
default:
case TheaterSeatState.AVAILABLE:
return 'black';
}
}
isHoverable(): boolean {
switch (this.state()) {
default:
case TheaterSeatState.AVAILABLE:
return this.seatService.getSeatIsSelectable();
case TheaterSeatState.RESERVED:
case TheaterSeatState.BOOKED:
return false;
}
}
updateSelectedSeats(selectedSeat: Sitzplatz) : void {
if(!this.selected){
this.seatService.pushSelectedSeat(selectedSeat);

View File

@@ -34,7 +34,7 @@ export class SelectedSeatsService {
this.selectedSeatsSignal.set([]);
}
getSeatIsSelected(): boolean{
getSeatIsSelectable(): boolean{
return this.seatIsSelectable;
}

View File

@@ -27,6 +27,7 @@ export class TheaterOverlayComponent implements OnInit {
ngOnInit() {
this.showId = Number(this.route.snapshot.paramMap.get('id')!);
this.selectedSeatService.clearSelectedSeatsList();
this.selectedSeatService.setSeatIsSelectableTrue();
this.loadPerformanceAndSeats();
}

View File

@@ -0,0 +1,41 @@
.ticket-container {
max-height: 200px;
overflow-y: auto;
padding: 8px;
position: relative;
}
.ticket-container::before,
.ticket-container::after {
content: '';
position: sticky;
left: 0;
right: 0;
height: 20px;
pointer-events: none;
z-index: 2;
}
.ticket-container::before {
top: 0;
background: linear-gradient(to bottom, rgba(0,0,0,0.1), transparent);
}
.ticket-container::after {
bottom: 0;
background: linear-gradient(to top, rgba(0,0,0,0.1), transparent);
}
.ticket-container::-webkit-scrollbar {
width: 8px;
}
.ticket-container::-webkit-scrollbar-track {
background: transparent;
}
.ticket-container::-webkit-scrollbar-thumb {
background-color: rgba(0,0,0,0.2);
border-radius: 4px;
}

View File

@@ -0,0 +1,9 @@
<div class="ticket-container">
@for (ticket of tickets(); track $index) {
<app-ticket-small [ticket]="ticket"></app-ticket-small>
@if ($index + 1 != tickets().length) {
<div class="h-2"></div>
}
}
</div>

View File

@@ -0,0 +1,12 @@
import { Component, input } from '@angular/core';
import { Eintrittskarte } from '@infinimotion/model-frontend';
@Component({
selector: 'app-ticket-list',
standalone: false,
templateUrl: './ticket-list.component.html',
styleUrl: './ticket-list.component.css',
})
export class TicketListComponent {
tickets = input.required<Eintrittskarte[]>();
}

View File

@@ -0,0 +1,16 @@
<div class="bg-white rounded-md shadow-sm w-full h-fit p-1 flex space-x-1 justify-between items-center">
<mat-icon class="opacity-50" style="font-size: 40px; width: 40px; height: 40px">
local_activity
</mat-icon>
<div class="flex flex-col flex-1 text-sm leading-tight ml-1">
<p>{{ticket().seat.row.category.name}} • Reihe {{convertIntoRowName(ticket().seat.row.position)}} Platz {{ticket().seat.position}}</p>
<div class="flex items-center space-x-2">
<p>Ticketcode:</p>
<p class="font-mono">
<strong>{{ ticket().code }}</strong>
</p>
</div>
</div>
<qrcode [qrdata]="ticket().code" [width]="42" [errorCorrectionLevel]="'M'"></qrcode>
</div>

View File

@@ -0,0 +1,16 @@
import { Eintrittskarte } from '@infinimotion/model-frontend';
import { Component, input } from '@angular/core';
@Component({
selector: 'app-ticket-small',
standalone: false,
templateUrl: './ticket-small.component.html',
styleUrl: './ticket-small.component.css',
})
export class TicketSmallComponent {
ticket = input.required<Eintrittskarte>();
convertIntoRowName(n: number): string {
return String.fromCharCode(64 + n);
}
}