Add seat selection and no seats components

Introduces SeatSelectionComponent and NoSeatsInHallComponent for improved seat category display and handling cases with no available seats. Updates order flow to show seat categories, loading spinner, and total price. Refactors TheaterOverlayComponent to provide seat categories, and updates styles and dependencies.
This commit is contained in:
2025-11-13 01:48:28 +01:00
parent 8e4a024299
commit db0322d443
16 changed files with 131 additions and 16 deletions

8
package-lock.json generated
View File

@@ -16,7 +16,7 @@
"@angular/material": "^20.2.9", "@angular/material": "^20.2.9",
"@angular/platform-browser": "^20.3.0", "@angular/platform-browser": "^20.3.0",
"@angular/router": "^20.3.0", "@angular/router": "^20.3.0",
"@infinimotion/model-frontend": "^0.0.85", "@infinimotion/model-frontend": "^0.0.89",
"@tailwindcss/postcss": "^4.1.14", "@tailwindcss/postcss": "^4.1.14",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
@@ -1298,9 +1298,9 @@
} }
}, },
"node_modules/@infinimotion/model-frontend": { "node_modules/@infinimotion/model-frontend": {
"version": "0.0.85", "version": "0.0.89",
"resolved": "https://git.infinimotion.de/api/packages/infinimotion/npm/%40infinimotion%2Fmodel-frontend/-/0.0.85/model-frontend-0.0.85.tgz", "resolved": "https://git.infinimotion.de/api/packages/infinimotion/npm/%40infinimotion%2Fmodel-frontend/-/0.0.89/model-frontend-0.0.89.tgz",
"integrity": "sha512-QPiZNl//Y1JdxtXk+VScc67h1K664z68PUCXRff9fRf4IHlYXtqutc+ainK8vxOVSqqL6EEmDAtbLsRwrG6kRg==", "integrity": "sha512-lvvQy8RWs41Bz52uBgsUKkwn8teGlgxlmG8Rvsgkh+v1IMVWFWVQmfMS7Rznd0lCZRgK1ByihH80X9eAN12idA==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/@inquirer/ansi": { "node_modules/@inquirer/ansi": {

View File

@@ -30,7 +30,7 @@
"@angular/material": "^20.2.9", "@angular/material": "^20.2.9",
"@angular/platform-browser": "^20.3.0", "@angular/platform-browser": "^20.3.0",
"@angular/router": "^20.3.0", "@angular/router": "^20.3.0",
"@infinimotion/model-frontend": "^0.0.85", "@infinimotion/model-frontend": "^0.0.89",
"@tailwindcss/postcss": "^4.1.14", "@tailwindcss/postcss": "^4.1.14",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",

View File

@@ -12,6 +12,7 @@ import { MatIconModule } from '@angular/material/icon';
import { MatTabsModule } from '@angular/material/tabs'; import { MatTabsModule } from '@angular/material/tabs';
import { MatToolbarModule } from '@angular/material/toolbar'; import { MatToolbarModule } from '@angular/material/toolbar';
import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
@@ -52,6 +53,8 @@ import { LoginDialog } from './login/login.dialog';
import { PerformanceInfoComponent } from './performance-info/performance-info.component'; import { PerformanceInfoComponent } from './performance-info/performance-info.component';
import { ShoppingCartComponent } from './shopping-cart/shopping-cart.component'; import { ShoppingCartComponent } from './shopping-cart/shopping-cart.component';
import { OrderComponent } from './order/order.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';
@NgModule({ @NgModule({
@@ -87,6 +90,8 @@ import { OrderComponent } from './order/order.component';
PerformanceInfoComponent, PerformanceInfoComponent,
ShoppingCartComponent, ShoppingCartComponent,
OrderComponent, OrderComponent,
SeatSelectionComponent,
NoSeatsInHallComponent,
], ],
imports: [ imports: [
AppRoutingModule, AppRoutingModule,
@@ -98,6 +103,7 @@ import { OrderComponent } from './order/order.component';
MatTabsModule, MatTabsModule,
MatToolbarModule, MatToolbarModule,
MatProgressBarModule, MatProgressBarModule,
MatProgressSpinnerModule,
MatSnackBarModule, MatSnackBarModule,
MatAutocompleteModule, MatAutocompleteModule,
MatInputModule, MatInputModule,

View File

@@ -0,0 +1,7 @@
<div class="flex flex-col items-center justify-center py-8 text-gray-500 m-auto">
<mat-icon class="material-symbols-outlined text-6xl mb-4 opacity-50" style="font-size: 40px; width: 40px; height: 40px">
brightness_alert
</mat-icon>
<p class="text-lg">Huch?! Keine Sitzplätze?</p>
<p class="text-sm">Hast du ein Glück, Stehplätze sind kostenlos.</p>
</div>

View File

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

View File

@@ -1,3 +1,7 @@
mat-stepper { mat-stepper {
background: transparent !important; background: transparent !important;
} }
::ng-deep .mat-horizontal-stepper-header{
pointer-events: none !important;
}

View File

@@ -1,4 +1,15 @@
<div class="w-full h-full"> <div class="w-full h-full">
@if (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()) {
<mat-stepper <mat-stepper
orientation="horizontal" orientation="horizontal"
linear="true" linear="true"
@@ -8,12 +19,37 @@
<form [formGroup]="firstFormGroup"> <form [formGroup]="firstFormGroup">
<ng-template matStepLabel>Warenkorb</ng-template> <ng-template matStepLabel>Warenkorb</ng-template>
@if (performance()) { <app-performance-info class="w-full h-50" [performance]="performance()!"></app-performance-info>
<app-performance-info class="w-full h-50" [performance]="performance()!"></app-performance-info>
}
<div> <div class="mb-4 mt-2 p-2">
<button mat-button matStepperNext>Next</button>
@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-divider></mat-divider>
<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-gradient-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent">
75,00 €
</p>
</div>
<div class="flex space-x-5 mt-10">
<button mat-button matButton="outlined" matStepperNext class="w-1/2">Reservieren</button>
<button mat-button matButton="filled" matStepperNext class="w-1/2">Buchen</button>
</div> </div>
</form> </form>
</mat-step> </mat-step>
@@ -46,4 +82,5 @@
</div> </div>
</mat-step> </mat-step>
</mat-stepper> </mat-stepper>
}
</div> </div>

View File

@@ -1,4 +1,5 @@
import { Vorstellung } from '@infinimotion/model-frontend'; import { LoadingService } from './../loading.service';
import { Sitzkategorie, Vorstellung } from '@infinimotion/model-frontend';
import { Component, inject, input } from '@angular/core'; import { Component, inject, input } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms'; import { FormBuilder, Validators } from '@angular/forms';
@@ -10,8 +11,10 @@ import { FormBuilder, Validators } from '@angular/forms';
}) })
export class OrderComponent { export class OrderComponent {
performance = input<Vorstellung>(); performance = input<Vorstellung>();
seatCategories = input.required<Sitzkategorie[]>();
private _formBuilder = inject(FormBuilder); private _formBuilder = inject(FormBuilder);
loadingService = inject(LoadingService);
firstFormGroup = this._formBuilder.group({ firstFormGroup = this._formBuilder.group({
firstCtrl: ['', Validators.required], firstCtrl: ['', Validators.required],

View File

@@ -11,9 +11,9 @@
<div class="text-md"> <div class="text-md">
<h3 class="opacity-75">{{ getStartTimeString() }} • {{ performance().hall.name }}</h3> <h3 class="opacity-75">{{ getStartTimeString() }} • {{ performance().hall.name }}</h3>
<h1 class="font-semibold my-1">{{ movie().title }}</h1> <h1 class="font-semibold mb-1.5">{{ movie().title }}</h1>
<div class="flex items-center"> <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-rating [rating]="movie().rating" class="rounded-sm shadow-xs px-1 py-0.25 text-sm"></app-movie-rating>
<app-movie-duration [duration]="movie().duration" [showIcon]="false" class="ml-1.5 opacity-75"></app-movie-duration> <app-movie-duration [duration]="movie().duration" [showIcon]="false" class="ml-1.5 opacity-75"></app-movie-duration>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,3 @@
.seat-name {
color: var(--mat-sys-primary);
}

View File

@@ -0,0 +1,11 @@
<h2 class="seat-name mb-1">{{ seatCategory().name }}</h2>
<div class="flex items-center justify-between text-lg">
<div class="flex items-center space-x-2 w-3/7">
<mat-icon style="font-size: 30px; width: 30px; height: 30px">
{{ seatCategory().icon }}
</mat-icon>
<p>{{ getPriceDisplay(seatCategory().price) }}</p>
</div>
<p>×&thinsp;2</p>
<p class="w-2/7 text-right">25.00 €</p>
</div>

View File

@@ -0,0 +1,18 @@
import { Component, input } from '@angular/core';
import { Sitzkategorie } from '@infinimotion/model-frontend';
@Component({
selector: 'app-seat-selection',
standalone: false,
templateUrl: './seat-selection.component.html',
styleUrl: './seat-selection.component.css'
})
export class SeatSelectionComponent {
seatCategory = input.required<Sitzkategorie>();
amount: number = 1;
getPriceDisplay(price: number): string {
return `${(price / 100).toFixed(2)}`;
}
}

View File

@@ -4,6 +4,6 @@
<app-theater-layout [seatsPerRow]="seatsPerRow" class="m-10 w-7/10"></app-theater-layout> <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> <app-order class="m-10 w-3/10" [performance]="performance" [seatCategories]="seatCategories"></app-order>
</div> </div>

View File

@@ -2,7 +2,7 @@ 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, forkJoin, of, tap} from 'rxjs'; import {catchError, forkJoin, of, tap} from 'rxjs';
import {Sitzplatz, Vorstellung} from '@infinimotion/model-frontend'; import {Sitzkategorie, 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';
@@ -19,6 +19,7 @@ export class TheaterOverlayComponent implements OnInit {
showId!: number; showId!: number;
seatsPerRow: { seat: Sitzplatz, state: TheaterSeatState }[][] = [] seatsPerRow: { seat: Sitzplatz, state: TheaterSeatState }[][] = []
performance: Vorstellung | undefined; performance: Vorstellung | undefined;
seatCategories: Sitzkategorie[] = [];
constructor(private route: ActivatedRoute) {} constructor(private route: ActivatedRoute) {}
@@ -52,13 +53,26 @@ loadPerformanceAndSeats() {
state: TheaterSeatState state: TheaterSeatState
}[][] { }[][] {
let rows: { seat: Sitzplatz, state: TheaterSeatState }[][] = []; let rows: { seat: Sitzplatz, state: TheaterSeatState }[][] = [];
const categoryMap = new Map<number, Sitzkategorie>();
resp.seats.forEach(seat => { resp.seats.forEach(seat => {
if (!rows[seat.row.position]) { if (!rows[seat.row.position]) {
rows[seat.row.position] = []; rows[seat.row.position] = [];
} }
let state = resp.booked.find(other => other.id == seat.id) ? TheaterSeatState.BOOKED : resp.reserved.find(other => other.id == seat.id) ? TheaterSeatState.RESERVED : TheaterSeatState.AVAILABLE;
let state = resp.booked.find(other => other.id == seat.id) ? TheaterSeatState.BOOKED
: resp.reserved.find(other => other.id == seat.id) ? TheaterSeatState.RESERVED
: TheaterSeatState.AVAILABLE;
rows[seat.row.position].push({seat: seat, state: state}); rows[seat.row.position].push({seat: seat, state: state});
if (seat.row.category && !categoryMap.has(seat.row.category.id)) {
categoryMap.set(seat.row.category.id, seat.row.category);
}
}); });
this.seatCategories = Array.from(categoryMap.values()).sort((a, b) => a.id - b.id);
rows = rows.filter(row => row.length > 0).sort((a, b) => a[0].seat.row.position - b[0].seat.row.position); rows = rows.filter(row => row.length > 0).sort((a, b) => a[0].seat.row.position - b[0].seat.row.position);
rows.forEach(row => row.sort((a, b) => a.seat.position - b.seat.position)); rows.forEach(row => row.sort((a, b) => a.seat.position - b.seat.position));
return rows; return rows;

View File

@@ -8,6 +8,7 @@
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet">
</head> </head>
<body> <body>
<app-root></app-root> <app-root></app-root>