frontend-toegevoegd
This commit is contained in:
222
agiledancers-fe/src/app/shared/entity-editor.ts
Normal file
222
agiledancers-fe/src/app/shared/entity-editor.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user