Files
agiledancers/agiledancers-fe/src/app/shared/entity-editor.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

351 lines
11 KiB
TypeScript

import { Component, inject, input, OnInit, output, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { firstValueFrom, Observable } from 'rxjs';
import {
EditorTarget,
GROUP_STYLES,
GROUP_TYPES,
GroupStyle,
GroupType,
DayPart,
Show,
} from '../core/api-models';
import { SchoolApi } from '../core/school-api';
import { apiError } from '../core/api-error';
import { DialogFrame } from './dialog-frame';
@Component({
selector: 'app-entity-editor',
imports: [FormsModule, DialogFrame],
template: `
<app-dialog-frame [title]="title" [busy]="busy()" (dismissed)="closed.emit()">
<form #form="ngForm" (ngSubmit)="form.valid && save()">
@if (error()) {
<p class="alert error" role="alert">{{ error() }}</p>
}
@if (target().kind === 'group') {
<label
>Naam / categorie<input
autofocus
name="category"
[(ngModel)]="name"
required
pattern=".*\\S.*"
maxlength="160"
placeholder="Bijvoorbeeld: Hiphop tieners"
/></label>
<label for="group-type">Type</label
><select id="group-type" name="type" [(ngModel)]="type" required>
@for (option of types; track option.value) {
<option [ngValue]="option.value">{{ option.label }}</option>
}
</select>
<div class="form-grid">
<label
>Leeftijd vanaf<input
type="number"
name="startAge"
[(ngModel)]="startAge"
required
min="0"
step="1"
/></label>
<label
>Leeftijd tot en met<input
type="number"
name="endAge"
[(ngModel)]="endAge"
required
[min]="startAge ?? 0"
step="1"
/></label>
</div>
<fieldset>
<legend>Dansstijlen <span class="muted">(optioneel)</span></legend>
<div class="checkbox-grid">
@for (style of styles; track style.value) {
<label class="checkbox"
><input
type="checkbox"
[name]="style.value"
[ngModel]="selectedStyles.includes(style.value)"
(ngModelChange)="toggleStyle(style.value, $event)"
/>{{ style.label }}</label
>
}
</div>
</fieldset>
} @else {
<label
>Naam<input
autofocus
name="name"
[(ngModel)]="name"
required
pattern=".*\\S.*"
maxlength="160"
[placeholder]="target().kind === 'school' ? 'Bijvoorbeeld: 24 Dance Studio' : 'Naam'"
/></label>
@if (target().kind === 'dancer') {
<label
>Geboortedatum<input
type="date"
name="birthDate"
[(ngModel)]="date"
required
[max]="today"
/></label>
}
@if (target().kind === 'show') {
<fieldset>
<legend>Gedateerde dagdelen (maximaal zes)</legend>
@for (part of dayParts; track $index; let i = $index) {
<div class="form-grid">
<label
>Datum<input
type="date"
[name]="'partDate' + i"
[(ngModel)]="part.date"
required
/></label>
<label
>Dagdeel<select [name]="'partType' + i" [(ngModel)]="part.type" required>
<option value="MORNING">Ochtend</option>
<option value="AFTERNOON">Middag</option>
<option value="EVENING">Avond</option>
</select></label
>
<button type="button" class="text-button" (click)="removePart(i)">
Dagdeel verwijderen
</button>
</div>
}
<button
type="button"
class="button secondary"
[disabled]="dayParts.length >= 6"
(click)="addPart()"
>
Dagdeel toevoegen
</button>
</fieldset>
@if (dayParts.length) {
<label
>Gewenst aantal dagdelen (x)
<input
type="number"
name="preferredDayParts"
[(ngModel)]="preferredDayParts"
min="1"
max="6"
step="1"
required
/>
</label>
<p class="form-hint">
Gewone groepen dansen in precies x dagdelen. Kleuters eenmaal; showcases ieder
dagdeel. Is x groter dan het aantal dagdelen, corrigeer x vóór inplannen.
</p>
} @else {
<p class="form-hint">Zonder dagdelen blijft deze show ongepland.</p>
}
}
}
<div class="dialog-actions">
<button
type="button"
class="button secondary"
[disabled]="busy()"
(click)="closed.emit()"
>
Annuleren
</button>
<button
class="button primary"
type="submit"
[disabled]="busy() || form.invalid || !validDetails()"
>
{{ busy() ? 'Opslaan…' : 'Opslaan' }}
</button>
</div>
</form>
</app-dialog-frame>
`,
})
export class EntityEditor implements OnInit {
readonly target = input.required<EditorTarget>();
readonly revision = input<number>();
private originalRevision?: number;
readonly schoolId = input<number>();
readonly shows = input<Show[]>([]);
readonly closed = output<void>();
readonly saved = output<void>();
readonly busy = signal(false);
readonly error = signal('');
readonly types = GROUP_TYPES;
readonly styles = GROUP_STYLES;
readonly today = new Date().toLocaleDateString('en-CA');
private readonly api = inject(SchoolApi);
dayParts: DayPart[] = [];
private preferredDayPartsValue = 2;
private preferredDayPartsEdited = false;
get preferredDayParts(): number {
return this.preferredDayPartsValue;
}
set preferredDayParts(value: number) {
this.preferredDayPartsValue = value;
this.preferredDayPartsEdited = true;
}
name = '';
date = '';
type: GroupType = 'REGULAR';
startAge: number | null = 0;
endAge: number | null = 18;
selectedStyles: GroupStyle[] = [];
get title(): string {
const labels = { school: 'Dansschool', dancer: 'Danser', group: 'Dansgroep', show: 'Show' };
return `${labels[this.target().kind]} ${this.target().value ? 'bewerken' : 'toevoegen'}`;
}
ngOnInit(): void {
this.originalRevision = this.revision();
const target = this.target();
if (!target.value) return;
if (target.kind === 'group') {
this.name = target.value.category;
this.type = target.value.type;
this.startAge = target.value.startAge;
this.endAge = target.value.endAge;
this.selectedStyles = [...target.value.styles];
} else {
this.name = target.value.name;
if (target.kind === 'dancer') this.date = target.value.birthDate;
if (target.kind === 'show') {
this.dayParts = target.value.dayParts.map((p) => ({ ...p }));
this.preferredDayParts = target.value.preferredDayParts;
}
}
}
addPart(): void {
if (this.dayParts.length >= 6) return;
this.dayParts.push({ date: '', type: 'AFTERNOON', danceGroupIds: [] });
if (!this.target().value && !this.preferredDayPartsEdited)
this.preferredDayPartsValue = Math.min(2, this.dayParts.length);
}
removePart(index: number): void {
this.dayParts.splice(index, 1);
}
validCounts(): boolean {
return (
Number.isInteger(this.preferredDayParts) &&
this.preferredDayParts >= 1 &&
this.preferredDayParts <= 6
);
}
toggleStyle(style: GroupStyle, checked: boolean): void {
this.selectedStyles = checked
? [...this.selectedStyles, style]
: this.selectedStyles.filter((value) => value !== style);
}
validDetails(): boolean {
if (!this.name.trim()) return false;
if (this.target().kind === 'dancer') return !!this.date && this.date <= this.today;
if (this.target().kind === 'show')
return (
this.dayParts.length <= 6 &&
this.dayParts.every((p) => !!p.date && !!p.type) &&
new Set(this.dayParts.map((p) => p.date + ':' + p.type)).size === this.dayParts.length &&
(!this.dayParts.length || this.validCounts())
);
if (this.target().kind === 'group')
return (
this.startAge !== null &&
this.endAge !== null &&
Number.isInteger(this.startAge) &&
Number.isInteger(this.endAge) &&
this.startAge >= 0 &&
this.endAge >= this.startAge
);
return true;
}
async save(): Promise<void> {
if (this.busy() || !this.validDetails()) return;
const current = this.target();
let affected: Show[] = [];
if (current.kind === 'group' && (!current.value || current.value.type !== this.type))
affected = this.shows();
if (
current.kind === 'show' &&
current.value &&
(JSON.stringify(current.value.dayParts.map((p) => ({ date: p.date, type: p.type }))) !==
JSON.stringify(this.dayParts.map((p) => ({ date: p.date, type: p.type }))) ||
current.value.preferredDayParts !== this.preferredDayParts)
)
affected = [current.value];
const names = affected
.filter((s) => s.dayParts.some((p) => p.danceGroupIds.length))
.map((s) => s.name);
if (
names.length &&
!window.confirm(
`Deze wijziging kan de planning ongeldig maken van: ${names.join(', ')}. Doorgaan?`,
)
)
return;
this.busy.set(true);
this.error.set('');
const target = this.target();
const id = target.value?.id;
const schoolId = this.schoolId();
let request: Observable<unknown>;
switch (target.kind) {
case 'school':
request = this.api.saveSchool({ name: this.name.trim() }, id, this.originalRevision);
break;
case 'dancer':
request = this.api.saveDancer(
schoolId!,
{ name: this.name.trim(), birthDate: this.date },
id,
this.originalRevision,
);
break;
case 'group':
request = this.api.saveGroup(
schoolId!,
{
category: this.name.trim(),
type: this.type,
startAge: this.startAge!,
endAge: this.endAge!,
styles: this.selectedStyles,
},
id,
this.originalRevision,
);
break;
case 'show':
request = this.api.saveShow(
schoolId!,
{
name: this.name.trim(),
dayParts: this.dayParts.map((p) => ({ date: p.date, type: p.type })),
preferredDayParts: this.preferredDayParts,
},
id,
this.originalRevision,
);
break;
}
try {
await firstValueFrom(request);
this.saved.emit();
} catch (error: unknown) {
this.error.set(apiError(error));
} finally {
this.busy.set(false);
}
}
}