Merge branch 'main' of git.infinimotion.de:infinimotion/frontend

This commit is contained in:
Marcel-Anker
2025-11-18 14:01:07 +01:00
13 changed files with 157 additions and 131 deletions

View File

@@ -23,6 +23,8 @@ import { MatDividerModule } from '@angular/material/divider';
import { MatDialogClose, MatDialogTitle, MatDialogContent, MatDialogActions } from "@angular/material/dialog"; import { MatDialogClose, MatDialogTitle, MatDialogContent, MatDialogActions } from "@angular/material/dialog";
import { MatStepperModule } from '@angular/material/stepper'; import { MatStepperModule } from '@angular/material/stepper';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatBadgeModule } from '@angular/material/badge';
import { MatTooltipModule } from '@angular/material/tooltip';
import { HeaderComponent } from './header/header.component'; import { HeaderComponent } from './header/header.component';
import { HomeComponent } from './home/home.component'; import { HomeComponent } from './home/home.component';
@@ -135,6 +137,8 @@ import { StatisticsComponent } from './statistics/statistics.component';
NgxMaskDirective, NgxMaskDirective,
NgxMaskPipe, NgxMaskPipe,
QRCodeComponent, QRCodeComponent,
MatBadgeModule,
MatTooltipModule,
], ],
providers: [ providers: [
provideBrowserGlobalErrorListeners(), provideBrowserGlobalErrorListeners(),

View File

@@ -53,6 +53,12 @@ export class HttpService {
} }
/* POST /api/order-transaction/create */
saveAddOrder(req: {order:Bestellung, tickets:Eintrittskarte[]}): Observable<{order:Bestellung, tickets:Eintrittskarte[]}> {
return this.http.post<{order: Bestellung, tickets: Eintrittskarte[]}>(`${this.baseUrl}order-transaction/create`, req);
}
/* Eintrittskarte APIs */ /* Eintrittskarte APIs */
/* GET /api/eintrittskarte/{id} */ /* GET /api/eintrittskarte/{id} */

View File

@@ -1,11 +1,11 @@
<div class="flex flex-row items-center justify-center gap-8 h-1/1"> <div class="flex items-center h-1/1 justify-center space-x-30">
<div class="max-w-xl m-auto"> <div class="max-w-xl">
<section class="felx flex-row"> <section class="felx flex-row">
<div class="flex items-center"> <div class="flex items-center">
<h1 class="text-3xl font-bold"> <h1 class="text-3xl font-bold">
Willkommen bei Willkommen bei
</h1> </h1>
<div class="bg-gradient-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent text-3xl font-bold"> <div class="bg-linear-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent text-3xl font-bold">
&nbsp;InfiniMotion &nbsp;InfiniMotion
</div> </div>
<h1 class="text-3xl font-bold">! 🎬</h1> <h1 class="text-3xl font-bold">! 🎬</h1>
@@ -41,7 +41,7 @@
Wir haben uns bei Gestaltung und Stil bewusst an bestehenden Kinowebsites orientiert. Wir haben uns bei Gestaltung und Stil bewusst an bestehenden Kinowebsites orientiert.
Dabei handelt es sich um eine rein stilistische Anlehnung; diese Seite verfolgt keinerlei kommerzielle Zwecke und dient ausschließlich universitären Zwecken. Dabei handelt es sich um eine rein stilistische Anlehnung; diese Seite verfolgt keinerlei kommerzielle Zwecke und dient ausschließlich universitären Zwecken.
Marken, Designs oder Funktionalitäten, die bekannten Anbietern ähneln, sind nicht als Kopie zum Wettbewerb gedacht, sondern als pragmatische Inspirationsquelle im Rahmen der Praxisarbeit. Marken, Designs oder Funktionalitäten, die bekannten Anbietern ähneln, sind nicht als Kopie zum Wettbewerb gedacht, sondern als pragmatische Inspirationsquelle im Rahmen der Praxisarbeit.
<a href="https://infinimotion.de" target="_blank" class="bg-gradient-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent"> <a href="https://infinimotion.de" target="_blank" class="bg-linear-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent">
https://infinimotion.de https://infinimotion.de
</a> </a>
wird zum Projektende offline genommen. wird zum Projektende offline genommen.
@@ -52,7 +52,7 @@
</section> </section>
</div> </div>
<div class="max-w-md mr-60 mt-10"> <div class="mt-10">
<mat-vertical-stepper [linear]="false" [selectedIndex]="5" class="always-open-stepper"> <mat-vertical-stepper [linear]="false" [selectedIndex]="5" class="always-open-stepper">
<mat-step <mat-step
@@ -81,7 +81,7 @@
<mat-step <mat-step
[completed]="isCompleted(3)" [completed]="isCompleted(3)"
[editable]="isEditable(3)"> [editable]="true">
<ng-template matStepLabel> <ng-template matStepLabel>
<span>Sprint #3:&nbsp; &nbsp; Vorstellungstickets reservieren und buchen</span> <span>Sprint #3:&nbsp; &nbsp; Vorstellungstickets reservieren und buchen</span>
</ng-template> </ng-template>
@@ -104,5 +104,5 @@
</mat-step> </mat-step>
</mat-vertical-stepper> </mat-vertical-stepper>
</div>
</div> </div>

View File

@@ -7,7 +7,7 @@ import { Component } from '@angular/core';
styleUrl: './main.component.css' styleUrl: './main.component.css'
}) })
export class MainComponent { export class MainComponent {
currentSprint = 3; currentSprint = 4;
isCompleted(index: number): boolean { isCompleted(index: number): boolean {
return index <= this.currentSprint; return index <= this.currentSprint;

View File

@@ -3,8 +3,8 @@
@if ( icon() ) { @if ( icon() ) {
<mat-icon style="font-size: 35px; width: 35px; height: 35px; opacity: 50%;">{{ icon() }}</mat-icon> <mat-icon style="font-size: 35px; width: 35px; height: 35px; opacity: 50%;">{{ icon() }}</mat-icon>
} }
<p class="text-2xl font-medium pl-2 bg-gradient-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent"> <p class="text-2xl font-medium pl-2 bg-linear-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent">
{{ title() }} {{ label() }}
</p> </p>
</div> </div>
@if ( searchBar() ) { @if ( searchBar() ) {

View File

@@ -7,7 +7,7 @@ import { Component, input, output } from '@angular/core';
styleUrl: './menu-header.component.css' styleUrl: './menu-header.component.css'
}) })
export class MenuHeaderComponent { export class MenuHeaderComponent {
title = input.required<string>(); label = input.required<string>();
icon = input<string>(); icon = input<string>();
searchBar = input<boolean>(false); searchBar = input<boolean>(false);

View File

@@ -1,4 +1,4 @@
<app-menu-header title="Film aus IMDb importieren" icon="cloud_download"></app-menu-header> <app-menu-header label="Film aus IMDb importieren" icon="cloud_download"></app-menu-header>
<div class="w-6/10 m-auto my-20"> <div class="w-6/10 m-auto my-20">
<form class="movie-search-form w-full" (ngSubmit)="DoSubmit()"> <form class="movie-search-form w-full" (ngSubmit)="DoSubmit()">

View File

@@ -1,11 +1,15 @@
<div class="flex items-center space-x-4">
@if (searchControl.value && searchControl.value.length > 0) {
<button mat-icon-button #tooltip="matTooltip" matTooltip="Filter löschen" matTooltipPosition="above" class="w-11! h-11! opacity-50" (click)="searchControl.setValue('')">
<mat-icon style="font-size: 25px; width: 25px; height: 25px;">filter_alt_off</mat-icon>
</button>
}
<form class="movie-search-form w-88"> <form class="movie-search-form w-88">
<mat-form-field class="w-full" subscriptSizing="dynamic"> <mat-form-field class="w-full" subscriptSizing="dynamic">
<mat-label>Film suchen</mat-label> <mat-label>Film suchen</mat-label>
<input class="w-full" type="text" matInput [formControl]="searchControl" [matAutocomplete]="auto" (click)="searchControl.setValue('')"> <input class="w-full" type="text" matInput [formControl]="searchControl" [matAutocomplete]="auto">
<!-- @if (searchControl.hasError('filmNotFound')) { -->
<!-- <mat-error>Film existiert nicht</mat-error> -->
<!-- } -->
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete"> <mat-autocomplete autoActiveFirstOption #auto="matAutocomplete">
@for (option of filteredOptions | async; track option) { @for (option of filteredOptions | async; track option) {
@@ -14,3 +18,5 @@
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>
</form> </form>
</div>

View File

@@ -161,37 +161,31 @@ export class OrderComponent {
submitOrder(order: Bestellung, seats: Sitzplatz[], performance: Vorstellung, mode: SubmissionMode) { submitOrder(order: Bestellung, seats: Sitzplatz[], performance: Vorstellung, mode: SubmissionMode) {
this.httpService.addOrder(order).pipe(
// Order erstellen
switchMap(createdOrder => {
// Tickets parallel erstellen // Tickets anlegen
const ticketObservables = seats.map(seat => { const tickets = seats.map(seat => {
const ticket = this.generateNewTicketObject(performance, seat, createdOrder); return this.generateNewTicketObject(performance, seat, order);;
return this.httpService.addTicket(ticket);
}); });
// Warten bis alles fertig sind // Transaktionssicher Sitzplatzbuchung
return forkJoin(ticketObservables).pipe( this.httpService.saveAddOrder({order, tickets}).pipe(
tap(createdTickets => { tap(createdOrderAndTickets => {
// Success Handling // Success Handling
if (mode === 'reservation') { if (mode === 'reservation') {
this.orderState.set({ this.orderState.set({
status: 'reservation-success', status: 'reservation-success',
order: createdOrder order: createdOrderAndTickets.order
}); });
} else { } else {
this.orderState.set({ this.orderState.set({
status: 'purchase-success', status: 'purchase-success',
tickets: createdTickets tickets: createdOrderAndTickets.tickets
}); });
} }
this.selectedSeatsService.commit(); this.selectedSeatsService.commit();
this.loadingService.hide(); this.loadingService.hide();
this.showConfetti(); this.showConfetti();
})
);
}), }),
catchError(err => { catchError(err => {
// Error handling // Error handling

View File

@@ -5,3 +5,7 @@
::ng-deep .mat-mdc-tab .mdc-tab-indicator__content--underline { ::ng-deep .mat-mdc-tab .mdc-tab-indicator__content--underline {
border-color: #6366f1 !important; /* indigo-500 */ border-color: #6366f1 !important; /* indigo-500 */
} }
.mat-badge-content {
background: #dd2979;
}

View File

@@ -1,21 +1,26 @@
<app-menu-header title="Programmübersicht" icon="event" [searchBar]="true" (movieSearchResult)="movieSearchResult = $event"></app-menu-header> <app-menu-header label="Programmübersicht" icon="event" [searchBar]="true" (movieSearchResult)="movieSearchResult = $event"></app-menu-header>
<mat-tab-group mat-stretch-tabs> <mat-tab-group mat-stretch-tabs>
@for (dateInfo of dates; track dateInfo.date; let i = $index) { @for (dateInfo of dates; track dateInfo.date; let i = $index) {
<mat-tab [label]="dateInfo.label"> <mat-tab [label]="dateInfo.label">
<ng-template mat-tab-label>
<span [matBadge]="getMovieCount(i)" matBadgeOverlap="false" [matBadgeHidden]="!isSearch() || getMovieCount(i) === 0" [class]="(isSearch() && getMovieCount(i) === 0)? 'text-gray-300' : ''">
{{ dateInfo.label }}
</span>
</ng-template>
@if (getMovieCount(i) > 0) { @if (getMovieCount(i) > 0) {
@if (hasSearchResults(i)) {
@for (group of dateInfo.performances; track group.movie.id) { @for (group of dateInfo.performances; track group.movie.id) {
@if (group.movie.title.toLowerCase().includes(movieSearchResult.toLowerCase())) { @if (group.movie.title.toLowerCase().includes(movieSearchResult.toLowerCase())) {
<app-movie-schedule-info [movieGroup]="group"></app-movie-schedule-info> <app-movie-schedule-info [movieGroup]="group"></app-movie-schedule-info>
} }
} }
} @else { } @else {
@if (isSearch()) {
<app-movie-schedule-no-search-result [search]="movieSearchResult" [date]="dates[i].date"></app-movie-schedule-no-search-result> <app-movie-schedule-no-search-result [search]="movieSearchResult" [date]="dates[i].date"></app-movie-schedule-no-search-result>
}
} @else { } @else {
<app-movie-schedule-empty></app-movie-schedule-empty> <app-movie-schedule-empty></app-movie-schedule-empty>
} }
}
</mat-tab> </mat-tab>
} }
</mat-tab-group> </mat-tab-group>

View File

@@ -31,15 +31,7 @@ export class ScheduleComponent implements OnInit {
this.loadPerformances(this.bookableDays); this.loadPerformances(this.bookableDays);
} }
hasSearchResults(dateIndex: number): boolean { private generateDates(bookableDays: number) {
if (!this.movieSearchResult) return true;
return this.dates[dateIndex].performances.some(group =>
group.movie.title.toLowerCase().includes(this.movieSearchResult.toLowerCase())
);
}
generateDates(bookableDays: number) {
const today = new Date(); const today = new Date();
for (let i = 0; i < bookableDays; i++) { for (let i = 0; i < bookableDays; i++) {
const date = new Date(today); const date = new Date(today);
@@ -58,7 +50,7 @@ export class ScheduleComponent implements OnInit {
} }
} }
loadPerformances(bookableDays: number) { private loadPerformances(bookableDays: number) {
this.loading.show(); this.loading.show();
const filter = this.generateDateFilter(bookableDays); const filter = this.generateDateFilter(bookableDays);
this.http.getPerformacesByFilter(filter).pipe( this.http.getPerformacesByFilter(filter).pipe(
@@ -89,8 +81,7 @@ private generateDateFilter(bookableDays: number): string[] {
]; ];
} }
private assignPerformancesToDates() {
assignPerformancesToDates() {
// Gruppieren nach Datum // Gruppieren nach Datum
const groupedByDate: { [key: string]: Vorstellung[] } = {}; const groupedByDate: { [key: string]: Vorstellung[] } = {};
@@ -133,6 +124,22 @@ private generateDateFilter(bookableDays: number): string[] {
} }
getMovieCount(index: number): number { getMovieCount(index: number): number {
return this.dates[index].performances.length; if (!this.dates[index]?.performances) {
return 0;
}
const performances = this.dates[index].performances;
if (!this.isSearch()) {
return performances.length;
}
return performances.filter(group =>
group.movie.title.toLowerCase().includes(this.movieSearchResult.toLowerCase())
).length;
}
isSearch(): boolean {
return !!this.movieSearchResult && this.movieSearchResult.trim() !== '';
} }
} }

View File

@@ -1,4 +1,4 @@
<app-menu-header title="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 justify-between h-100">