Files
agiledancers/agiledancers-fe/src/app/schools/school-page.ts
Wanja Krah 694bf3fa86
All checks were successful
Build, test and publish / verify (push) Successful in 7m31s
S16 postgresql opslag added
2026-11-04 17:10:25 +01:00

277 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<SchoolData | null>(null);
readonly section = signal('overview');
readonly loading = signal(true);
readonly error = signal('');
readonly notice = signal('');
readonly editor = signal<EditorTarget | null>(null);
readonly scheduleEditing = signal<Show | null>(null);
readonly members = signal<DanceGroup | null>(null);
readonly deleting = signal<DeleteTarget | null>(null);
readonly schedulingStatus = inject(SchedulingStatus);
private readonly starting = signal<number[]>([]);
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<string>();
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<void> {
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<void> {
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<void> {
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<void> {
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);
}
}
}