Make movie schedule components functional

Introduces MovieGroup and Performance models for better type safety and data handling. Refactors movie-related components to use Angular signals (input/computed) and updates templates to bind data dynamically. Updates HttpService to support Vorstellung API endpoints. The schedule component now loads and groups performances by date and movie, passing structured data to child components for rendering.
This commit is contained in:
2025-10-30 01:38:43 +01:00
parent 6ebde0b5f5
commit 98626d11ed
19 changed files with 213 additions and 69 deletions

View File

@@ -1,4 +1,4 @@
import { Kinosaal } from '@infinimotion/model-frontend'; import { Kinosaal, Vorstellung } from '@infinimotion/model-frontend';
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { inject, Injectable } from "@angular/core"; import { inject, Injectable } from "@angular/core";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
@@ -6,7 +6,10 @@ import { Observable } from "rxjs";
@Injectable({providedIn: 'root'}) @Injectable({providedIn: 'root'})
export class HttpService { export class HttpService {
private http = inject(HttpClient); private http = inject(HttpClient);
private baseUrl = 'https://infinimotion.de/api/'; private baseUrl = '/api/';
/* Kinosaal APIs */
/* GET /api/kinosaal */ /* GET /api/kinosaal */
getAllKinosaal(): Observable<Kinosaal[]> { getAllKinosaal(): Observable<Kinosaal[]> {
@@ -32,4 +35,32 @@ export class HttpService {
deleteKinosaal(id: number): Observable<void> { deleteKinosaal(id: number): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}kinosaal/${id}`); return this.http.delete<void>(`${this.baseUrl}kinosaal/${id}`);
} }
/* Vorstellung APIs */
/* GET /api/vorstellung */
getPerformaces(): Observable<Vorstellung[]> {
return this.http.get<Vorstellung[]>(`${this.baseUrl}vorstellung`);
}
/* GET /api/vorstellung/{id} */
getPerformaceById(id: number): Observable<Vorstellung> {
return this.http.get<Vorstellung>(`${this.baseUrl}vorstellung/${id}`);
}
/* POST /api/vorstellung */
addPerformace(vorstellung: Omit<Vorstellung, 'id'>): Observable<Vorstellung> {
return this.http.post<Vorstellung>(`${this.baseUrl}vorstellung`, vorstellung);
}
/* PUT /api/vorstellung/{id} */
updatePerformace(id: number, vorstellung: Partial<Vorstellung>): Observable<Vorstellung> {
return this.http.put<Vorstellung>(`${this.baseUrl}vorstellung/${id}`, vorstellung);
}
/* DELETE /api/vorstellung/{id} */
deletePerformace(id: number): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}vorstellung/${id}`);
}
} }

View File

@@ -0,0 +1,7 @@
import { Film } from '@infinimotion/model-frontend';
import { Performance } from './performance.model';
export interface MovieGroup {
movie: Film;
performances: Performance[];
}

View File

@@ -0,0 +1,6 @@
export class Performance {
id!: number;
hall!: string;
start!: Date;
utilisation?: number;
}

View File

@@ -1,3 +1,3 @@
<span class="flex rounded-sm text-black text-sm px-2 py-1.5"> <span class="flex rounded-sm text-black text-sm px-2 py-1.5">
{{ getCategoryText() }} {{ category() }}
</span> </span>

View File

@@ -1,4 +1,4 @@
import { Component, Input } from '@angular/core'; import { Component, input } from '@angular/core';
@Component({ @Component({
selector: 'app-movie-category', selector: 'app-movie-category',
@@ -7,9 +7,5 @@ import { Component, Input } from '@angular/core';
styleUrl: './movie-category.component.css' styleUrl: './movie-category.component.css'
}) })
export class MovieCategoryComponent { export class MovieCategoryComponent {
@Input() category: string = '-'; category = input<string>('-');
getCategoryText(): string {
return this.category;
}
} }

View File

@@ -1,4 +1,4 @@
<span class="flex items-center text-black text-sm rounded-sm px-2 py-1.5"> <span class="flex items-center text-black text-sm rounded-sm px-2 py-1.5">
<mat-icon class="mr-1" style="font-size: 20px; width: 20px; height: 20px;" fontIcon="schedule"></mat-icon> <mat-icon class="mr-1" style="font-size: 20px; width: 20px; height: 20px;" fontIcon="schedule"></mat-icon>
{{ getDurationText() }} {{ durationText() }}
</span> </span>

View File

@@ -1,4 +1,4 @@
import { Component, Input } from '@angular/core'; import { Component, input, computed } from '@angular/core';
@Component({ @Component({
selector: 'app-movie-duration', selector: 'app-movie-duration',
@@ -7,9 +7,7 @@ import { Component, Input } from '@angular/core';
styleUrl: './movie-duration.component.css' styleUrl: './movie-duration.component.css'
}) })
export class MovieDurationComponent { export class MovieDurationComponent {
@Input() duration: number = 0; duration = input<number>(0);
getDurationText(): string { durationText = computed(() => `${this.duration()} Min.`);
return `${this.duration} Min.`;
}
} }

View File

@@ -1,14 +1,14 @@
<a routerLink="/schedule" class="bg-gray-200 m-2 flex flex-col items-center justify-between rounded-md overflow-hidden text-xl shadow-lg transform transition-all duration-300 hover:scale-105"> <a routerLink="/schedule" class="bg-gray-200 m-2 flex flex-col items-center justify-between rounded-md overflow-hidden text-xl shadow-lg transform transition-all duration-300 hover:scale-105">
<div class="bg-gradient-to-r from-indigo-500 to-pink-600 w-full text-center text-white font-medium rounded-t-md py-0.5"> <div class="bg-gradient-to-r from-indigo-500 to-pink-600 w-full text-center text-white font-medium rounded-t-md py-0.5">
<p>Kino 1</p> <p>{{ hall() }}</p>
</div> </div>
<h1 class="flex-1 flex items-center justify-center text-black font-bold text-3xl px-2 py-1"> <h1 class="flex-1 flex items-center justify-center text-black font-bold text-3xl px-2 py-1">
15:30 {{ startTime() }}
</h1> </h1>
<div class="bg-green-600 w-full text-center text-white font-medium rounded-b-md py-0.5"> <div [style.background-color]="utilisationBackground()" [class]="fullStyle()" class="w-full text-center text-white font-medium rounded-b-md py-0.5 ">
<p>Tickets</p> <p>Tickets</p>
</div> </div>

View File

@@ -1,4 +1,4 @@
import { Component } from '@angular/core'; import { Component, input, computed } from '@angular/core';
@Component({ @Component({
selector: 'app-movie-performance', selector: 'app-movie-performance',
@@ -7,5 +7,40 @@ import { Component } from '@angular/core';
styleUrl: './movie-performance.component.css' styleUrl: './movie-performance.component.css'
}) })
export class MoviePerformanceComponent { export class MoviePerformanceComponent {
id = input.required<number>();
hall = input.required<string>();
start = input.required<Date>();
utilisation = input<number | undefined>();
startTime = computed(() =>
this.start().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })
);
utilisationBackground = computed(() => {
const u = this.utilisation() ?? -1;
const color_map = new Map<number, string>([
[100, '#da1414'],
[75, '#e76800'],
[50, '#efbe12'],
[25, '#8bbb00'],
[0, '#10a20b']
]);
for (const [threshold, color] of color_map) {
if (u >= threshold) {
return color;
}
}
return '#424242';
});
fullStyle = computed(() => {
if (this.utilisation() === 100) {
return 'line-through';
}
return '';
});
} }

View File

@@ -1,8 +1,8 @@
<div class="w-64 mx-auto my-2"> <div class="w-64 mx-auto my-2">
<img src="assets/test-movie-poster.jpg" alt="Movie Poster" class="w-full h-auto shadow-md"> <img [src]="movie().image" alt="Movie Poster" class="w-full h-auto shadow-md">
</div> </div>
<div class="flex gap-1 justify-between"> <div class="flex gap-1 justify-between">
<app-movie-rating [rating]="12"></app-movie-rating> <app-movie-rating [rating]="movie().rating"></app-movie-rating>
<app-movie-duration [duration]="181"></app-movie-duration> <app-movie-duration [duration]="movie().duration"></app-movie-duration>
<app-movie-category [category]="'Action'"></app-movie-category> <app-movie-category [category]="movie().category.name"></app-movie-category>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { Component } from '@angular/core'; import { Component, input } from '@angular/core';
import { Film } from '@infinimotion/model-frontend';
@Component({ @Component({
selector: 'app-movie-poster', selector: 'app-movie-poster',
@@ -7,5 +8,5 @@ import { Component } from '@angular/core';
styleUrl: './movie-poster.component.css' styleUrl: './movie-poster.component.css'
}) })
export class MoviePosterComponent { export class MoviePosterComponent {
movie = input.required<Film>();
} }

View File

@@ -1,3 +1,3 @@
<span [class]="getRatingColor()" class="text-black flex rounded-sm shadow-md text-sm px-2 py-1.5"> <span [class]="ratingColor()" class="text-black flex rounded-sm shadow-md text-sm px-2 py-1.5">
{{ getRatingText() }} {{ ratingText() }}
</span> </span>

View File

@@ -1,4 +1,4 @@
import { Component, Input } from '@angular/core'; import { Component, input, computed } from '@angular/core';
@Component({ @Component({
selector: 'app-movie-rating', selector: 'app-movie-rating',
@@ -7,23 +7,16 @@ import { Component, Input } from '@angular/core';
styleUrl: './movie-rating.component.css' styleUrl: './movie-rating.component.css'
}) })
export class MovieRatingComponent { export class MovieRatingComponent {
@Input() rating: number = 0; rating = input<number>(0);
getRatingColor(): string { ratingColor = computed(() => {
if (this.rating >= 18) { const r = this.rating();
return 'bg-red-500'; if (r >= 18) return 'bg-red-500';
} else if (this.rating >= 16) { if (r >= 16) return 'bg-blue-500';
return 'bg-blue-500'; if (r >= 12) return 'bg-green-500';
} else if (this.rating >= 12) { if (r >= 6) return 'bg-yellow-300';
return 'bg-green-500'; return 'bg-white';
} else if (this.rating >= 6) { });
return 'bg-yellow-300';
} else {
return 'bg-white';
}
}
getRatingText(): string { ratingText = computed(() => `FSK ${this.rating()}`);
return `FSK ${this.rating}`;
}
} }

View File

@@ -1,13 +1,13 @@
<div class="w-3/5 mx-auto flex flex-col md:flex-row gap-4"> <div class="w-2/3 mx-auto flex flex-col md:flex-row gap-4">
<app-movie-poster></app-movie-poster> <app-movie-poster [movie]="movie"></app-movie-poster>
<div> <div>
<div class="m-2 mb-4"> <div class="m-2 mb-4">
<h1 class="text-4xl font-bold mb-2">Avengers: Endgame</h1> <h1 class="text-4xl font-bold mb-2">{{ movie.title }}</h1>
<p class="text-xl"> <p class="text-base">
Long Movie description Long Movie description Long Movie description Long Movie description Long Movie description Long Movie descriptionLong Movie description Long Movie description Long Movie description {{ movie.description }}
</p> </p>
</div> </div>
<app-movie-schedule-times></app-movie-schedule-times> <app-movie-schedule-times [performances]="performances"></app-movie-schedule-times>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { Component } from '@angular/core'; import { Component, input } from '@angular/core';
import { MovieGroup } from '../model/movie-group.model';
@Component({ @Component({
selector: 'app-movie-schedule-info', selector: 'app-movie-schedule-info',
@@ -7,5 +8,13 @@ import { Component } from '@angular/core';
styleUrl: './movie-schedule-info.component.css' styleUrl: './movie-schedule-info.component.css'
}) })
export class MovieScheduleInfoComponent { export class MovieScheduleInfoComponent {
readonly movieGroup = input.required<MovieGroup>();
get movie() {
return this.movieGroup().movie;
}
get performances() {
return this.movieGroup().performances;
}
} }

View File

@@ -1,5 +1,10 @@
<div class="flex flex-wrap"> <div class="flex flex-wrap">
<app-movie-performance></app-movie-performance> @for (perf of performances(); track $index) {
<app-movie-performance></app-movie-performance> <app-movie-performance
<app-movie-performance></app-movie-performance> [id]="perf.id"
[hall]="perf.hall"
[start]="perf.start"
[utilisation]="perf.utilisation">
</app-movie-performance>
}
</div> </div>

View File

@@ -1,4 +1,5 @@
import { Component } from '@angular/core'; import { Performance } from './../model/performance.model';
import { Component, input } from '@angular/core';
@Component({ @Component({
selector: 'app-movie-schedule-times', selector: 'app-movie-schedule-times',
@@ -7,5 +8,5 @@ import { Component } from '@angular/core';
styleUrl: './movie-schedule-times.component.css' styleUrl: './movie-schedule-times.component.css'
}) })
export class MovieScheduleTimesComponent { export class MovieScheduleTimesComponent {
performances = input.required<Performance[]>();
} }

View File

@@ -1,12 +1,12 @@
<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">
@if (getMovieCount(i)> 0) { @if (getMovieCount(i) > 0) {
@for (movie of [].constructor(getMovieCount(i)); track movie) { @for (group of dateInfo.performances; track group.movie.id) {
<app-movie-schedule-info></app-movie-schedule-info> <app-movie-schedule-info [movieGroup]="group"></app-movie-schedule-info>
} }
} @else { } @else {
<app-movie-schedule-empty></app-movie-schedule-empty> <app-movie-schedule-empty></app-movie-schedule-empty>
} }
</mat-tab> </mat-tab>
} }

View File

@@ -1,4 +1,8 @@
import { Component } from '@angular/core'; import { Component, inject, OnInit } from '@angular/core';
import { HttpService } from '../http.service';
import { Vorstellung } from '@infinimotion/model-frontend';
import { Performance } from '../model/performance.model';
import { MovieGroup } from '../model/movie-group.model';
@Component({ @Component({
selector: 'app-schedule', selector: 'app-schedule',
@@ -6,16 +10,23 @@ import { Component } from '@angular/core';
templateUrl: './schedule.component.html', templateUrl: './schedule.component.html',
styleUrl: './schedule.component.css' styleUrl: './schedule.component.css'
}) })
export class ScheduleComponent { export class ScheduleComponent implements OnInit {
dates: { label: string; date: Date }[] = []; dates: { label: string; date: Date; performances: MovieGroup[] }[] = [];
performaces: Vorstellung[] = [];
private http = inject(HttpService);
constructor() { constructor() {
this.generateDates(); this.generateDates();
} }
ngOnInit() {
this.loadPerformances();
}
generateDates() { generateDates() {
const today = new Date(); const today = new Date();
for (let i = 0; i < 31; i++) { for (let i = 0; i < 14; i++) {
const date = new Date(today); const date = new Date(today);
date.setDate(today.getDate() + i); date.setDate(today.getDate() + i);
@@ -28,12 +39,63 @@ dates: { label: string; date: Date }[] = [];
label = date.toLocaleDateString('de-DE', { weekday: 'short' }) + '. ' + date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit'}); label = date.toLocaleDateString('de-DE', { weekday: 'short' }) + '. ' + date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit'});
} }
this.dates.push({ label, date }); this.dates.push({ label, date, performances: []});
}
}
loadPerformances() {
this.http.getPerformaces().subscribe({
next: (data) => {
this.performaces = Array.isArray(data) ? data : [data];
this.assignPerformancesToDates();
},
error: (err) => console.error('Fehler beim Laden der Performances', err),
});
}
assignPerformancesToDates() {
// Gruppieren nach Datum
const groupedByDate: { [key: string]: Vorstellung[] } = {};
for (const vorstellung of this.performaces) {
const dateKey = new Date(vorstellung.start).toDateString();
if (!groupedByDate[dateKey]) {
groupedByDate[dateKey] = [];
}
groupedByDate[dateKey].push(vorstellung);
}
// Gruppieren nach Film
for (const dateInfo of this.dates) {
const dateKey = dateInfo.date.toDateString();
const dailyPerformances: Vorstellung[] = groupedByDate[dateKey] || [];
const movieMap = new Map<number, { movie: typeof dailyPerformances[0]['movie']; performances: Performance[] }>();
for (const perf of dailyPerformances) {
const movieId = perf.movie.id;
if (!movieMap.has(movieId)) {
movieMap.set(movieId, { movie: perf.movie, performances: [] });
}
const performance: Performance = {
id: perf.id,
hall: perf.hall.name,
start: new Date(perf.start),
// utilisation: 0 // TODO: perf.utilisation einrichten
};
movieMap.get(movieId)!.performances.push(performance);
}
// MovieGroups erstellen
dateInfo.performances = Array.from(movieMap.values()).map((entry) => ({
movie: entry.movie,
performances: entry.performances.sort((a, b) => a.start.getTime() - b.start.getTime()),
})) as MovieGroup[];
} }
} }
getMovieCount(index: number): number { getMovieCount(index: number): number {
// Hier kannst du später die echten Filmzahlen zurückgeben return this.dates[index].performances.length;
return index === 0 ? 10 : index === 1 ? 0 : 4;
} }
} }