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:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -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": {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
7
src/app/no-seats-in-hall/no-seats-in-hall.component.html
Normal file
7
src/app/no-seats-in-hall/no-seats-in-hall.component.html
Normal 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>
|
||||||
11
src/app/no-seats-in-hall/no-seats-in-hall.component.ts
Normal file
11
src/app/no-seats-in-hall/no-seats-in-hall.component.ts
Normal 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 {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
mat-stepper {
|
mat-stepper {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::ng-deep .mat-horizontal-stepper-header{
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
3
src/app/seat-selection/seat-selection.component.css
Normal file
3
src/app/seat-selection/seat-selection.component.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.seat-name {
|
||||||
|
color: var(--mat-sys-primary);
|
||||||
|
}
|
||||||
11
src/app/seat-selection/seat-selection.component.html
Normal file
11
src/app/seat-selection/seat-selection.component.html
Normal 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>× 2</p>
|
||||||
|
<p class="w-2/7 text-right">25.00 €</p>
|
||||||
|
</div>
|
||||||
18
src/app/seat-selection/seat-selection.component.ts
Normal file
18
src/app/seat-selection/seat-selection.component.ts
Normal 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)} €`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user