import { SchoolTransfer } from '../shared/school-transfer'; import { Component, computed, DestroyRef, effect, inject, OnInit, signal } from '@angular/core'; import { DatePipe } from '@angular/common'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Router, RouterLink, RouterLinkActive } from '@angular/router'; import { catchError, EMPTY, firstValueFrom, switchMap } from 'rxjs'; import { DanceGroup, EditorTarget, GROUP_STYLES, GROUP_TYPES, ResourceKind, SchoolData, Show, } from '../core/api-models'; import { apiError } from '../core/api-error'; import { SchedulingStatus } from '../core/scheduling-status'; import { SchoolApi } from '../core/school-api'; import { EntityEditor } from '../shared/entity-editor'; import { ConfirmDialog } from '../shared/confirm-dialog'; import { ScheduleEditor } from '../shared/schedule-editor'; import { CoachEditor } from '../shared/coach-editor'; import { GroupMembers } from '../shared/group-members'; interface DeleteTarget { kind: ResourceKind; id: number; name: string; explanation: string; } @Component({ selector: 'app-school-page', imports: [ ScheduleEditor, CoachEditor, SchoolTransfer, DatePipe, RouterLink, RouterLinkActive, EntityEditor, ConfirmDialog, GroupMembers, ], templateUrl: './school-page.html', }) export class SchoolPage implements OnInit { private readonly api = inject(SchoolApi); private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); private readonly destroyRef = inject(DestroyRef); readonly data = signal(null); readonly section = signal('overview'); readonly loading = signal(true); readonly error = signal(''); readonly notice = signal(''); readonly editor = signal(null); readonly scheduleEditing = signal(null); readonly members = signal(null); readonly deleting = signal(null); readonly schedulingStatus = inject(SchedulingStatus); private readonly starting = signal([]); readonly scheduling = computed(() => [ ...this.starting(), ...this.schedulingStatus .runs() .filter((run) => run.schoolId === this.data()?.school.id && run.state === 'RUNNING') .map((run) => run.showId), ]); private readonly completed = new Set(); constructor() { effect(() => { const schoolId = this.data()?.school.id; let changed = false; for (const run of this.schedulingStatus.runs()) { if (run.schoolId !== schoolId || run.state === 'RUNNING' || this.completed.has(run.runId)) continue; this.completed.add(run.runId); changed = true; } if (changed) void this.reload(); }); } readonly deleteBusy = signal(false); readonly deleteError = signal(''); readonly search = signal(''); readonly dancers = computed(() => [...(this.data()?.dancers ?? [])] .filter((d) => d.name.toLocaleLowerCase('nl').includes(this.search().toLocaleLowerCase('nl'))) .sort((a, b) => a.name.localeCompare(b.name, 'nl')), ); readonly shows = computed(() => [...(this.data()?.shows ?? [])].sort((a, b) => this.firstDate(a).localeCompare(this.firstDate(b)), ), ); readonly today = new Date().toLocaleDateString('en-CA'); readonly upcoming = computed(() => this.shows().find((show) => this.lastDate(show) >= this.today), ); readonly tabs = [ { value: 'overview', label: 'Overzicht' }, { value: 'dancers', label: 'Dansers' }, { value: 'groups', label: 'Dansgroepen' }, { value: 'shows', label: 'Shows' }, ]; ngOnInit(): void { this.route.paramMap .pipe( switchMap((params) => { this.loading.set(true); this.error.set(''); this.search.set(''); this.editor.set(null); this.members.set(null); this.scheduleEditing.set(null); this.deleting.set(null); this.section.set(params.get('section') ?? 'overview'); return this.api.schoolData(Number(params.get('schoolId'))).pipe( catchError((error) => { this.data.set(null); this.error.set(apiError(error)); this.loading.set(false); return EMPTY; }), ); }), takeUntilDestroyed(this.destroyRef), ) .subscribe((data) => { this.data.set(data); this.loading.set(false); }); } firstDate(show: Show): string { return show.dayParts.map((p) => p.date).sort()[0] ?? ''; } lastDate(show: Show): string { return ( show.dayParts .map((p) => p.date) .sort() .at(-1) ?? '' ); } dateRange(show: Show): string { const first = this.firstDate(show), last = this.lastDate(show); return !first ? 'No dates configured' : first === last ? first : `${first} – ${last}`; } scheduleValid(show: Show): boolean { return show.violations?.length === 0; } groupNamesFor(ids: number[]): string { return ( ids.map((id) => this.data()?.groups.find((g) => g.id === id)?.category ?? '?').join(', ') || 'Geen groepen' ); } async schedule(show: Show): Promise { const schoolId = this.data()?.school.id; if ( schoolId === undefined || this.scheduling().includes(show.id) || !show.dayParts.length || !this.data()?.groups.length ) return; if ( !window.confirm( `Groepen inplannen voor ${show.name}? Dit vervangt alle huidige toewijzingen.`, ) ) return; this.starting.update((ids) => [...ids, show.id]); this.error.set(''); try { const result = await firstValueFrom(this.api.schedule(schoolId, show.id)); this.schedulingStatus.started(result); } catch (error: unknown) { this.error.set(apiError(error)); } finally { this.starting.update((ids) => ids.filter((id) => id !== show.id)); } } async reload(): Promise { const id = this.data()?.school.id; if (id === undefined) return; this.error.set(''); try { const data = await firstValueFrom(this.api.schoolData(id)); if (this.data()?.school.id === id) this.data.set(data); } catch (error: unknown) { this.error.set(apiError(error)); } } retry(): void { window.location.reload(); } async saved(): Promise { this.editor.set(null); this.notice.set('Wijzigingen opgeslagen.'); await this.reload(); } groupType(value: string): string { return GROUP_TYPES.find((option) => option.value === value)?.label ?? value; } groupStyle(value: string): string { return GROUP_STYLES.find((option) => option.value === value)?.label ?? value; } groupNames(dancerId: number): string { return ( this.data() ?.groups.filter((group) => group.dancerIds.includes(dancerId)) .map((group) => group.category) .join(', ') || 'Nog geen groep' ); } private deletionRevision?: number; askDelete(target: DeleteTarget): void { this.deletionRevision = this.data()?.school.revision; this.deleteError.set(''); if (target.kind === 'group') { const names = this.data() ?.shows.filter((s) => s.dayParts.some((p) => p.danceGroupIds.length)) .map((s) => s.name) ?? []; if (names.length) target = { ...target, explanation: `${target.explanation} De planning kan ongeldig worden van: ${names.join(', ')}.`, }; } this.deleting.set(target); } membershipChanged(ids: number[]): void { void this.reload(); const groupId = this.members()?.id; this.data.update((data) => data ? { ...data, groups: data.groups.map((group) => group.id === groupId ? { ...group, dancerIds: [...ids] } : group, ), } : data, ); } async confirmDelete(): Promise { const target = this.deleting(); const schoolId = this.data()?.school.id; if (!target || schoolId === undefined || this.deleteBusy()) return; this.deleteBusy.set(true); this.deleteError.set(''); try { await firstValueFrom( this.api.delete(target.kind, schoolId, target.id, this.deletionRevision), ); this.deleting.set(null); if (target.kind === 'school') await this.router.navigate(['/schools']); else { this.notice.set(`${target.name} is verwijderd.`); await this.reload(); } } catch (error: unknown) { this.deleteError.set(apiError(error)); } finally { this.deleteBusy.set(false); } } }