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,57 +52,57 @@
</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
[completed]="isCompleted(0)" [completed]="isCompleted(0)"
[editable]="isEditable(0)"> [editable]="isEditable(0)">
<ng-template matStepLabel> <ng-template matStepLabel>
<span>Sprint #0:&nbsp; &nbsp; Planung, Installation und Vorbereitung</span> <span>Sprint #0:&nbsp; &nbsp; Planung, Installation und Vorbereitung</span>
</ng-template> </ng-template>
</mat-step> </mat-step>
<mat-step <mat-step
[completed]="isCompleted(1)" [completed]="isCompleted(1)"
[editable]="isEditable(1)"> [editable]="isEditable(1)">
<ng-template matStepLabel> <ng-template matStepLabel>
<span>Sprint #1:&nbsp; &nbsp; Programmübersicht</span> <span>Sprint #1:&nbsp; &nbsp; Programmübersicht</span>
</ng-template> </ng-template>
</mat-step> </mat-step>
<mat-step <mat-step
[completed]="isCompleted(2)" [completed]="isCompleted(2)"
[editable]="isEditable(2)"> [editable]="isEditable(2)">
<ng-template matStepLabel> <ng-template matStepLabel>
<span>Sprint #2:&nbsp; &nbsp; Kinosäle anzeigen</span> <span>Sprint #2:&nbsp; &nbsp; Kinosäle anzeigen</span>
</ng-template> </ng-template>
</mat-step> </mat-step>
<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>
</mat-step> </mat-step>
<mat-step <mat-step
[completed]="isCompleted(4)" [completed]="isCompleted(4)"
[editable]="isEditable(4)"> [editable]="isEditable(4)">
<ng-template matStepLabel> <ng-template matStepLabel>
<span>Sprint #4:&nbsp; &nbsp; Statistiken auswerten und anzeigen</span> <span>Sprint #4:&nbsp; &nbsp; Statistiken auswerten und anzeigen</span>
</ng-template> </ng-template>
</mat-step> </mat-step>
<mat-step <mat-step
[completed]="isCompleted(5)" [completed]="isCompleted(5)"
[editable]="isEditable(5)"> [editable]="isEditable(5)">
<ng-template matStepLabel> <ng-template matStepLabel>
<span>Sprint #5:&nbsp; &nbsp; Aufbereitung und Optimierung</span> <span>Sprint #5:&nbsp; &nbsp; Aufbereitung und Optimierung</span>
</ng-template> </ng-template>
</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,16 +1,22 @@
<form class="movie-search-form w-88"> <div class="flex items-center space-x-4">
<mat-form-field class="w-full" subscriptSizing="dynamic">
<mat-label>Film suchen</mat-label>
<input class="w-full" type="text" matInput [formControl]="searchControl" [matAutocomplete]="auto" (click)="searchControl.setValue('')">
<!-- @if (searchControl.hasError('filmNotFound')) { --> @if (searchControl.value && searchControl.value.length > 0) {
<!-- <mat-error>Film existiert nicht</mat-error> --> <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>
}
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete"> <form class="movie-search-form w-88">
@for (option of filteredOptions | async; track option) { <mat-form-field class="w-full" subscriptSizing="dynamic">
<mat-option [value]="option">{{option}}</mat-option> <mat-label>Film suchen</mat-label>
} <input class="w-full" type="text" matInput [formControl]="searchControl" [matAutocomplete]="auto">
</mat-autocomplete>
</mat-form-field> <mat-autocomplete autoActiveFirstOption #auto="matAutocomplete">
</form> @for (option of filteredOptions | async; track option) {
<mat-option [value]="option">{{option}}</mat-option>
}
</mat-autocomplete>
</mat-form-field>
</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,20 +1,25 @@
<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 {
<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> @if (isSearch()) {
<app-movie-schedule-no-search-result [search]="movieSearchResult" [date]="dates[i].date"></app-movie-schedule-no-search-result>
} @else {
<app-movie-schedule-empty></app-movie-schedule-empty>
}
} }
</mat-tab> </mat-tab>
} }

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(
@@ -75,22 +67,21 @@ export class ScheduleComponent implements OnInit {
).subscribe(); ).subscribe();
} }
private generateDateFilter(bookableDays: number): string[] { private generateDateFilter(bookableDays: number): string[] {
const startDate = new Date(); const startDate = new Date();
const endDate = new Date(); const endDate = new Date();
endDate.setDate(startDate.getDate() + bookableDays - 1); endDate.setDate(startDate.getDate() + bookableDays - 1);
const startStr = startDate.toISOString().split('T')[0] + 'T00:00:00'; const startStr = startDate.toISOString().split('T')[0] + 'T00:00:00';
const endStr = endDate.toISOString().split('T')[0] + 'T23:59:59'; const endStr = endDate.toISOString().split('T')[0] + 'T23:59:59';
return [ return [
`ge;start;date;${startStr}`, `ge;start;date;${startStr}`,
`le;start;date;${endStr}`, `le;start;date;${endStr}`,
]; ];
} }
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">