frontend-toegevoegd
This commit is contained in:
423
agiledancers-fe/src/app/schools/school-page.html
Normal file
423
agiledancers-fe/src/app/schools/school-page.html
Normal file
@@ -0,0 +1,423 @@
|
||||
<a routerLink="/schools" class="back-link">← Alle dansscholen</a>
|
||||
@if (loading()) {
|
||||
<div class="loading-state" role="status"><span class="spinner"></span> Dansschool laden…</div>
|
||||
} @else if (!data()) {
|
||||
<div class="empty-state">
|
||||
<h1>Dansschool niet beschikbaar</h1>
|
||||
<p role="alert">{{ error() }}</p>
|
||||
<button type="button" class="button secondary" (click)="retry()">Opnieuw proberen</button>
|
||||
</div>
|
||||
} @else if (data(); as context) {
|
||||
<div class="page-heading school-heading">
|
||||
<div class="school-title">
|
||||
<span aria-hidden="true" class="avatar large">{{
|
||||
context.school.name.charAt(0).toUpperCase()
|
||||
}}</span>
|
||||
<div>
|
||||
<p class="eyebrow">MIJN DANSSCHOOL</p>
|
||||
<h1>{{ context.school.name }}</h1>
|
||||
<p class="lede">Jouw dansers. Jouw ritme. Jouw podium.</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="button secondary"
|
||||
(click)="editor.set({ kind: 'school', value: context.school })"
|
||||
>
|
||||
School bewerken
|
||||
</button>
|
||||
</div>
|
||||
<nav class="tabs" aria-label="Dansschool onderdelen">
|
||||
@for (tab of tabs; track tab.value) {
|
||||
<a
|
||||
[routerLink]="['/schools', context.school.id, tab.value]"
|
||||
routerLinkActive="active"
|
||||
ariaCurrentWhenActive="page"
|
||||
>{{ tab.label }}
|
||||
@switch (tab.value) {
|
||||
@case ('dancers') {
|
||||
<span>{{ context.dancers.length }}</span>
|
||||
}
|
||||
@case ('groups') {
|
||||
<span>{{ context.groups.length }}</span>
|
||||
}
|
||||
@case ('shows') {
|
||||
<span>{{ context.shows.length }}</span>
|
||||
}
|
||||
}
|
||||
</a>
|
||||
}
|
||||
</nav>
|
||||
@if (notice()) {
|
||||
<div class="alert success" role="status">
|
||||
{{ notice()
|
||||
}}<button type="button" aria-label="Melding sluiten" (click)="notice.set('')">×</button>
|
||||
</div>
|
||||
}
|
||||
@if (error()) {
|
||||
<div class="alert error" role="alert">
|
||||
{{ error()
|
||||
}}<button type="button" class="button secondary" (click)="reload()">Vernieuwen</button>
|
||||
</div>
|
||||
}
|
||||
@switch (section()) {
|
||||
@case ('overview') {
|
||||
<div class="stats-grid">
|
||||
<a class="stat" [routerLink]="['/schools', context.school.id, 'dancers']"
|
||||
><span class="stat-label">Dansers <span aria-hidden="true">↗</span></span
|
||||
><strong>{{ context.dancers.length }}</strong
|
||||
><span class="stat-caption">Alle talenten bij elkaar</span></a
|
||||
>
|
||||
<a class="stat" [routerLink]="['/schools', context.school.id, 'groups']"
|
||||
><span class="stat-label">Dansgroepen <span aria-hidden="true">↗</span></span
|
||||
><strong>{{ context.groups.length }}</strong
|
||||
><span class="stat-caption">Samen de vloer op</span></a
|
||||
>
|
||||
<a class="stat" [routerLink]="['/schools', context.school.id, 'shows']"
|
||||
><span class="stat-label">Shows <span aria-hidden="true">↗</span></span
|
||||
><strong>{{ context.shows.length }}</strong
|
||||
><span class="stat-caption">Momenten om naar uit te kijken</span></a
|
||||
>
|
||||
</div>
|
||||
<div class="overview-grid">
|
||||
<section class="panel">
|
||||
<p class="eyebrow">DE VOLGENDE STAP</p>
|
||||
<h2>Breng je school in beweging</h2>
|
||||
<p class="muted">Een goede show begint in de studio. Zorg dat iedereen een plek heeft.</p>
|
||||
<div class="quick-actions">
|
||||
<button type="button" (click)="editor.set({ kind: 'dancer' })">
|
||||
<span class="step-number">01</span
|
||||
><span
|
||||
><strong>Voeg een danser toe</strong
|
||||
><small>Maak ruimte voor nieuw talent</small></span
|
||||
><span aria-hidden="true">+</span>
|
||||
</button>
|
||||
<button type="button" (click)="editor.set({ kind: 'group' })">
|
||||
<span class="step-number">02</span
|
||||
><span
|
||||
><strong>Stel een dansgroep samen</strong
|
||||
><small>Verbind dansers met dezelfde passie</small></span
|
||||
><span aria-hidden="true">+</span>
|
||||
</button>
|
||||
<button type="button" (click)="editor.set({ kind: 'show' })">
|
||||
<span class="step-number">03</span
|
||||
><span
|
||||
><strong>Plan een show</strong
|
||||
><small>Zet het volgende podium op de kalender</small></span
|
||||
><span aria-hidden="true">+</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="show-spotlight">
|
||||
<div class="spotlight-decoration" aria-hidden="true">✳</div>
|
||||
<p class="eyebrow">IN DE SPOTLIGHT</p>
|
||||
@if (upcoming(); as show) {
|
||||
<h2>{{ show.name }}</h2>
|
||||
<p class="spotlight-date">{{ show.date | date: 'd MMMM yyyy' }}</p>
|
||||
<p>Drie dagdelen. Eén gedeelde passie.</p>
|
||||
<a class="button light" [routerLink]="['/schools', context.school.id, 'shows']"
|
||||
>Shows bekijken <span aria-hidden="true">→</span></a
|
||||
>
|
||||
} @else {
|
||||
<h2>Klaar voor<br />het grote podium?</h2>
|
||||
<p>Maak een show aan en geef je dansers iets om naartoe te werken.</p>
|
||||
<button type="button" class="button light" (click)="editor.set({ kind: 'show' })">
|
||||
Eerste show plannen <span aria-hidden="true">→</span>
|
||||
</button>
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
<section class="danger-zone">
|
||||
<div>
|
||||
<h3>Dansschool verwijderen</h3>
|
||||
<p>Verwijdert ook alle dansers, groepen en shows van deze school.</p>
|
||||
</div>
|
||||
<button
|
||||
class="button danger-outline"
|
||||
type="button"
|
||||
(click)="
|
||||
askDelete({
|
||||
kind: 'school',
|
||||
id: context.school.id,
|
||||
name: context.school.name,
|
||||
explanation:
|
||||
'Alle dansers, dansgroepen en shows van deze school worden ook definitief verwijderd.',
|
||||
})
|
||||
"
|
||||
>
|
||||
School verwijderen
|
||||
</button>
|
||||
</section>
|
||||
}
|
||||
@case ('dancers') {
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>Dansers</h2>
|
||||
<p class="muted">De mensen die jouw school in beweging brengen.</p>
|
||||
</div>
|
||||
<button type="button" class="button primary" (click)="editor.set({ kind: 'dancer' })">
|
||||
+ Danser toevoegen
|
||||
</button>
|
||||
</div>
|
||||
@if (context.dancers.length) {
|
||||
<label class="search-field"
|
||||
><span aria-hidden="true">⌕</span
|
||||
><input
|
||||
type="search"
|
||||
aria-label="Zoek een danser"
|
||||
placeholder="Zoek op naam…"
|
||||
[value]="search()"
|
||||
(input)="search.set($any($event.target).value)"
|
||||
/></label>
|
||||
}
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<caption class="sr-only">
|
||||
Dansers in
|
||||
{{
|
||||
context.school.name
|
||||
}}
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Danser</th>
|
||||
<th>Geboortedatum</th>
|
||||
<th>Dansgroepen</th>
|
||||
<th><span class="sr-only">Acties</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (dancer of dancers(); track dancer.id) {
|
||||
<tr>
|
||||
<td>
|
||||
<div class="person">
|
||||
<span aria-hidden="true" class="avatar small">{{
|
||||
dancer.name.charAt(0).toUpperCase()
|
||||
}}</span
|
||||
><strong>{{ dancer.name }}</strong>
|
||||
</div>
|
||||
</td>
|
||||
<td class="nowrap">{{ dancer.birthDate | date: 'd MMM yyyy' }}</td>
|
||||
<td class="muted">{{ groupNames(dancer.id) }}</td>
|
||||
<td>
|
||||
<div class="row-actions">
|
||||
<button
|
||||
class="text-button"
|
||||
type="button"
|
||||
[attr.aria-label]="dancer.name + ' bewerken'"
|
||||
(click)="editor.set({ kind: 'dancer', value: dancer })"
|
||||
>
|
||||
Bewerken</button
|
||||
><button
|
||||
class="text-button destructive"
|
||||
type="button"
|
||||
[attr.aria-label]="dancer.name + ' verwijderen'"
|
||||
(click)="
|
||||
askDelete({
|
||||
kind: 'dancer',
|
||||
id: dancer.id,
|
||||
name: dancer.name,
|
||||
explanation:
|
||||
'De danser wordt ook uit alle dansgroepen van deze school verwijderd.',
|
||||
})
|
||||
"
|
||||
>
|
||||
Verwijderen
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<div class="empty-state">
|
||||
<h3>{{ search() ? 'Geen dansers gevonden' : 'De dansvloer is nog leeg' }}</h3>
|
||||
<p>
|
||||
{{
|
||||
search()
|
||||
? 'Probeer een andere naam.'
|
||||
: 'Voeg je eerste danser toe. Daarna kun je die aan één of meerdere groepen koppelen.'
|
||||
}}
|
||||
</p>
|
||||
@if (!search()) {
|
||||
<button
|
||||
class="button primary"
|
||||
type="button"
|
||||
(click)="editor.set({ kind: 'dancer' })"
|
||||
>
|
||||
Danser toevoegen
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
@case ('groups') {
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>Dansgroepen</h2>
|
||||
<p class="muted">Iedere groep een eigen stijl. Iedere danser een plek.</p>
|
||||
</div>
|
||||
<button class="button primary" type="button" (click)="editor.set({ kind: 'group' })">
|
||||
+ Dansgroep toevoegen
|
||||
</button>
|
||||
</div>
|
||||
<div class="school-grid">
|
||||
@for (group of context.groups; track group.id) {
|
||||
<article class="group-card">
|
||||
<div class="card-top">
|
||||
<span class="group-symbol" aria-hidden="true">↗</span
|
||||
><span class="tag">{{ groupType(group.type) }}</span>
|
||||
</div>
|
||||
<h3>{{ group.category }}</h3>
|
||||
<p class="muted">
|
||||
{{ group.startAge }}–{{ group.endAge }} jaar · {{ group.dancerIds.length }} dansers
|
||||
</p>
|
||||
<div class="tag-list">
|
||||
@for (style of group.styles; track style) {
|
||||
<span class="style-tag">{{ groupStyle(style) }}</span>
|
||||
} @empty {
|
||||
<span class="muted">Nog geen dansstijlen</span>
|
||||
}
|
||||
</div>
|
||||
<button
|
||||
class="button secondary full-width"
|
||||
type="button"
|
||||
[attr.aria-label]="'Dansers beheren voor ' + group.category"
|
||||
(click)="members.set(group)"
|
||||
>
|
||||
Dansers beheren <span aria-hidden="true">→</span>
|
||||
</button>
|
||||
<div class="card-bottom">
|
||||
<button
|
||||
class="text-button"
|
||||
type="button"
|
||||
[attr.aria-label]="group.category + ' bewerken'"
|
||||
(click)="editor.set({ kind: 'group', value: group })"
|
||||
>
|
||||
Bewerken</button
|
||||
><button
|
||||
class="text-button destructive"
|
||||
type="button"
|
||||
[attr.aria-label]="group.category + ' verwijderen'"
|
||||
(click)="
|
||||
askDelete({
|
||||
kind: 'group',
|
||||
id: group.id,
|
||||
name: group.category,
|
||||
explanation:
|
||||
'De groep wordt verwijderd. De dansers blijven beschikbaar in de school.',
|
||||
})
|
||||
"
|
||||
>
|
||||
Verwijderen
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
} @empty {
|
||||
<div class="empty-state full-width">
|
||||
<span class="empty-symbol" aria-hidden="true">↗</span>
|
||||
<h2>Samen vind je het ritme</h2>
|
||||
<p>Maak een dansgroep aan en koppel er dansers aan.</p>
|
||||
<button class="button primary" type="button" (click)="editor.set({ kind: 'group' })">
|
||||
Eerste dansgroep aanmaken
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@case ('shows') {
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>Shows</h2>
|
||||
<p class="muted">Van maanden oefenen naar een onvergetelijk optreden.</p>
|
||||
</div>
|
||||
<button class="button primary" type="button" (click)="editor.set({ kind: 'show' })">
|
||||
+ Show toevoegen
|
||||
</button>
|
||||
</div>
|
||||
<div class="show-list">
|
||||
@for (show of shows(); track show.id) {
|
||||
<article class="show-card">
|
||||
<div class="date-tile">
|
||||
<strong>{{ show.date | date: 'dd' }}</strong
|
||||
><span>{{ show.date | date: 'MMM yyyy' }}</span>
|
||||
</div>
|
||||
<div class="show-details">
|
||||
<h3>{{ show.name }}</h3>
|
||||
<div class="tag-list">
|
||||
<span class="style-tag">Ochtend</span><span class="style-tag">Middag</span
|
||||
><span class="style-tag">Avond</span>
|
||||
</div>
|
||||
<p class="muted">Groepen indelen over dagdelen is nog niet beschikbaar.</p>
|
||||
</div>
|
||||
<div class="row-actions">
|
||||
<button
|
||||
class="text-button"
|
||||
type="button"
|
||||
[attr.aria-label]="show.name + ' bewerken'"
|
||||
(click)="editor.set({ kind: 'show', value: show })"
|
||||
>
|
||||
Bewerken</button
|
||||
><button
|
||||
class="text-button destructive"
|
||||
type="button"
|
||||
[attr.aria-label]="show.name + ' verwijderen'"
|
||||
(click)="
|
||||
askDelete({
|
||||
kind: 'show',
|
||||
id: show.id,
|
||||
name: show.name,
|
||||
explanation: 'Deze show wordt definitief verwijderd.',
|
||||
})
|
||||
"
|
||||
>
|
||||
Verwijderen
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
} @empty {
|
||||
<div class="empty-state">
|
||||
<span class="empty-symbol" aria-hidden="true">✦</span>
|
||||
<h2>Het podium wacht op je</h2>
|
||||
<p>Plan een show met een ochtend-, middag- en avonddeel.</p>
|
||||
<button class="button primary" type="button" (click)="editor.set({ kind: 'show' })">
|
||||
Eerste show plannen
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (editor(); as target) {
|
||||
<app-entity-editor
|
||||
[target]="target"
|
||||
[schoolId]="context.school.id"
|
||||
(closed)="editor.set(null)"
|
||||
(saved)="saved()"
|
||||
/>
|
||||
}
|
||||
@if (members(); as group) {
|
||||
<app-group-members
|
||||
[group]="group"
|
||||
[schoolId]="context.school.id"
|
||||
[dancers]="context.dancers"
|
||||
(closed)="members.set(null)"
|
||||
(changed)="membershipChanged($event)"
|
||||
/>
|
||||
}
|
||||
@if (deleting(); as target) {
|
||||
<app-confirm-dialog
|
||||
[name]="target.name"
|
||||
[explanation]="target.explanation"
|
||||
[busy]="deleteBusy()"
|
||||
[error]="deleteError()"
|
||||
(closed)="deleting.set(null)"
|
||||
(confirmed)="confirmDelete()"
|
||||
/>
|
||||
}
|
||||
}
|
||||
162
agiledancers-fe/src/app/schools/school-page.ts
Normal file
162
agiledancers-fe/src/app/schools/school-page.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { Component, computed, DestroyRef, 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,
|
||||
} from '../core/api-models';
|
||||
import { apiError } from '../core/api-error';
|
||||
import { SchoolApi } from '../core/school-api';
|
||||
import { EntityEditor } from '../shared/entity-editor';
|
||||
import { ConfirmDialog } from '../shared/confirm-dialog';
|
||||
import { GroupMembers } from '../shared/group-members';
|
||||
|
||||
interface DeleteTarget {
|
||||
kind: ResourceKind;
|
||||
id: number;
|
||||
name: string;
|
||||
explanation: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-school-page',
|
||||
imports: [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 members = signal<DanceGroup | null>(null);
|
||||
readonly deleting = signal<DeleteTarget | null>(null);
|
||||
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) => a.date.localeCompare(b.date)),
|
||||
);
|
||||
readonly today = new Date().toLocaleDateString('en-CA');
|
||||
readonly upcoming = computed(() => this.shows().find((show) => show.date >= 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.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);
|
||||
});
|
||||
}
|
||||
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'
|
||||
);
|
||||
}
|
||||
askDelete(target: DeleteTarget): void {
|
||||
this.deleteError.set('');
|
||||
this.deleting.set(target);
|
||||
}
|
||||
membershipChanged(ids: number[]): void {
|
||||
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.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
93
agiledancers-fe/src/app/schools/schools-page.html
Normal file
93
agiledancers-fe/src/app/schools/schools-page.html
Normal file
@@ -0,0 +1,93 @@
|
||||
<div class="page-heading">
|
||||
<div>
|
||||
<p class="eyebrow">JOUW ORGANISATIE</p>
|
||||
<h1>Alle ruimte om te dansen.</h1>
|
||||
<p class="lede">Van de eerste les tot het grote podium. Beheer je dansschool op één plek.</p>
|
||||
</div>
|
||||
<button type="button" class="button primary" (click)="editor.set(true)">
|
||||
<span aria-hidden="true">+</span> Dansschool toevoegen
|
||||
</button>
|
||||
</div>
|
||||
@if (notice()) {
|
||||
<div class="alert success" role="status">
|
||||
{{ notice()
|
||||
}}<button type="button" aria-label="Melding sluiten" (click)="notice.set('')">×</button>
|
||||
</div>
|
||||
}
|
||||
@if (loading()) {
|
||||
<div class="loading-state" role="status"><span class="spinner"></span> Dansscholen laden…</div>
|
||||
} @else if (error()) {
|
||||
<div class="alert error" role="alert">
|
||||
<p>{{ error() }}</p>
|
||||
<button class="button secondary" type="button" (click)="load()">Opnieuw proberen</button>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="stats-grid">
|
||||
<div class="stat">
|
||||
<span class="stat-label">Dansscholen</span><strong>{{ schools().length }}</strong
|
||||
><span class="stat-caption">Een plek voor iedere danser</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Dansers</span><strong>{{ totalDancers() }}</strong
|
||||
><span class="stat-caption">Samen in beweging</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Dansgroepen</span><strong>{{ totalGroups() }}</strong
|
||||
><span class="stat-caption">Ieder met een eigen ritme</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-heading">
|
||||
<h2>
|
||||
Mijn dansscholen <span class="count-badge">{{ schools().length }}</span>
|
||||
</h2>
|
||||
<span class="muted">Selecteer een school om aan de slag te gaan</span>
|
||||
</div>
|
||||
<div class="school-grid">
|
||||
@for (school of schools(); track school.id) {
|
||||
<a class="school-card" [routerLink]="['/schools', school.id, 'overview']">
|
||||
<div class="card-top">
|
||||
<span aria-hidden="true" class="avatar">{{ school.name.charAt(0).toUpperCase() }}</span
|
||||
><span class="round-arrow" aria-hidden="true">↗</span>
|
||||
</div>
|
||||
<h3>{{ school.name }}</h3>
|
||||
<p>Dansschool</p>
|
||||
<div class="card-meta">
|
||||
<span
|
||||
><strong>{{ school.dancerIds.length }}</strong> dansers</span
|
||||
><span
|
||||
><strong>{{ school.danceGroupIds.length }}</strong> groepen</span
|
||||
><span
|
||||
><strong>{{ school.showIds.length }}</strong> shows</span
|
||||
>
|
||||
</div>
|
||||
<div class="card-link">Dansschool beheren <span aria-hidden="true">→</span></div>
|
||||
</a>
|
||||
} @empty {
|
||||
<div class="empty-state full-width">
|
||||
<span class="empty-symbol" aria-hidden="true">✳</span>
|
||||
<h2>Hier begint jouw dansschool</h2>
|
||||
<p>Maak een school aan en geef je dansers, groepen en shows een thuis.</p>
|
||||
<button class="button primary" type="button" (click)="editor.set(true)">
|
||||
Eerste dansschool aanmaken
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<aside class="tip-panel">
|
||||
<span class="tip-icon" aria-hidden="true">✦</span>
|
||||
<div>
|
||||
<h3>Van studio naar spotlight</h3>
|
||||
<p>
|
||||
Voeg je dansers toe, stel groepen samen en maak alvast een show aan. Zo bouw je stap voor
|
||||
stap aan je volgende optreden.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
}
|
||||
@if (editor()) {
|
||||
<app-entity-editor
|
||||
[target]="{ kind: 'school' }"
|
||||
(closed)="editor.set(false)"
|
||||
(saved)="created()"
|
||||
/>
|
||||
}
|
||||
55
agiledancers-fe/src/app/schools/schools-page.ts
Normal file
55
agiledancers-fe/src/app/schools/schools-page.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Component, computed, DestroyRef, inject, OnInit, signal } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { School } from '../core/api-models';
|
||||
import { SchoolApi } from '../core/school-api';
|
||||
import { apiError } from '../core/api-error';
|
||||
import { EntityEditor } from '../shared/entity-editor';
|
||||
|
||||
@Component({
|
||||
selector: 'app-schools-page',
|
||||
imports: [RouterLink, EntityEditor],
|
||||
templateUrl: './schools-page.html',
|
||||
})
|
||||
export class SchoolsPage implements OnInit {
|
||||
private readonly api = inject(SchoolApi);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
readonly schools = signal<School[]>([]);
|
||||
readonly loading = signal(true);
|
||||
readonly error = signal('');
|
||||
readonly editor = signal(false);
|
||||
readonly notice = signal('');
|
||||
readonly totalDancers = computed(() =>
|
||||
this.schools().reduce((sum, school) => sum + school.dancerIds.length, 0),
|
||||
);
|
||||
readonly totalGroups = computed(() =>
|
||||
this.schools().reduce((sum, school) => sum + school.danceGroupIds.length, 0),
|
||||
);
|
||||
ngOnInit(): void {
|
||||
this.load();
|
||||
}
|
||||
load(): void {
|
||||
this.loading.set(true);
|
||||
this.error.set('');
|
||||
this.api
|
||||
.schools()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: (schools) => {
|
||||
this.schools.set(schools);
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: (error) => {
|
||||
this.error.set(apiError(error));
|
||||
this.loading.set(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
created(): void {
|
||||
this.editor.set(false);
|
||||
this.notice.set(
|
||||
'Dansschool aangemaakt. Open de school om dansers, groepen en shows toe te voegen.',
|
||||
);
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user