merging main

This commit is contained in:
Marcel-Anker
2025-11-19 11:11:23 +01:00
18 changed files with 279 additions and 30 deletions

View File

@@ -66,8 +66,6 @@ import { PurchaseSuccessComponent } from './purchase-success/purchase-success.co
import { PurchaseFailedComponent } from './purchase-failed/purchase-failed.component'; import { PurchaseFailedComponent } from './purchase-failed/purchase-failed.component';
import { TicketSmallComponent } from './ticket-small/ticket-small.component'; import { TicketSmallComponent } from './ticket-small/ticket-small.component';
import { TicketListComponent } from './ticket-list/ticket-list.component'; import { TicketListComponent } from './ticket-list/ticket-list.component';
import { StatisticsComponent } from './statistics/statistics.component';
@NgModule({ @NgModule({
@@ -111,7 +109,6 @@ import { StatisticsComponent } from './statistics/statistics.component';
PurchaseFailedComponent, PurchaseFailedComponent,
TicketSmallComponent, TicketSmallComponent,
TicketListComponent, TicketListComponent,
StatisticsComponent,
], ],
imports: [ imports: [
AppRoutingModule, AppRoutingModule,

View File

@@ -13,7 +13,7 @@ import {StatisticsComponent} from './statistics/statistics.component';
const routes: Routes = [ const routes: Routes = [
// Seiten ohne Layout // Seiten ohne Layout
{ path: 'landing', component: HomeComponent }, { path: 'landing', component: HomeComponent },
{ path: 'poc-model', component: PocModelComponent }, { path: 'poc-model', component: PocModelComponent, data: { allowMobile: true } },
// Seiten mit MainLayout // Seiten mit MainLayout
{ {
@@ -29,12 +29,6 @@ const routes: Routes = [
data: { roles: ['admin'] }, // Array von erlaubten Rollen. Derzeit gäbe es 'admin' und 'employee' data: { roles: ['admin'] }, // Array von erlaubten Rollen. Derzeit gäbe es 'admin' und 'employee'
}, },
{ path: 'selection/performance/:id', component: TheaterOverlayComponent}, { path: 'selection/performance/:id', component: TheaterOverlayComponent},
{
path: 'admin/statistics',
component: StatisticsComponent,
canActivate: [AuthGuard],
data: { roles: ['admin'] }, // Array von erlaubten Rollen. Derzeit gäbe es 'admin' und 'employee'
},
], ],
}, },

View File

@@ -1 +1,2 @@
<app-zoom-warning></app-zoom-warning>
<router-outlet /> <router-outlet />

View File

@@ -0,0 +1,29 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class DeviceDetectionService {
private _isMobile: boolean;
constructor() {
this._isMobile = this.checkIfMobile();
}
isMobile(): boolean {
return this._isMobile;
}
private checkIfMobile(): boolean {
const userAgent = navigator.userAgent.toLowerCase();
const isMobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent);
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const isSmallScreen = window.innerWidth < 768;
return isMobileUA || (isTouchDevice && isSmallScreen);
}
recheckDevice(): void {
this._isMobile = this.checkIfMobile();
}
}

View File

@@ -1,6 +1,6 @@
<a [routerLink]="route" class="bg-gray-200 m-2 flex flex-col items-center justify-between rounded-md overflow-hidden text-xl shadow-lg transform transition-all duration-300 hover:scale-105"> <a [routerLink]="route" class="bg-gray-200 m-2 flex flex-col items-center justify-between rounded-md overflow-hidden text-xl shadow-lg transform transition-all duration-300 hover:scale-105">
<div class="bg-gradient-to-r from-indigo-500 to-pink-600 w-full text-center text-white font-medium rounded-t-md py-0.5 px-2"> <div class="bg-linear-to-r from-indigo-500 to-pink-600 w-full text-center text-white font-medium rounded-t-md py-0.5 px-2">
<p>{{ hall() }}</p> <p>{{ hall() }}</p>
</div> </div>

View File

@@ -15,7 +15,7 @@ export class MoviePerformanceComponent implements OnInit {
route: string = ''; route: string = '';
ngOnInit() { ngOnInit() {
this.route = `../selection/performance/${this.id()}`; this.route = `../performance/${this.id()}/checkout`;
} }
startTime = computed(() => startTime = computed(() =>

View File

@@ -13,7 +13,7 @@
<h3 class="opacity-75">{{ getStartTimeString() }} • {{ performance().hall.name }}</h3> <h3 class="opacity-75">{{ getStartTimeString() }} • {{ performance().hall.name }}</h3>
<h1 class="font-semibold mb-0.5">{{ movie().title }}</h1> <h1 class="font-semibold mb-0.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-1 py-0.25 text-sm"></app-movie-rating> <app-movie-rating [rating]="movie().rating" class="rounded-sm shadow-xs px-1 py-px 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

@@ -6,6 +6,6 @@
<p class="text-center">Leider konnten Ihre Sitzplätze nicht gekauft werden. Dies kann passieren, wenn andere Nutzer zeitgleich versucht haben, dieselben Sitzplätze zu kaufen.</p> <p class="text-center">Leider konnten Ihre Sitzplätze nicht gekauft werden. Dies kann passieren, wenn andere Nutzer zeitgleich versucht haben, dieselben Sitzplätze zu kaufen.</p>
<button mat-button matButton="filled" class="error-button mt-4 w-80">Erneut versuchen</button> <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> <button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="error-button w-80 mt-1">Zurück zur Programmauswahl</button>
</div> </div>

View File

@@ -2,10 +2,10 @@
<h1 class="text-xl font-bold">Vielen Dank für Ihren Einkauf!</h1> <h1 class="text-xl font-bold">Vielen Dank für Ihren Einkauf!</h1>
<p class="text-center">Ihre Sitzplätze wurden erfolgreich gebucht.</p> <p class="text-center">Ihre Sitzplätze wurden erfolgreich gebucht.</p>
<app-ticket-list [tickets]="tickets()" class="w-8/10"></app-ticket-list> <app-ticket-list [tickets]="tickets()" class="w-8/10 my-4"></app-ticket-list>
<button mat-button disabled="true" matButton="filled" class="success-button w-80 mt-2">Tickets herunterladen</button> <button mat-button disabled="true" matButton="filled" class="success-button w-80 mt-4">Tickets herunterladen</button>
<button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="success-button w-80">Zurück zur Programmauswahl</button> <button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="success-button w-80 mt-1">Zurück zur Programmauswahl</button>
</div> </div>

View File

@@ -6,6 +6,6 @@
<p class="text-center">Leider konnten Ihre Sitzplätze nicht reserviert werden. Dies kann passieren, wenn andere Nutzer gleichzeitig versucht haben, dieselben Sitzplätze zu reservieren.</p> <p class="text-center">Leider konnten Ihre Sitzplätze nicht reserviert werden. 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 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> <button routerLink="/schedule" mat-button matButton="outlined" color="accent" class="error-button w-80 mt-1">Zurück zur Programmauswahl</button>
</div> </div>

View File

@@ -1,17 +1,15 @@
<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"> <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> <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> <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"> <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> <strong>{{ order().code }}</strong>
</div> </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="filled" color="accent" class="success-button mt-2 w-80">Tickets jetzt online bezahlen</button>
<button [disabled]="true" mat-button matButton="outlined" color="accent" class="success-button w-80">Tickets jetzt online bezahlen</button> <button routerLink="/schedule" mat-button matButton="outlined" class="success-button mb-4 w-80 mt-1">Zurück zur Programmauswahl</button>
<div class="text-green-500 cursor-pointer w-fit mt-1" (click)="cancelReservation()"> <div class="text-green-500 cursor-pointer w-fit mt-2" (click)="cancelReservation()">
Reservierung stornieren Reservierung stornieren
</div> </div>
</div> </div>

View File

@@ -3,12 +3,12 @@
Leinwand Leinwand
</p> </p>
</div> </div>
<div> <div class="mb-5">
@for (row of seatsPerRow(); track $index) { @for (row of seatsPerRow(); track $index) {
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<!-- Speaker --> <!-- Speaker -->
<div class="shrink-0 pl-25"> <div class="shrink-0 pl-20">
@if ($index % 4 === 0) { @if ($index % 4 === 0) {
<mat-icon class="material-symbols-outlined opacity-25" style="font-size: 30px; width: 30px; height: 30px"> <mat-icon class="material-symbols-outlined opacity-25" style="font-size: 30px; width: 30px; height: 30px">
speaker speaker
@@ -25,7 +25,7 @@
<app-seat-row class="flex justify-center" [rowSeatList]="row"></app-seat-row> <app-seat-row class="flex justify-center" [rowSeatList]="row"></app-seat-row>
<!-- Speaker --> <!-- Speaker -->
<div class="shrink-0 pr-25"> <div class="shrink-0 pr-20">
@if ($index % 4 === 0) { @if ($index % 4 === 0) {
<mat-icon class="material-symbols-outlined opacity-25 mirrored" style="font-size: 30px; width: 30px; height: 30px"> <mat-icon class="material-symbols-outlined opacity-25 mirrored" style="font-size: 30px; width: 30px; height: 30px">
speaker speaker

View File

@@ -1,8 +1,8 @@
<app-menu-header label="Vorstellungstickets kaufen" icon="local_activity" [backToSchedule]="true"></app-menu-header> <app-menu-header label="Vorstellungstickets kaufen" icon="local_activity" [backToSchedule]="true"></app-menu-header>
<div class="flex justify-between h-100"> <div class="flex h-fit">
<div class="w-7/10 p-10 h-188"> <div class="w-7/10 p-10 h-fit">
<div> <div>
@if (!performance && (loading.loading$ | async)){ @if (!performance && (loading.loading$ | async)){
<div class="w-full h-full flex items-center justify-center mt-70"> <div class="w-full h-full flex items-center justify-center mt-70">
@@ -18,6 +18,6 @@
</div> </div>
</div> </div>
<app-order class="m-10 mr-20 w-3/10" [performance]="performance" [seatCategories]="seatCategories"></app-order> <app-order class="mt-10 mr-30 w-3/10" [performance]="performance" [seatCategories]="seatCategories"></app-order>
</div> </div>

View File

@@ -25,6 +25,7 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
readonly loading = inject(LoadingService); readonly loading = inject(LoadingService);
showId!: number; showId!: number;
orderId?: string;
seatsPerRow = signal<{ seat: Sitzplatz | null, state: TheaterSeatState | null }[][]>([]); seatsPerRow = signal<{ seat: Sitzplatz | null, state: TheaterSeatState | null }[][]>([]);
performance: Vorstellung | undefined; performance: Vorstellung | undefined;
seatCategories: Sitzkategorie[] = []; seatCategories: Sitzkategorie[] = [];
@@ -33,7 +34,8 @@ export class TheaterOverlayComponent implements OnInit, OnDestroy {
private isInitialLoad = signal(true); private isInitialLoad = signal(true);
ngOnInit() { ngOnInit() {
this.showId = Number(this.route.snapshot.paramMap.get('id')!); this.showId = Number(this.route.snapshot.paramMap.get('performanceId')!);
this.orderId = this.route.snapshot.queryParams['paramName'];
this.selectedSeatService.clearSelection(); this.selectedSeatService.clearSelection();
this.selectedSeatService.setSeatSelectable(true); this.selectedSeatService.setSeatSelectable(true);

View File

@@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, fromEvent } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class ZoomDetectionService {
private zoomLevel$ = new BehaviorSubject<number>(this.getZoomLevel());
constructor() {
// Zoom-Änderungen überwachen
fromEvent(window, 'resize')
.pipe(debounceTime(200))
.subscribe(() => {
this.zoomLevel$.next(this.getZoomLevel());
});
}
getZoomLevel(): number {
const devicePixelRatio = window.devicePixelRatio || 1;
return devicePixelRatio;
}
getZoomLevel$() {
return this.zoomLevel$.asObservable();
}
isZoomOutOfRange(minZoom: number = 0.95, maxZoom: number = 1.05): boolean {
const currentZoom = this.getZoomLevel();
return currentZoom < minZoom || currentZoom > maxZoom;
}
}

View File

@@ -0,0 +1,14 @@
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px) scale(0.9);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.zoom-info-box {
animation: slideIn 0.3s ease-out;
}

View File

@@ -0,0 +1,81 @@
@if (isOutOfRange && !isDismissed && !isMobile) {
<div class="zoom-info-box fixed bottom-5 right-5 z-9999 w-[500px] bg-amber-300 border-4 border-dashed rounded-md shadow-lg p-6 py-8 items-center justify-center flex flex-col space-y-2 origin-bottom-right transition-all duration-300" [style.transform]="getCompensationTransform()">
<button
(click)="dismissWarning()"
class="absolute top-2 right-2 w-10 h-10 flex items-center justify-center rounded-full hover:bg-amber-400 transition-colors"
aria-label="Warnung schließen">
<span class="text-3xl leading-none">×</span>
</button>
<div class="relative mb-5">
<mat-icon class="material-symbols-outlined" style="font-size: 100px; width: 100px; height: 100px">
screenshot_monitor
</mat-icon>
<mat-icon class="material-symbols-outlined absolute top-[calc(50%-9px)] left-1/2 -translate-x-1/2 -translate-y-1/2" style="font-size: 30px; width: 30px; height: 30px">
warning
</mat-icon>
</div>
<h1 class="text-xl font-bold">Browser-Zoom nicht optimal!</h1>
<p class="text-center">Ihr Browser-Zoom ist auf <strong>{{ currentZoomPercentage }}%</strong> eingestellt.<br>Für die beste Darstellung empfehlen wir <strong>100%</strong>.</p>
<div class="mt-4 text-sm space-y-3 w-full">
<div class="bg-white/50 rounded p-3">
<p class="font-semibold mb-2">Browser-Zoom zurücksetzen:</p>
<div class="space-y-1 ml-1">
<p class="flex items-center gap-1.5">
<kbd class="px-2 py-1 bg-white rounded shadow border border-gray-500 font-mono text-xs">Strg</kbd>
<span>/</span>
<kbd class="px-2 py-1 bg-white rounded shadow border border-gray-500 font-mono text-xs"></kbd>
<span>+</span>
<kbd class="px-2 py-1 bg-white rounded shadow border border-gray-500 font-mono text-xs">0</kbd>
<span class="px-1">(Windows, Linux / Mac)</span>
</p>
</div>
</div>
<div class="bg-white/50 rounded p-3">
<p class="font-semibold mb-2">Windows-Skalierung prüfen:</p>
<ol class="list-decimal ml-6 space-y-1">
<li>Öffnen Sie die <strong>Windows-Einstellungen</strong></li>
<li>Navigieren Sie zu <strong>System</strong><strong>Bildschirm</strong></li>
<li>Setzen Sie unter <strong>"Skalierung"</strong> den Wert auf <strong>100%</strong></li>
</ol>
</div>
</div>
</div>
}
@if (showMobileWarning) {
<div class="header z-99999 px-8 pt-4 pb-3 relative bg-white text-center">
<div class="flex items-center justify-center space-x-4 transition m-auto">
<img src="assets/logo.png" class="h-10 w-10 transform scale-175 translate-y-px" />
<h1 class="text-3xl font-semibold tracking-wide">InfiniMotion</h1>
</div>
</div>
<div class="fixed inset-0 z-99998 bg-amber-300 flex items-center justify-center p-6">
<div class="max-w-md w-full text-center space-y-6 mt-10">
<div class="relative inline-block">
<mat-icon class="material-symbols-outlined" style="font-size: 120px; width: 120px; height: 120px">
screenshot_monitor
</mat-icon>
<mat-icon class="material-symbols-outlined absolute top-[calc(50%-11px)] left-1/2 -translate-x-1/2 -translate-y-1/2" style="font-size: 35px; width: 35px; height: 35px">
warning
</mat-icon>
</div>
<h1 class="text-2xl font-bold text-gray-800">Nur am PC verfügbar</h1>
<p class="text-base text-gray-700">
Diese Anwendung ist für die Nutzung am <strong>Desktop-PC</strong> optimiert und kann auf mobilen Geräten nicht verwendet werden.
</p>
<div class="text-xs text-gray-600 mt-12">
<p>Deine derzeitige URL:</p>
<p class="font-mono bg-white/70 px-3 py-2 rounded mt-1 break-all">
{{ currentUrl }}
</p>
</div>
</div>
</div>
}

View File

@@ -0,0 +1,100 @@
import { DeviceDetectionService } from './../device-detection.service';
import { Component, HostListener, inject, OnDestroy, OnInit } from '@angular/core';
import { filter, Subject, takeUntil } from 'rxjs';
import { ZoomDetectionService } from '../zoom-detection.service';
import { NavigationEnd, Router } from '@angular/router';
@Component({
selector: 'app-zoom-warning',
standalone: false,
templateUrl: './zoom-warning.component.html',
styleUrl: './zoom-warning.component.css',
})
export class ZoomWarningComponent implements OnInit, OnDestroy {
currentZoomPercentage = 100;
isOutOfRange = false;
isDismissed = false;
isMobile = false;
showMobileWarning = false;
currentUrl = '';
private destroy$ = new Subject<void>();
private currentZoomLevel = 1;
private lastZoomLevel = 0;
zoomDetectionService = inject(ZoomDetectionService);
deviceDetectionService = inject(DeviceDetectionService)
constructor(private router: Router) {
this.isMobile = this.deviceDetectionService.isMobile();
this.currentUrl = window.location.href;
this.checkIfShouldShowMobileWarning();
}
ngOnInit() {
this.router.events
.pipe(
filter(event => event instanceof NavigationEnd),
takeUntil(this.destroy$)
)
.subscribe(() => {
this.checkIfShouldShowMobileWarning();
});
if (!this.isMobile) {
this.updateZoomInfo();
this.zoomDetectionService.getZoomLevel$()
.pipe(takeUntil(this.destroy$))
.subscribe((newZoomLevel) => {
if (Math.abs(newZoomLevel - this.lastZoomLevel) > 0.01) {
this.lastZoomLevel = newZoomLevel;
this.isDismissed = false;
this.updateZoomInfo();
}
});
}
}
private checkIfShouldShowMobileWarning() {
if (!this.isMobile) {
this.showMobileWarning = false;
return;
}
const currentRoute = this.router.routerState.root;
const allowMobile = this.getRouteData(currentRoute, 'allowMobile');
this.showMobileWarning = !allowMobile;
}
private getRouteData(route: any, key: string): any {
while (route) {
if (route.snapshot?.data?.[key] !== undefined) {
return route.snapshot.data[key];
}
route = route.firstChild;
}
return false;
}
private updateZoomInfo() {
this.currentZoomLevel = this.zoomDetectionService.getZoomLevel();
this.currentZoomPercentage = Math.round(this.currentZoomLevel * 100);
this.isOutOfRange = this.zoomDetectionService.isZoomOutOfRange();
}
getCompensationTransform(): string {
const scale = 1 / this.currentZoomLevel;
return `scale(${scale})`;
}
dismissWarning() {
this.isDismissed = true;
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}