frontend-toegevoegd

This commit is contained in:
2026-10-27 15:34:56 +01:00
parent 777d9f199d
commit 02337fafaa
43 changed files with 10548 additions and 0 deletions

View File

@@ -0,0 +1,222 @@
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 } 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') {
<label>Datum<input type="date" name="showDate" [(ngModel)]="date" required /></label>
<p class="form-hint">
De show krijgt automatisch een ochtend-, middag- en avonddeel. De groepsindeling volgt
later.
</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 schoolId = input<number>();
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);
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 {
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.date = target.value.date;
}
}
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.date;
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;
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);
break;
case 'dancer':
request = this.api.saveDancer(
schoolId!,
{ name: this.name.trim(), birthDate: this.date },
id,
);
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,
);
break;
case 'show':
request = this.api.saveShow(schoolId!, { name: this.name.trim(), date: this.date }, id);
break;
}
try {
await firstValueFrom(request);
this.saved.emit();
} catch (error: unknown) {
this.error.set(apiError(error));
} finally {
this.busy.set(false);
}
}
}