Add order and performance info components to ticket overlay

Introduces OrderComponent, PerformanceInfoComponent, and ShoppingCartComponent for the ticket purchase flow. Updates theater-overlay to display seat selection alongside order details and performance info. Refactors seat and performance data loading, improves UI structure, and enhances movie info display components for consistency.
This commit is contained in:
2025-11-12 10:33:34 +01:00
parent 5699f23540
commit f489073118
22 changed files with 234 additions and 39 deletions

View File

@@ -49,6 +49,9 @@ import { MovieImporterComponent } from './movie-importer/movie-importer.componen
import { MovieImportNoSearchResultComponent } from './movie-import-no-search-result/movie-import-no-search-result.component'; import { MovieImportNoSearchResultComponent } from './movie-import-no-search-result/movie-import-no-search-result.component';
import { MovieImportSearchInfoComponent } from './movie-import-search-info/movie-import-search-info.component'; import { MovieImportSearchInfoComponent } from './movie-import-search-info/movie-import-search-info.component';
import { LoginDialog } from './login/login.dialog'; import { LoginDialog } from './login/login.dialog';
import { PerformanceInfoComponent } from './performance-info/performance-info.component';
import { ShoppingCartComponent } from './shopping-cart/shopping-cart.component';
import { OrderComponent } from './order/order.component';
@NgModule({ @NgModule({
@@ -81,6 +84,9 @@ import { LoginDialog } from './login/login.dialog';
MovieImportNoSearchResultComponent, MovieImportNoSearchResultComponent,
MovieImportSearchInfoComponent, MovieImportSearchInfoComponent,
LoginDialog, LoginDialog,
PerformanceInfoComponent,
ShoppingCartComponent,
OrderComponent,
], ],
imports: [ imports: [
AppRoutingModule, AppRoutingModule,

View File

@@ -10,5 +10,13 @@
@if ( searchBar() ) { @if ( searchBar() ) {
<app-movie-search (movieSearchResult)="movieSearchResult.emit($event)"></app-movie-search> <app-movie-search (movieSearchResult)="movieSearchResult.emit($event)"></app-movie-search>
} }
@if ( backToSchedule() ) {
<div class="mt-1 mr-4">
<a [routerLink]="['/schedule']" class="opacity-50 hover:underline flex items-center gap-x-1">
<mat-icon style="font-size: 20px; width: 20px; height: 20px;">arrow_back</mat-icon>
<span>Zurück zur Programmübersicht</span>
</a>
</div>
}
</div> </div>
<mat-divider></mat-divider> <mat-divider></mat-divider>

View File

@@ -12,4 +12,6 @@ export class MenuHeaderComponent {
searchBar = input<boolean>(false); searchBar = input<boolean>(false);
movieSearchResult = output<string>(); movieSearchResult = output<string>();
backToSchedule = input<boolean>(false);
} }

View File

@@ -1,3 +1,3 @@
<span class="flex rounded-sm text-black text-sm px-2 py-1.5"> <span>
{{ category() }} {{ category() }}
</span> </span>

View File

@@ -1,4 +1,6 @@
<span class="flex items-center text-black text-sm rounded-sm px-2 py-1.5"> <span class="flex items-center">
<mat-icon class="mr-1" style="font-size: 20px; width: 20px; height: 20px;" fontIcon="schedule"></mat-icon> @if (showIcon()) {
<mat-icon class="mr-1" style="font-size: 20px; width: 20px; height: 20px;" fontIcon="schedule"></mat-icon>
}
{{ durationText() }} {{ durationText() }}
</span> </span>

View File

@@ -8,9 +8,13 @@ import { Component, input, computed } from '@angular/core';
}) })
export class MovieDurationComponent { export class MovieDurationComponent {
duration = input<number>(0); duration = input<number>(0);
showIcon = input<boolean>(true);
durationText = computed(() => { durationText = computed(() => {
if (this.duration() > 0) { if (this.duration() > 0) {
if (!this.showIcon()) {
return `${this.duration()} Minuten`;
}
return `${this.duration()} Min.`; return `${this.duration()} Min.`;
} }
return 'N/A'; return 'N/A';

View File

@@ -7,7 +7,7 @@
> >
</div> </div>
<div class="flex gap-1 justify-between"> <div class="flex gap-1 justify-between">
<app-movie-rating [rating]="movie().rating"></app-movie-rating> <app-movie-rating [rating]="movie().rating" class="text-black text-sm rounded-sm shadow-md px-2 py-1.5"></app-movie-rating>
<app-movie-duration [duration]="movie().duration"></app-movie-duration> <app-movie-duration [duration]="movie().duration" class="text-black text-sm rounded-sm px-2 py-1.5"></app-movie-duration>
<app-movie-category [category]="movie().category.name"></app-movie-category> <app-movie-category [category]="movie().category.name" class="text-black text-sm px-2 py-1.5"></app-movie-category>
</div> </div>

View File

@@ -1,3 +1,3 @@
<span [class]="ratingColor()" class="text-black flex rounded-sm shadow-md text-sm px-2 py-1.5"> <span>
{{ ratingText() }} {{ ratingText() }}
</span> </span>

View File

@@ -1,4 +1,4 @@
import { Component, input, computed } from '@angular/core'; import { Component, input, computed, HostBinding } from '@angular/core';
@Component({ @Component({
selector: 'app-movie-rating', selector: 'app-movie-rating',
@@ -7,6 +7,11 @@ import { Component, input, computed } from '@angular/core';
styleUrl: './movie-rating.component.css' styleUrl: './movie-rating.component.css'
}) })
export class MovieRatingComponent { export class MovieRatingComponent {
@HostBinding('class') get hostClasses(): string {
return this.ratingColor();
}
rating = input<number>(0); rating = input<number>(0);
ratingColor = computed(() => { ratingColor = computed(() => {

View File

@@ -0,0 +1,3 @@
mat-stepper {
background: transparent !important;
}

View File

@@ -0,0 +1,49 @@
<div class="w-full h-full">
<mat-stepper
orientation="horizontal"
linear="true"
#stepper
>
<mat-step [stepControl]="firstFormGroup">
<form [formGroup]="firstFormGroup">
<ng-template matStepLabel>Warenkorb</ng-template>
@if (performance()) {
<app-performance-info class="w-full h-50" [performance]="performance()!"></app-performance-info>
}
<div>
<button mat-button matStepperNext>Next</button>
</div>
</form>
</mat-step>
<mat-step [stepControl]="secondFormGroup">
<form [formGroup]="secondFormGroup">
<ng-template matStepLabel>Anschrift</ng-template>
<mat-form-field>
<mat-label>Address</mat-label>
<input
matInput
formControlName="secondCtrl"
placeholder="Ex. 1 Main St, New York, NY"
required
/>
</mat-form-field>
<div>
<button mat-button matStepperPrevious>Back</button>
<button mat-button matStepperNext>Next</button>
</div>
</form>
</mat-step>
-
<mat-step>
<ng-template matStepLabel>Zahlung</ng-template>
<p>You are now done.</p>
<div>
<button mat-button matStepperPrevious>Back</button>
<button mat-button (click)="stepper.reset()">Reset</button>
</div>
</mat-step>
</mat-stepper>
</div>

View File

@@ -0,0 +1,22 @@
import { Vorstellung } from '@infinimotion/model-frontend';
import { Component, inject, input } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-order',
standalone: false,
templateUrl: './order.component.html',
styleUrl: './order.component.css'
})
export class OrderComponent {
performance = input<Vorstellung>();
private _formBuilder = inject(FormBuilder);
firstFormGroup = this._formBuilder.group({
firstCtrl: ['', Validators.required],
});
secondFormGroup = this._formBuilder.group({
secondCtrl: ['', Validators.required],
});
}

View File

@@ -0,0 +1,14 @@
.info-box {
color: var(--mat-sys-on-surface);
}
::ng-deep .mat-step-header .mat-step-icon {
background-color: #ccc;
color: white;
}
::ng-deep .mat-step-header .mat-step-icon-selected,
::ng-deep .mat-step-icon-state-edit {
background-color: var(--color-indigo-500) !important;
}

View File

@@ -0,0 +1,21 @@
<div class="info-box bg-indigo-100 h-22 flex items-center space-x-3 rounded-md p-1 shadow-sm">
<div class="h-full">
<img
[src]="movie().image && movie().image !== 'N/A' ? movie().image : 'assets/poster_placeholder.png'"
alt="Movie Poster"
class="h-full w-auto shadow-xs rounded-sm"
(error)="onPosterError($event)"
>
</div>
<div class="text-md">
<h3 class="opacity-75">{{ getStartTimeString() }} • {{ performance().hall.name }}</h3>
<h1 class="font-semibold my-1">{{ movie().title }}</h1>
<div class="flex items-center">
<app-movie-rating [rating]="movie().rating" class="rounded-sm shadow-xs px-0.75 py-0.2 text-sm"></app-movie-rating>
<app-movie-duration [duration]="movie().duration" [showIcon]="false" class="ml-1.5 opacity-75"></app-movie-duration>
</div>
</div>
</div>

View File

@@ -0,0 +1,30 @@
import { Component, computed, input } from '@angular/core';
import { Vorstellung } from '@infinimotion/model-frontend';
@Component({
selector: 'app-performance-info',
standalone: false,
templateUrl: './performance-info.component.html',
styleUrl: './performance-info.component.css'
})
export class PerformanceInfoComponent {
performance = input.required<Vorstellung>();
getStartTimeString(): string {
const date = new Date(this.performance().start);
return date.toLocaleDateString('de-DE', { weekday: 'short' }) + '. ' + date.toLocaleDateString('de-DE') + ', ' + date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) + ' Uhr';
}
movie() {
return this.performance().movie
}
onPosterError(event: Event) {
const img = event.target as HTMLImageElement;
const placeholder = 'assets/poster_placeholder.png';
if (img.src !== window.location.origin + placeholder) {
img.src = placeholder;
}
}
}

View File

@@ -0,0 +1 @@
<p>shopping-cart works!</p>

View File

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

View File

@@ -0,0 +1,4 @@
:host {
border-radius: 0.5rem;
background-color: white;
}

View File

@@ -1,3 +1,10 @@
@for (row of seatsPerRow(); track $index) { <div class="m-auto w-200 h-10 bg-gray-300 mb-20" style="clip-path: polygon(0% 0%,100% 0%,90% 100%,10% 100%);">
<app-seat-row class="flex justify-center" [rowSeatList]="row"></app-seat-row> <p class="flex justify-center text-2xl fond-bold p-1">
} Leinwand
</p>
</div>
<div>
@for (row of seatsPerRow(); track $index) {
<app-seat-row class="flex justify-center" [rowSeatList]="row"></app-seat-row>
}
</div>

View File

@@ -1,9 +1,9 @@
<div class="m-auto w-200 h-10 bg-gray-300 mb-20" style="clip-path: polygon(0% 0%,100% 0%,90% 100%,10% 100%);"> <app-menu-header title="Vorstellungstickets kaufen" icon="local_activity" [backToSchedule]="true"></app-menu-header>
<p class="flex justify-center text-2xl fond-bold p-1">
Leinwand
</p>
</div>
<div class="">
<app-theater-layout [seatsPerRow]="seatsPerRow"></app-theater-layout>
</div>
<div class="flex justify-between h-max">
<app-theater-layout [seatsPerRow]="seatsPerRow" class="m-10 w-7/10"></app-theater-layout>
<app-order class="m-10 w-3/10" [performance]="performance"></app-order>
</div>

View File

@@ -1,8 +1,8 @@
import {Component, inject, OnInit} from '@angular/core'; import {Component, inject, OnInit} from '@angular/core';
import {HttpService} from '../http.service'; import {HttpService} from '../http.service';
import {LoadingService} from '../loading.service'; import {LoadingService} from '../loading.service';
import {catchError, of, tap} from 'rxjs'; import {catchError, forkJoin, of, tap} from 'rxjs';
import {Sitzplatz} from '@infinimotion/model-frontend'; import {Sitzplatz, Vorstellung} from '@infinimotion/model-frontend';
import {TheaterSeatState} from '../model/theater-seat-state.model'; import {TheaterSeatState} from '../model/theater-seat-state.model';
import {ActivatedRoute} from '@angular/router'; import {ActivatedRoute} from '@angular/router';
@@ -18,28 +18,34 @@ export class TheaterOverlayComponent implements OnInit {
showId!: number; showId!: number;
seatsPerRow: { seat: Sitzplatz, state: TheaterSeatState }[][] = [] seatsPerRow: { seat: Sitzplatz, state: TheaterSeatState }[][] = []
performance: Vorstellung | undefined;
constructor(private route: ActivatedRoute) {} constructor(private route: ActivatedRoute) {}
ngOnInit() { ngOnInit() {
this.showId = Number(this.route.snapshot.paramMap.get('id')!); this.showId = Number(this.route.snapshot.paramMap.get('id')!);
this.loadShowSeats(); this.loadPerformanceAndSeats();
} }
loadShowSeats() { loadPerformanceAndSeats() {
this.loading.show(); this.loading.show();
this.http.getSeatsByShowId(this.showId).pipe(
tap((data) => { forkJoin({
this.seatsPerRow = this.converter(data) performance: this.http.getPerformaceById(this.showId),
this.loading.hide(); seats: this.http.getSeatsByShowId(this.showId)
}), }).pipe(
catchError(err => { tap(({ performance, seats }) => {
this.loading.showError(err); this.performance = performance;
console.error('Fehler beim Laden der Vorstellung', err); this.seatsPerRow = this.converter(seats);
return of([]); this.loading.hide();
}) }),
).subscribe(); catchError(err => {
} this.loading.showError(err);
console.error('Fehler beim Laden', err);
return of({ performance: null, seats: [] });
})
).subscribe();
}
converter(resp: { seats: Sitzplatz[], reserved: Sitzplatz[], booked: Sitzplatz[] }): { converter(resp: { seats: Sitzplatz[], reserved: Sitzplatz[], booked: Sitzplatz[] }): {
seat: Sitzplatz, seat: Sitzplatz,