All checks were successful
Build, test and publish / verify (push) Successful in 7m31s
188 lines
6.4 KiB
TypeScript
188 lines
6.4 KiB
TypeScript
import { Component, inject, input, OnInit, output, signal } from '@angular/core';
|
|
import { FormsModule } from '@angular/forms';
|
|
import { firstValueFrom } from 'rxjs';
|
|
import { DanceGroup, 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-schedule-editor',
|
|
imports: [FormsModule, DialogFrame],
|
|
template: `
|
|
<app-dialog-frame
|
|
[title]="show().name + ' — indeling'"
|
|
[busy]="busy()"
|
|
(dismissed)="closed.emit()"
|
|
>
|
|
<p>
|
|
{{ !dirty() && current()?.finalized ? 'Definitief' : 'Concept' }} · preferredDayParts =
|
|
{{ show().preferredDayParts }}
|
|
</p>
|
|
<p>
|
|
Tussen twee dansen van dezelfde danser zijn twee andere optredens nodig. Je kunt een
|
|
onvolledige indeling als concept opslaan.
|
|
</p>
|
|
@if (error()) {
|
|
<p role="alert" class="alert error">{{ error() }}</p>
|
|
}
|
|
@if (dirty()) {
|
|
<p role="status">
|
|
Niet-opgeslagen wijzigingen. Sla het concept op om alle regels te controleren.
|
|
</p>
|
|
}
|
|
@for (violation of current()?.violations ?? []; track $index) {
|
|
<p class="alert error">{{ violation }}</p>
|
|
}
|
|
<fieldset [disabled]="busy()">
|
|
@for (part of show().dayParts; track $index; let p = $index) {
|
|
<fieldset>
|
|
<legend>
|
|
{{ part.date }} · {{ part.type }} ({{ assignments[p].length }} optredens)
|
|
</legend>
|
|
<ol>
|
|
@for (id of assignments[p]; track $index; let i = $index) {
|
|
<li>
|
|
{{ name(id) }}
|
|
<button
|
|
type="button"
|
|
[disabled]="i === 0"
|
|
(click)="reorder(p, i, -1)"
|
|
[attr.aria-label]="name(id) + ' omhoog'"
|
|
>
|
|
↑
|
|
</button>
|
|
<button
|
|
type="button"
|
|
[disabled]="i === assignments[p].length - 1"
|
|
(click)="reorder(p, i, 1)"
|
|
[attr.aria-label]="name(id) + ' omlaag'"
|
|
>
|
|
↓
|
|
</button>
|
|
<label
|
|
>Verplaatsen naar
|
|
<select [ngModel]="p" (ngModelChange)="move(p, i, +$event)">
|
|
@for (target of show().dayParts; track $index; let t = $index) {
|
|
<option [ngValue]="t">{{ target.date }} · {{ target.type }}</option>
|
|
}
|
|
</select>
|
|
</label>
|
|
<button type="button" (click)="remove(p, i)">Optreden verwijderen</button>
|
|
</li>
|
|
}
|
|
</ol>
|
|
<label
|
|
>Groep toevoegen
|
|
<select [ngModel]="null" (ngModelChange)="add(p, +$event)">
|
|
<option [ngValue]="null">Kies een groep</option>
|
|
@for (group of groups(); track group.id) {
|
|
@if (group.dancerIds.length && !assignments[p].includes(group.id)) {
|
|
<option [ngValue]="group.id">{{ group.category }}</option>
|
|
}
|
|
}
|
|
</select>
|
|
</label>
|
|
</fieldset>
|
|
}
|
|
</fieldset>
|
|
<div class="dialog-actions">
|
|
<button type="button" class="button secondary" [disabled]="busy()" (click)="closed.emit()">
|
|
Gereed
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="button primary"
|
|
[disabled]="busy() || !dirty()"
|
|
(click)="save()"
|
|
>
|
|
Concept opslaan
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="button primary"
|
|
[disabled]="busy() || dirty() || current()?.finalized || !!current()?.violations?.length"
|
|
(click)="finalize()"
|
|
>
|
|
Definitief maken
|
|
</button>
|
|
</div>
|
|
</app-dialog-frame>
|
|
`,
|
|
})
|
|
export class ScheduleEditor implements OnInit {
|
|
readonly show = input.required<Show>();
|
|
readonly groups = input.required<DanceGroup[]>();
|
|
readonly revision = input<number>();
|
|
private originalRevision?: number;
|
|
readonly schoolId = input.required<number>();
|
|
readonly closed = output<void>();
|
|
readonly saved = output<void>();
|
|
readonly busy = signal(false);
|
|
readonly dirty = signal(false);
|
|
readonly error = signal('');
|
|
readonly current = signal<Show | null>(null);
|
|
private readonly api = inject(SchoolApi);
|
|
assignments: number[][] = [];
|
|
|
|
ngOnInit(): void {
|
|
this.originalRevision = this.revision();
|
|
this.current.set(this.show());
|
|
this.assignments = this.show().dayParts.map((p) => [...p.danceGroupIds]);
|
|
}
|
|
name(id: number): string {
|
|
return this.groups().find((g) => g.id === id)?.category ?? `Groep ${id}`;
|
|
}
|
|
reorder(p: number, i: number, delta: number): void {
|
|
const list = this.assignments[p];
|
|
[list[i], list[i + delta]] = [list[i + delta], list[i]];
|
|
this.dirty.set(true);
|
|
}
|
|
move(p: number, i: number, target: number): void {
|
|
if (target === p || this.assignments[target].includes(this.assignments[p][i])) return;
|
|
this.assignments[target].push(this.assignments[p].splice(i, 1)[0]);
|
|
this.dirty.set(true);
|
|
}
|
|
add(p: number, id: number): void {
|
|
if (!id || this.assignments[p].includes(id)) return;
|
|
this.assignments[p].push(id);
|
|
this.dirty.set(true);
|
|
}
|
|
remove(p: number, i: number): void {
|
|
this.assignments[p].splice(i, 1);
|
|
this.dirty.set(true);
|
|
}
|
|
async save(): Promise<void> {
|
|
await this.perform(false);
|
|
}
|
|
async finalize(): Promise<void> {
|
|
await this.perform(true);
|
|
}
|
|
private async perform(finalize: boolean): Promise<void> {
|
|
if (this.busy()) return;
|
|
this.busy.set(true);
|
|
this.error.set('');
|
|
try {
|
|
const result = await firstValueFrom(
|
|
finalize
|
|
? this.api.finalize(this.schoolId(), this.show().id, this.originalRevision)
|
|
: this.api.saveAssignments(
|
|
this.schoolId(),
|
|
this.show().id,
|
|
this.assignments,
|
|
this.originalRevision,
|
|
),
|
|
);
|
|
if (this.originalRevision !== undefined) this.originalRevision++;
|
|
this.current.set(result);
|
|
this.assignments = result.dayParts.map((p) => [...p.danceGroupIds]);
|
|
this.dirty.set(false);
|
|
this.saved.emit();
|
|
} catch (error: unknown) {
|
|
this.error.set(apiError(error));
|
|
} finally {
|
|
this.busy.set(false);
|
|
}
|
|
}
|
|
}
|