Add performance scheduling feature for employees

Introduces a new PerformanceSchedulingComponent with UI and logic for creating single performances and recurring performance plans. Updates routing, navigation, and service APIs to support scheduling by employees and admins. Also includes minor refactoring and integration of MatDatepickerModule.
This commit is contained in:
2025-11-28 17:34:48 +01:00
parent 659f837578
commit 82b927c9a1
9 changed files with 529 additions and 4 deletions

View File

@@ -0,0 +1,294 @@
import { Component, inject, OnInit, DestroyRef, ViewChild } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, FormGroupDirective, ValidationErrors, Validators } from '@angular/forms';
import { MAT_DATE_LOCALE } from '@angular/material/core';
import { provideNativeDateAdapter } from '@angular/material/core';
import { HttpService } from '../http.service';
import { LoadingService } from '../loading.service';
import { catchError, forkJoin, map, Observable, of, startWith } from 'rxjs';
import { Film, Kinosaal, Plan, Sitzplatz, Vorstellung } from '@infinimotion/model-frontend';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatSnackBar } from '@angular/material/snack-bar';
@Component({
selector: 'app-performance-scheduling',
standalone: false,
templateUrl: './performance-scheduling.component.html',
styleUrl: './performance-scheduling.component.css',
providers: [
provideNativeDateAdapter(),
{ provide: MAT_DATE_LOCALE, useValue: 'de-DE' }
]
})
export class PerformanceSchedulingComponent implements OnInit {
private fb = inject(FormBuilder);
private http = inject(HttpService);
private destroyRef = inject(DestroyRef);
public loading = inject(LoadingService);
constructor(private snackBar: MatSnackBar) {}
@ViewChild('formDirectivePerformance') formDirectivePerformance!: FormGroupDirective;
@ViewChild('formDirectiveSeries') formDirectiveSeries!: FormGroupDirective;
minDate = new Date();
movies!: Film[];
filteredMoviesPerformance!: Observable<Film[]>;
filteredMoviesPlan!: Observable<Film[]>;
halls: Kinosaal[] = [];
hallSeatMap = new Map<number, Sitzplatz[]>();
weekdays = [
{ label: 'Montag', value: 1 },
{ label: 'Dienstag', value: 2 },
{ label: 'Mittwoch', value: 3 },
{ label: 'Donnerstag', value: 4 },
{ label: 'Freitag', value: 5 },
{ label: 'Samstag', value: 6 },
{ label: 'Sonntag', value: 7 }
];
weekdayFilter = (date: Date | null): boolean => {
if (!date) return false;
const selectedWeekday = this.seriesForm.get('weekday')?.value;
if (!selectedWeekday) return true;
const weekday = date.getDay() === 0 ? 7 : date.getDay();
return weekday === selectedWeekday;
};
performanceForm!: FormGroup;
seriesForm!: FormGroup;
ngOnInit() {
this.performanceForm = this.fb.group({
movie: ['', [Validators.required, Validators.minLength(3)]],
hall: ['', Validators.required],
date: [null, Validators.required],
time: ['', Validators.required]
}, {
validators: this.dateTimeInFutureValidator()
});
this.seriesForm = this.fb.group({
movie: ['', [Validators.required, Validators.minLength(3)]],
hall: ['', Validators.required],
weekday: ['', Validators.required],
first: [null, Validators.required],
time: ['', Validators.required]
}, {
validators: this.dateTimeInFutureValidator()
});
// Wochentagüberprüfung
const weekdayControl = this.seriesForm.get('weekday');
const dateControl = this.seriesForm.get('first');
weekdayControl?.valueChanges.subscribe(() => {
const currentDate = dateControl?.value;
if (currentDate && !this.weekdayFilter(currentDate)) {
dateControl?.setValue(null);
}
dateControl?.updateValueAndValidity();
});
dateControl?.valueChanges.subscribe((date: Date | null) => {
if (!date) return;
const weekday = date.getDay() === 0 ? 7 : date.getDay();
weekdayControl?.setValue(weekday, { emitEvent: false });
});
this.loadData();
}
dateTimeInFutureValidator() {
return (formGroup: AbstractControl): ValidationErrors | null => {
const dateControl = formGroup.get('date');
const timeControl = formGroup.get('time');
if (!dateControl?.value || !timeControl?.value) {
return null;
}
const selectedDateTime = this.combineDateAndTime(dateControl.value, timeControl.value);
const now = new Date();
if (selectedDateTime <= now) {
return { dateTimeInPast: true };
}
return null;
};
}
hasDateTimeError(formGroup: AbstractControl): boolean {
return formGroup.hasError('dateTimeInPast') &&
formGroup.get('date')?.touched === true &&
formGroup.get('time')?.touched === true;
}
hallName(formGroup: AbstractControl): string | undefined {
const hall: Kinosaal | null = formGroup.get('hall')?.value ?? null;
return hall?.name;
}
displayMovie(movie: Film | null): string {
return movie ? movie.title : '';
}
loadData() {
const moviesRequest = this.http.getMovies();
const seatsRequest = this.http.getAllSeats();
this.loading.show();
this.hallSeatMap.clear();
this.halls = [];
forkJoin([moviesRequest, seatsRequest]).subscribe({
next: ([movies, seats]) => {
this.movies = movies.sort((a, b) => a.title.localeCompare(b.title));
seats.forEach((seat) => {
const hallId = seat.row.hall.id;
if (!this.hallSeatMap.has(hallId)) {
this.halls.push(seat.row.hall);
this.hallSeatMap.set(hallId, []);
}
this.hallSeatMap.get(hallId)?.push(seat);
});
this.filteredMoviesPerformance = this.performanceForm.get('movie')!.valueChanges.pipe(
startWith(''),
map(value => this._filterMovies(value))
);
this.filteredMoviesPlan = this.seriesForm.get('movie')!.valueChanges.pipe(
startWith(''),
map(value => this._filterMovies(value))
);
this.loading.hide()
},
error: (err) => {
console.error('Fehler beim Laden der Filme/Kinosäle', err);
this.loading.showError(err);
},
});
}
private _filterMovies(value: string | Film): Film[] {
const filterValue = typeof value === 'string' ? value.toLowerCase() : value?.title.toLowerCase() ?? '';
return this.movies.filter(movie => movie.title.toLowerCase().includes(filterValue));
}
onPerformanceSubmit() {
if (!this.performanceForm.valid) {
return;
}
this.loading.show();
const formValue = this.performanceForm.value;
const datetime = this.combineDateAndTime(formValue.date, formValue.time);
const performance = this.generateNewPerformanceObject(datetime, formValue.hall, formValue.movie)
this.http.createPerformance(performance).pipe(
map(performance => {
this.loading.hide();
this.formDirectivePerformance.resetForm();
this.snackBar.open(
`Vorstellung wurde erfolgreich unter der ID ${performance.id} angelegt.`,
'Schließen',
{
duration: 5000,
panelClass: ['success-snackbar'],
horizontalPosition: 'right',
verticalPosition: 'top'
}
);
}),
catchError(err => {
this.loading.showError(err);
this.performanceForm.setErrors({ serverError: true });
console.log('Fehler beim Anlegen der Vorstellung', err);
return of(null);
}),
takeUntilDestroyed(this.destroyRef)
).subscribe();
}
private generateNewPerformanceObject(start: Date, hall: Kinosaal, movie: Film): Vorstellung {
return {
id: 0, // Wird durch das Backend gesetzt
start: start,
hall: hall,
movie: movie
};
}
private combineDateAndTime(date: Date, time: string): Date {
const [hours, minutes] = time.split(':').map(Number);
const combined = new Date(date);
combined.setHours(hours, minutes, 0, 0);
return combined;
}
onPlanSubmit() {
if (!this.seriesForm.valid) {
return;
}
this.loading.show();
const formValue = this.seriesForm.value;
const series = this.generateNewSeriesObject(formValue.weekday, formValue.time, formValue.first, formValue.hall, formValue.movie)
this.http.createPerformanceSeries(series).pipe(
map(performance => {
this.loading.hide();
this.formDirectiveSeries.resetForm();
this.seriesForm.reset({
movie: '',
hall: '',
weekday: '',
first: null,
time: ''
});
this.snackBar.open(
`Vorstellungsplan wurde erfolgreich unter der ID ${performance.id} angelegt.`,
'Schließen',
{
duration: 5000,
panelClass: ['success-snackbar'],
horizontalPosition: 'right',
verticalPosition: 'top'
}
);
}),
catchError(err => {
this.loading.showError(err);
this.seriesForm.setErrors({ serverError: true });
console.log('Fehler beim Anlegen des Plans', err);
return of(null);
}),
takeUntilDestroyed(this.destroyRef)
).subscribe();
}
private generateNewSeriesObject(weekday: number, time: Date, first: Date, hall: Kinosaal, movie: Film): Plan {
return {
id: 0, // Wird durch das Backend gesetzt
weekday: weekday,
time: time,
first: first,
hall: hall,
movie: movie
};
}
}