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 { MatStepperModule } from '@angular/material/stepper';
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 { HomeComponent } from './home/home.component';
@@ -135,6 +137,8 @@ import { StatisticsComponent } from './statistics/statistics.component';
NgxMaskDirective,
NgxMaskPipe,
QRCodeComponent,
MatBadgeModule,
MatTooltipModule,
],
providers: [
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 */
/* 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="max-w-xl m-auto">
<div class="flex items-center h-1/1 justify-center space-x-30">
<div class="max-w-xl">
<section class="felx flex-row">
<div class="flex items-center">
<h1 class="text-3xl font-bold">
Willkommen bei
</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
</div>
<h1 class="text-3xl font-bold">! 🎬</h1>
@@ -41,7 +41,7 @@
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.
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
</a>
wird zum Projektende offline genommen.
@@ -52,57 +52,57 @@
</section>
</div>
<div class="max-w-md mr-60 mt-10">
<mat-vertical-stepper [linear]="false" [selectedIndex]="5" class="always-open-stepper">
<div class="mt-10">
<mat-vertical-stepper [linear]="false" [selectedIndex]="5" class="always-open-stepper">
<mat-step
[completed]="isCompleted(0)"
[editable]="isEditable(0)">
<ng-template matStepLabel>
<span>Sprint #0:&nbsp; &nbsp; Planung, Installation und Vorbereitung</span>
</ng-template>
</mat-step>
<mat-step
[completed]="isCompleted(0)"
[editable]="isEditable(0)">
<ng-template matStepLabel>
<span>Sprint #0:&nbsp; &nbsp; Planung, Installation und Vorbereitung</span>
</ng-template>
</mat-step>
<mat-step
[completed]="isCompleted(1)"
[editable]="isEditable(1)">
<ng-template matStepLabel>
<span>Sprint #1:&nbsp; &nbsp; Programmübersicht</span>
</ng-template>
</mat-step>
<mat-step
[completed]="isCompleted(1)"
[editable]="isEditable(1)">
<ng-template matStepLabel>
<span>Sprint #1:&nbsp; &nbsp; Programmübersicht</span>
</ng-template>
</mat-step>
<mat-step
[completed]="isCompleted(2)"
[editable]="isEditable(2)">
<ng-template matStepLabel>
<span>Sprint #2:&nbsp; &nbsp; Kinosäle anzeigen</span>
</ng-template>
</mat-step>
<mat-step
[completed]="isCompleted(2)"
[editable]="isEditable(2)">
<ng-template matStepLabel>
<span>Sprint #2:&nbsp; &nbsp; Kinosäle anzeigen</span>
</ng-template>
</mat-step>
<mat-step
[completed]="isCompleted(3)"
[editable]="isEditable(3)">
<ng-template matStepLabel>
<span>Sprint #3:&nbsp; &nbsp; Vorstellungstickets reservieren und buchen</span>
</ng-template>
</mat-step>
<mat-step
[completed]="isCompleted(3)"
[editable]="true">
<ng-template matStepLabel>
<span>Sprint #3:&nbsp; &nbsp; Vorstellungstickets reservieren und buchen</span>
</ng-template>
</mat-step>
<mat-step
[completed]="isCompleted(4)"
[editable]="isEditable(4)">
<ng-template matStepLabel>
<span>Sprint #4:&nbsp; &nbsp; Statistiken auswerten und anzeigen</span>
</ng-template>
</mat-step>
<mat-step
[completed]="isCompleted(4)"
[editable]="isEditable(4)">
<ng-template matStepLabel>
<span>Sprint #4:&nbsp; &nbsp; Statistiken auswerten und anzeigen</span>
</ng-template>
</mat-step>
<mat-step
[completed]="isCompleted(5)"
[editable]="isEditable(5)">
<ng-template matStepLabel>
<span>Sprint #5:&nbsp; &nbsp; Aufbereitung und Optimierung</span>
</ng-template>
</mat-step>
</mat-vertical-stepper>
<mat-step
[completed]="isCompleted(5)"
[editable]="isEditable(5)">
<ng-template matStepLabel>
<span>Sprint #5:&nbsp; &nbsp; Aufbereitung und Optimierung</span>
</ng-template>
</mat-step>
</mat-vertical-stepper>
</div>
</div>

View File

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

View File

@@ -3,8 +3,8 @@
@if ( 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">
{{ title() }}
<p class="text-2xl font-medium pl-2 bg-linear-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent">
{{ label() }}
</p>
</div>
@if ( searchBar() ) {

View File

@@ -7,7 +7,7 @@ import { Component, input, output } from '@angular/core';
styleUrl: './menu-header.component.css'
})
export class MenuHeaderComponent {
title = input.required<string>();
label = input.required<string>();
icon = input<string>();
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">
<form class="movie-search-form w-full" (ngSubmit)="DoSubmit()">

View File

@@ -1,16 +1,22 @@
<form class="movie-search-form w-88">
<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('')">
<div class="flex items-center space-x-4">
<!-- @if (searchControl.hasError('filmNotFound')) { -->
<!-- <mat-error>Film existiert nicht</mat-error> -->
<!-- } -->
@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>
}
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete">
@for (option of filteredOptions | async; track option) {
<mat-option [value]="option">{{option}}</mat-option>
}
</mat-autocomplete>
</mat-form-field>
</form>
<form class="movie-search-form w-88">
<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">
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete">
@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) {
this.httpService.addOrder(order).pipe(
// Order erstellen
switchMap(createdOrder => {
// Tickets parallel erstellen
const ticketObservables = seats.map(seat => {
const ticket = this.generateNewTicketObject(performance, seat, createdOrder);
return this.httpService.addTicket(ticket);
});
// Tickets anlegen
const tickets = seats.map(seat => {
return this.generateNewTicketObject(performance, seat, order);;
});
// Warten bis alles fertig sind
return forkJoin(ticketObservables).pipe(
tap(createdTickets => {
// Success Handling
if (mode === 'reservation') {
this.orderState.set({
status: 'reservation-success',
order: createdOrder
});
} else {
this.orderState.set({
status: 'purchase-success',
tickets: createdTickets
});
}
// Transaktionssicher Sitzplatzbuchung
this.httpService.saveAddOrder({order, tickets}).pipe(
tap(createdOrderAndTickets => {
// Success Handling
if (mode === 'reservation') {
this.orderState.set({
status: 'reservation-success',
order: createdOrderAndTickets.order
});
} else {
this.orderState.set({
status: 'purchase-success',
tickets: createdOrderAndTickets.tickets
});
}
this.selectedSeatsService.commit();
this.loadingService.hide();
this.showConfetti();
})
);
this.selectedSeatsService.commit();
this.loadingService.hide();
this.showConfetti();
}),
catchError(err => {
// Error handling

View File

@@ -5,3 +5,7 @@
::ng-deep .mat-mdc-tab .mdc-tab-indicator__content--underline {
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>
@for (dateInfo of dates; track dateInfo.date; let i = $index) {
<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 (hasSearchResults(i)) {
@for (group of dateInfo.performances; track group.movie.id) {
@if (group.movie.title.toLowerCase().includes(movieSearchResult.toLowerCase())) {
<app-movie-schedule-info [movieGroup]="group"></app-movie-schedule-info>
}
@for (group of dateInfo.performances; track group.movie.id) {
@if (group.movie.title.toLowerCase().includes(movieSearchResult.toLowerCase())) {
<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 {
<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>
}

View File

@@ -31,15 +31,7 @@ export class ScheduleComponent implements OnInit {
this.loadPerformances(this.bookableDays);
}
hasSearchResults(dateIndex: number): boolean {
if (!this.movieSearchResult) return true;
return this.dates[dateIndex].performances.some(group =>
group.movie.title.toLowerCase().includes(this.movieSearchResult.toLowerCase())
);
}
generateDates(bookableDays: number) {
private generateDates(bookableDays: number) {
const today = new Date();
for (let i = 0; i < bookableDays; i++) {
const date = new Date(today);
@@ -58,7 +50,7 @@ export class ScheduleComponent implements OnInit {
}
}
loadPerformances(bookableDays: number) {
private loadPerformances(bookableDays: number) {
this.loading.show();
const filter = this.generateDateFilter(bookableDays);
this.http.getPerformacesByFilter(filter).pipe(
@@ -75,22 +67,21 @@ export class ScheduleComponent implements OnInit {
).subscribe();
}
private generateDateFilter(bookableDays: number): string[] {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + bookableDays - 1);
private generateDateFilter(bookableDays: number): string[] {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(startDate.getDate() + bookableDays - 1);
const startStr = startDate.toISOString().split('T')[0] + 'T00:00:00';
const endStr = endDate.toISOString().split('T')[0] + 'T23:59:59';
const startStr = startDate.toISOString().split('T')[0] + 'T00:00:00';
const endStr = endDate.toISOString().split('T')[0] + 'T23:59:59';
return [
`ge;start;date;${startStr}`,
`le;start;date;${endStr}`,
];
}
return [
`ge;start;date;${startStr}`,
`le;start;date;${endStr}`,
];
}
assignPerformancesToDates() {
private assignPerformancesToDates() {
// Gruppieren nach Datum
const groupedByDate: { [key: string]: Vorstellung[] } = {};
@@ -133,6 +124,22 @@ private generateDateFilter(bookableDays: number): string[] {
}
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">