S12 - Async feedback for scheduling
All checks were successful
Build, test and publish / verify (push) Successful in 7m25s

This commit is contained in:
2026-11-03 23:18:44 +01:00
parent f1785ed174
commit c241392132
34 changed files with 1310 additions and 79 deletions

View File

@@ -344,7 +344,7 @@
</div>
<div class="show-list">
@for (show of shows(); track show.id) {
<article class="show-card">
<article class="show-card" [id]="'show-' + show.id">
<div class="date-tile">
<strong>{{ firstDate(show) | date: 'dd' }}</strong
><span>{{ firstDate(show) | date: 'MMM yyyy' }}</span>
@@ -372,6 +372,48 @@
: 'Geen geldige planning. Plan de groepen opnieuw in.'
}}
</p>
@if (schedulingStatus.run(context.school.id, show.id); as run) {
<div class="scheduling-progress" role="status">
@if (!schedulingStatus.connected()) {
<p>
Verbinding verbroken. Status mogelijk verouderd; de berekening loopt door.
</p>
}
@if (run.state === 'RUNNING') {
<p>
Bezig met inplannen · {{ schedulingStatus.phase(run) }} ·
{{ schedulingStatus.elapsed(run) }}
</p>
<p>
{{
run.progress.feasible
? 'Geldige oplossing gevonden; optimalisatie loopt door.'
: 'Nog geen geldige oplossing gevonden.'
}}
</p>
} @else {
<p>{{ run.message }}</p>
<p>
Afgerond: {{ run.finishedAt | date: 'dd-MM-yyyy HH:mm:ss' }} · Duur:
{{ schedulingStatus.elapsed(run) }}
</p>
@if (run.summary; as summary) {
<p>
Aanwezigheid: {{ summary.attendanceExcess }} extra dagdelen boven voorkeur
(showcasedansers, exclusief coaches).
</p>
<p>
Spreiding: {{ summary.performanceSpread }} optredens verschil tussen drukste
en rustigste dagdeel.
</p>
<p>
Variatie: {{ summary.varietyPenalty }} strafpunten voor opeenvolgende
groepen (lager is beter).
</p>
}
}
</div>
}
<button
type="button"
class="button primary"

View File

@@ -1,5 +1,5 @@
import { SchoolTransfer } from '../shared/school-transfer';
import { Component, computed, DestroyRef, inject, OnInit, signal } from '@angular/core';
import { Component, computed, DestroyRef, effect, 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';
@@ -14,6 +14,7 @@ import {
Show,
} from '../core/api-models';
import { apiError } from '../core/api-error';
import { SchedulingStatus } from '../core/scheduling-status';
import { SchoolApi } from '../core/school-api';
import { EntityEditor } from '../shared/entity-editor';
import { ConfirmDialog } from '../shared/confirm-dialog';
@@ -57,7 +58,30 @@ export class SchoolPage implements OnInit {
readonly scheduleEditing = signal<Show | null>(null);
readonly members = signal<DanceGroup | null>(null);
readonly deleting = signal<DeleteTarget | null>(null);
readonly scheduling = signal<number[]>([]);
readonly schedulingStatus = inject(SchedulingStatus);
private readonly starting = signal<number[]>([]);
readonly scheduling = computed(() => [
...this.starting(),
...this.schedulingStatus
.runs()
.filter((run) => run.schoolId === this.data()?.school.id && run.state === 'RUNNING')
.map((run) => run.showId),
]);
private readonly completed = new Set<string>();
constructor() {
effect(() => {
const schoolId = this.data()?.school.id;
let changed = false;
for (const run of this.schedulingStatus.runs()) {
if (run.schoolId !== schoolId || run.state === 'RUNNING' || this.completed.has(run.runId))
continue;
this.completed.add(run.runId);
changed = true;
}
if (changed) void this.reload();
});
}
readonly deleteBusy = signal(false);
readonly deleteError = signal('');
readonly search = signal('');
@@ -151,20 +175,15 @@ export class SchoolPage implements OnInit {
)
)
return;
this.scheduling.update((ids) => [...ids, show.id]);
this.starting.update((ids) => [...ids, show.id]);
this.error.set('');
try {
const result = await firstValueFrom(this.api.schedule(schoolId, show.id));
this.notice.set(
result.optimal
? 'Optimaal conceptrooster.'
: 'Geldig conceptrooster; optimaliteit niet bewezen.',
);
await this.reload();
this.schedulingStatus.started(result);
} catch (error: unknown) {
this.error.set(apiError(error));
} finally {
this.scheduling.update((ids) => ids.filter((id) => id !== show.id));
this.starting.update((ids) => ids.filter((id) => id !== show.id));
}
}
async reload(): Promise<void> {