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,7 +52,7 @@
</section>
</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-step
@@ -81,7 +81,7 @@
<mat-step
[completed]="isCompleted(3)"
[editable]="isEditable(3)">
[editable]="true">
<ng-template matStepLabel>
<span>Sprint #3:&nbsp; &nbsp; Vorstellungstickets reservieren und buchen</span>
</ng-template>
@@ -104,5 +104,5 @@
</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,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">
<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')) { -->
<!-- <mat-error>Film existiert nicht</mat-error> -->
<!-- } -->
<input class="w-full" type="text" matInput [formControl]="searchControl" [matAutocomplete]="auto">
<mat-autocomplete autoActiveFirstOption #auto="matAutocomplete">
@for (option of filteredOptions | async; track option) {
@@ -14,3 +18,5 @@
</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 => {
// Transaktionssicher Sitzplatzbuchung
this.httpService.saveAddOrder({order, tickets}).pipe(
tap(createdOrderAndTickets => {
// Success Handling
if (mode === 'reservation') {
this.orderState.set({
status: 'reservation-success',
order: createdOrder
order: createdOrderAndTickets.order
});
} else {
this.orderState.set({
status: 'purchase-success',
tickets: createdTickets
tickets: createdOrderAndTickets.tickets
});
}
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,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>
@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>
}
}
} @else {
@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-group>

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(
@@ -89,8 +81,7 @@ private generateDateFilter(bookableDays: number): string[] {
];
}
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">