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/platform-browser": "^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",
"postcss": "^8.5.6",
"rxjs": "~7.8.0",
@@ -1298,9 +1298,9 @@
}
},
"node_modules/@infinimotion/model-frontend": {
"version": "0.0.85",
"resolved": "https://git.infinimotion.de/api/packages/infinimotion/npm/%40infinimotion%2Fmodel-frontend/-/0.0.85/model-frontend-0.0.85.tgz",
"integrity": "sha512-QPiZNl//Y1JdxtXk+VScc67h1K664z68PUCXRff9fRf4IHlYXtqutc+ainK8vxOVSqqL6EEmDAtbLsRwrG6kRg==",
"version": "0.0.89",
"resolved": "https://git.infinimotion.de/api/packages/infinimotion/npm/%40infinimotion%2Fmodel-frontend/-/0.0.89/model-frontend-0.0.89.tgz",
"integrity": "sha512-lvvQy8RWs41Bz52uBgsUKkwn8teGlgxlmG8Rvsgkh+v1IMVWFWVQmfMS7Rznd0lCZRgK1ByihH80X9eAN12idA==",
"license": "ISC"
},
"node_modules/@inquirer/ansi": {

View File

@@ -30,7 +30,7 @@
"@angular/material": "^20.2.9",
"@angular/platform-browser": "^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",
"postcss": "^8.5.6",
"rxjs": "~7.8.0",

View File

@@ -12,6 +12,7 @@ import { MatIconModule } from '@angular/material/icon';
import { MatTabsModule } from '@angular/material/tabs';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
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 { 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';
@NgModule({
@@ -87,6 +90,8 @@ import { OrderComponent } from './order/order.component';
PerformanceInfoComponent,
ShoppingCartComponent,
OrderComponent,
SeatSelectionComponent,
NoSeatsInHallComponent,
],
imports: [
AppRoutingModule,
@@ -98,6 +103,7 @@ import { OrderComponent } from './order/order.component';
MatTabsModule,
MatToolbarModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatSnackBarModule,
MatAutocompleteModule,
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 {
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">
@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
orientation="horizontal"
linear="true"
@@ -8,12 +19,37 @@
<form [formGroup]="firstFormGroup">
<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>
<button mat-button matStepperNext>Next</button>
<div class="mb-4 mt-2 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-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>
</form>
</mat-step>
@@ -46,4 +82,5 @@
</div>
</mat-step>
</mat-stepper>
}
</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 { FormBuilder, Validators } from '@angular/forms';
@@ -10,8 +11,10 @@ import { FormBuilder, Validators } from '@angular/forms';
})
export class OrderComponent {
performance = input<Vorstellung>();
seatCategories = input.required<Sitzkategorie[]>();
private _formBuilder = inject(FormBuilder);
loadingService = inject(LoadingService);
firstFormGroup = this._formBuilder.group({
firstCtrl: ['', Validators.required],

View File

@@ -11,9 +11,9 @@
<div class="text-md">
<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">
<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>
</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-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>

View File

@@ -2,7 +2,7 @@ import {Component, inject, OnInit} from '@angular/core';
import {HttpService} from '../http.service';
import {LoadingService} from '../loading.service';
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 {ActivatedRoute} from '@angular/router';
@@ -19,6 +19,7 @@ export class TheaterOverlayComponent implements OnInit {
showId!: number;
seatsPerRow: { seat: Sitzplatz, state: TheaterSeatState }[][] = []
performance: Vorstellung | undefined;
seatCategories: Sitzkategorie[] = [];
constructor(private route: ActivatedRoute) {}
@@ -52,13 +53,26 @@ loadPerformanceAndSeats() {
state: TheaterSeatState
}[][] {
let rows: { seat: Sitzplatz, state: TheaterSeatState }[][] = [];
const categoryMap = new Map<number, Sitzkategorie>();
resp.seats.forEach(seat => {
if (!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});
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.forEach(row => row.sort((a, b) => a.seat.position - b.seat.position));
return rows;

View File

@@ -8,6 +8,7 @@
<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/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet">
</head>
<body>
<app-root></app-root>