schedule search and optimization #6
@@ -1,9 +1,10 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule, provideBrowserGlobalErrorListeners } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { provideHttpClient, withFetch } from '@angular/common/http';
|
||||
|
||||
|
||||
import { AppRoutingModule } from './app-routing-module';
|
||||
import { App } from './app';
|
||||
|
||||
@@ -12,7 +13,11 @@ import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
|
||||
import { HeaderComponent } from './header/header.component';
|
||||
import { HomeComponent } from './home/home.component';
|
||||
@@ -34,8 +39,10 @@ import { Header2Component } from './header-2/header-2.component';
|
||||
import { TheaterOverlayComponent } from './theater-overlay/theater-overlay.component';
|
||||
import { SeatComponent } from './seat/seat.component';
|
||||
import { SeatRowComponent } from './seat-row/seat-row.component';
|
||||
import {MatIconButton} from '@angular/material/button';
|
||||
import { TheaterLayoutComponent } from './theater-layout/theater-layout.component';
|
||||
import { MovieSearchComponent } from './movie-search/movie-search.component';
|
||||
import { ScheduleHeaderComponent } from './schedule-header/schedule-header.component';
|
||||
import { MovieScheduleNoSearchResultComponent } from './movie-schedule-no-search-result/movie-schedule-no-search-result.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
@@ -60,11 +67,15 @@ import { TheaterLayoutComponent } from './theater-layout/theater-layout.componen
|
||||
TheaterOverlayComponent,
|
||||
SeatComponent,
|
||||
SeatRowComponent,
|
||||
TheaterLayoutComponent
|
||||
TheaterLayoutComponent,
|
||||
MovieSearchComponent,
|
||||
ScheduleHeaderComponent,
|
||||
MovieScheduleNoSearchResultComponent
|
||||
],
|
||||
imports: [
|
||||
AppRoutingModule,
|
||||
BrowserModule,
|
||||
ReactiveFormsModule,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatIconModule,
|
||||
@@ -72,7 +83,11 @@ import { TheaterLayoutComponent } from './theater-layout/theater-layout.componen
|
||||
MatToolbarModule,
|
||||
MatProgressBarModule,
|
||||
MatSnackBarModule,
|
||||
MatIconButton
|
||||
MatAutocompleteModule,
|
||||
MatInputModule,
|
||||
MatFormFieldModule,
|
||||
MatIconButton,
|
||||
MatDividerModule
|
||||
],
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</a>
|
||||
|
||||
<div class="absolute left-1/2 transform -translate-x-1/2 text-center">
|
||||
<h2 class="text-3xl font-bold animate-fadeUp-delay bg-gradient-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent">
|
||||
<h2 class="text-3xl font-bold bg-gradient-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent">
|
||||
Absolut war gestern, Bewegung ist heute!
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Kinosaal, Sitzplatz, Vorstellung} from '@infinimotion/model-frontend';
|
||||
import { Kinosaal, Sitzplatz, Vorstellung, Film } from '@infinimotion/model-frontend';
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
@@ -51,6 +51,11 @@ export class HttpService {
|
||||
return this.http.get<Vorstellung>(`${this.baseUrl}vorstellung/${id}`);
|
||||
}
|
||||
|
||||
/* POST /api/vorstellung/filter */
|
||||
getPerformaceByFilter(filter: string[]): Observable<Vorstellung[]> {
|
||||
return this.http.post<Vorstellung[]>(`${this.baseUrl}vorstellung/filter`, filter);
|
||||
}
|
||||
|
||||
/* POST /api/vorstellung */
|
||||
addPerformace(vorstellung: Omit<Vorstellung, 'id'>): Observable<Vorstellung> {
|
||||
return this.http.post<Vorstellung>(`${this.baseUrl}vorstellung`, vorstellung);
|
||||
@@ -67,6 +72,13 @@ export class HttpService {
|
||||
}
|
||||
|
||||
|
||||
/* Film APIs */
|
||||
|
||||
/* GET /api/film */
|
||||
getMovies(): Observable<Film[]> {
|
||||
return this.http.get<Film[]>(`${this.baseUrl}film`);
|
||||
}
|
||||
|
||||
|
||||
/* Show-Seats APIs*/
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="flex flex-col items-center justify-center py-12 text-gray-500 h-100 m-auto">
|
||||
<mat-icon class="text-6xl mb-4 opacity-50" style="font-size: 50px; width: 50px; height: 50px;">search_off</mat-icon>
|
||||
<p class="text-lg">Keine Vorstellungen gefunden</p>
|
||||
<p class="text-sm">Für '{{ search() }}' finden am {{ date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric'} )}} kein Vorstellunge statt.</p>
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Component, input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-movie-schedule-no-search-result',
|
||||
standalone: false,
|
||||
templateUrl: './movie-schedule-no-search-result.component.html',
|
||||
styleUrl: './movie-schedule-no-search-result.component.css'
|
||||
})
|
||||
export class MovieScheduleNoSearchResultComponent {
|
||||
search = input.required<string>();
|
||||
date = input.required<Date>();
|
||||
}
|
||||
0
src/app/movie-search/movie-search.component.css
Normal file
0
src/app/movie-search/movie-search.component.css
Normal file
16
src/app/movie-search/movie-search.component.html
Normal file
16
src/app/movie-search/movie-search.component.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<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> -->
|
||||
<!-- } -->
|
||||
|
||||
<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>
|
||||
62
src/app/movie-search/movie-search.component.ts
Normal file
62
src/app/movie-search/movie-search.component.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Component, inject, OnInit, output, ViewEncapsulation } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { catchError, map, startWith, tap } from 'rxjs/operators';
|
||||
import { HttpService } from '../http.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-movie-search',
|
||||
standalone: false,
|
||||
templateUrl: './movie-search.component.html',
|
||||
styleUrl: './movie-search.component.css',
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class MovieSearchComponent implements OnInit {
|
||||
|
||||
movieSearchResult = output<string>();
|
||||
|
||||
options: string[] = [];
|
||||
filteredOptions: Observable<string[]> = new Observable();
|
||||
|
||||
searchControl = new FormControl('', (control) => {
|
||||
if (!control.value) return null;
|
||||
|
||||
const value = control.value.toLowerCase();
|
||||
const found = this.options.some(option =>
|
||||
option.toLowerCase().includes(value)
|
||||
);
|
||||
return found ? null : { filmNotFound: true };
|
||||
});
|
||||
|
||||
private http = inject(HttpService)
|
||||
|
||||
ngOnInit() {
|
||||
this.loadMovies();
|
||||
this.filteredOptions = this.searchControl.valueChanges.pipe(
|
||||
startWith(''),
|
||||
tap(value => this.movieSearchResult.emit(value || '')),
|
||||
map(value => this._filter(value || ''))
|
||||
);
|
||||
}
|
||||
|
||||
private _filter(value: string): string[] {
|
||||
const filterValue = value.toLowerCase();
|
||||
return this.options.filter(option =>
|
||||
option.toLowerCase().includes(filterValue)
|
||||
);
|
||||
}
|
||||
|
||||
private loadMovies() {
|
||||
this.http.getMovies().pipe(
|
||||
tap(movies => {
|
||||
this.options = movies
|
||||
.map(movie => movie.title)
|
||||
.sort();
|
||||
}),
|
||||
catchError(err => {
|
||||
console.error('Fehler beim Laden der Filme', err);
|
||||
return of([]);
|
||||
})
|
||||
).subscribe();
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ nav {
|
||||
}
|
||||
|
||||
.navbar {
|
||||
width: 200px;
|
||||
width: 250px;
|
||||
background-color: white;
|
||||
padding: 5px;
|
||||
transition: color .2s;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<a [routerLink]="[item.path]"
|
||||
routerLinkActive="active"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
class="text-xl font-bold flex justify-center hover:bg-gray-200 gradient-text rounded-sm">
|
||||
class="text-2xl pl-3 hover:bg-gray-200 gradient-text rounded-sm">
|
||||
{{ item.label }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ export class NavbarComponent {
|
||||
*/
|
||||
|
||||
navItems:{label:string, path:string}[] = [
|
||||
{label: 'Schedule', path: '/schedule'},
|
||||
{label: 'Programm', path: '/schedule'},
|
||||
{label: 'Kinosaal-test', path: '/theater-overlay'},
|
||||
]
|
||||
}
|
||||
|
||||
6
src/app/schedule-header/schedule-header.component.css
Normal file
6
src/app/schedule-header/schedule-header.component.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.schedule-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
10
src/app/schedule-header/schedule-header.component.html
Normal file
10
src/app/schedule-header/schedule-header.component.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<div class="schedule-header px-4 py-4 duration-1000">
|
||||
<div class="flex items-center">
|
||||
<mat-icon style="font-size: 35px; width: 35px; height: 35px; opacity: 50%;">event</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">
|
||||
Programmübersicht
|
||||
</p>
|
||||
</div>
|
||||
<app-movie-search (movieSearchResult)="movieSearchResult.emit($event)"></app-movie-search>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
11
src/app/schedule-header/schedule-header.component.ts
Normal file
11
src/app/schedule-header/schedule-header.component.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Component, output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-schedule-header',
|
||||
standalone: false,
|
||||
templateUrl: './schedule-header.component.html',
|
||||
styleUrl: './schedule-header.component.css'
|
||||
})
|
||||
export class ScheduleHeaderComponent {
|
||||
movieSearchResult = output<string>();
|
||||
}
|
||||
@@ -1,10 +1,18 @@
|
||||
<app-schedule-header (movieSearchResult)="movieSearchResult = $event"></app-schedule-header>
|
||||
|
||||
<mat-tab-group mat-stretch-tabs>
|
||||
@for (dateInfo of dates; track dateInfo.date; let i = $index) {
|
||||
<mat-tab [label]="dateInfo.label">
|
||||
@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 {
|
||||
<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>
|
||||
}
|
||||
|
||||
@@ -16,20 +16,32 @@ export class ScheduleComponent implements OnInit {
|
||||
dates: { label: string; date: Date; performances: MovieGroup[] }[] = [];
|
||||
performaces: Vorstellung[] = [];
|
||||
|
||||
movieSearchResult: string = '';
|
||||
|
||||
private readonly bookableDays: number = 14;
|
||||
|
||||
private http = inject(HttpService);
|
||||
private loading = inject(LoadingService)
|
||||
|
||||
constructor() {
|
||||
this.generateDates();
|
||||
this.generateDates(this.bookableDays);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.loadPerformances();
|
||||
this.loadPerformances(this.bookableDays);
|
||||
}
|
||||
|
||||
generateDates() {
|
||||
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) {
|
||||
const today = new Date();
|
||||
for (let i = 0; i < 14; i++) {
|
||||
for (let i = 0; i < bookableDays; i++) {
|
||||
const date = new Date(today);
|
||||
date.setDate(today.getDate() + i);
|
||||
|
||||
@@ -46,9 +58,10 @@ export class ScheduleComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
loadPerformances() {
|
||||
loadPerformances(bookableDays: number) {
|
||||
this.loading.show();
|
||||
this.http.getPerformaces().pipe(
|
||||
const filter = this.generateDateFilter(bookableDays);
|
||||
this.http.getPerformaceByFilter(filter).pipe(
|
||||
map(data => Array.isArray(data) ? data : [data]),
|
||||
tap(performaces => {
|
||||
this.performaces = performaces;
|
||||
@@ -57,12 +70,27 @@ export class ScheduleComponent implements OnInit {
|
||||
}),
|
||||
catchError(err => {
|
||||
this.loading.showError(err);
|
||||
console.error('Fehler beim Laden der Vorstellung', err);
|
||||
console.error('Fehler beim Laden der Vorstellungen', err);
|
||||
return of([]);
|
||||
})
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
return [
|
||||
`ge;start;date;${startStr}`,
|
||||
`le;start;date;${endStr}`,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
assignPerformancesToDates() {
|
||||
|
||||
// Gruppieren nach Datum
|
||||
|
||||
Reference in New Issue
Block a user