S9 OR Tools basic scheduling
All checks were successful
Build, test and publish / verify (push) Successful in 1m48s
All checks were successful
Build, test and publish / verify (push) Successful in 1m48s
This commit is contained in:
@@ -114,7 +114,7 @@
|
||||
<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 class="spotlight-date">{{ dateRange(show) }}</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
|
||||
@@ -345,16 +345,39 @@
|
||||
@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>
|
||||
<strong>{{ firstDate(show) | date: 'dd' }}</strong
|
||||
><span>{{ firstDate(show) | 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>
|
||||
<p>{{ dateRange(show) }}</p>
|
||||
@for (part of show.dayParts; track $index) {
|
||||
<p>{{ part.date }} · {{ part.type }}: {{ groupNamesFor(part.danceGroupIds) }}</p>
|
||||
}
|
||||
<p role="status">
|
||||
{{
|
||||
scheduleValid(show)
|
||||
? 'Planning voldoet aan alle optredensaantallen.'
|
||||
: 'Geen geldige planning. Plan de groepen opnieuw in.'
|
||||
}}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
class="button primary"
|
||||
[disabled]="
|
||||
scheduling().includes(show.id) || !context.groups.length || !show.dayParts.length
|
||||
"
|
||||
(click)="schedule(show)"
|
||||
>
|
||||
{{
|
||||
scheduling().includes(show.id)
|
||||
? 'Bezig met inplannen…'
|
||||
: 'Schedule groups — vervangt huidige toewijzingen'
|
||||
}}
|
||||
</button>
|
||||
@if (!context.groups.length || !show.dayParts.length) {
|
||||
<p>Inplannen vereist minstens één dansgroep en één dagdeel.</p>
|
||||
}
|
||||
</div>
|
||||
<div class="row-actions">
|
||||
<button
|
||||
@@ -385,7 +408,7 @@
|
||||
<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>
|
||||
<p>Plan een show met maximaal zes gedateerde dagdelen.</p>
|
||||
<button class="button primary" type="button" (click)="editor.set({ kind: 'show' })">
|
||||
Eerste show plannen
|
||||
</button>
|
||||
@@ -398,6 +421,7 @@
|
||||
<app-entity-editor
|
||||
[target]="target"
|
||||
[schoolId]="context.school.id"
|
||||
[shows]="context.shows"
|
||||
(closed)="editor.set(null)"
|
||||
(saved)="saved()"
|
||||
/>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
GROUP_TYPES,
|
||||
ResourceKind,
|
||||
SchoolData,
|
||||
Show,
|
||||
} from '../core/api-models';
|
||||
import { apiError } from '../core/api-error';
|
||||
import { SchoolApi } from '../core/school-api';
|
||||
@@ -51,6 +52,7 @@ export class SchoolPage implements OnInit {
|
||||
readonly editor = signal<EditorTarget | null>(null);
|
||||
readonly members = signal<DanceGroup | null>(null);
|
||||
readonly deleting = signal<DeleteTarget | null>(null);
|
||||
readonly scheduling = signal<number[]>([]);
|
||||
readonly deleteBusy = signal(false);
|
||||
readonly deleteError = signal('');
|
||||
readonly search = signal('');
|
||||
@@ -60,10 +62,14 @@ export class SchoolPage implements OnInit {
|
||||
.sort((a, b) => a.name.localeCompare(b.name, 'nl')),
|
||||
);
|
||||
readonly shows = computed(() =>
|
||||
[...(this.data()?.shows ?? [])].sort((a, b) => a.date.localeCompare(b.date)),
|
||||
[...(this.data()?.shows ?? [])].sort((a, b) =>
|
||||
this.firstDate(a).localeCompare(this.firstDate(b)),
|
||||
),
|
||||
);
|
||||
readonly today = new Date().toLocaleDateString('en-CA');
|
||||
readonly upcoming = computed(() => this.shows().find((show) => show.date >= this.today));
|
||||
readonly upcoming = computed(() =>
|
||||
this.shows().find((show) => this.lastDate(show) >= this.today),
|
||||
);
|
||||
readonly tabs = [
|
||||
{ value: 'overview', label: 'Overzicht' },
|
||||
{ value: 'dancers', label: 'Dansers' },
|
||||
@@ -98,6 +104,74 @@ export class SchoolPage implements OnInit {
|
||||
this.loading.set(false);
|
||||
});
|
||||
}
|
||||
firstDate(show: Show): string {
|
||||
return show.dayParts.map((p) => p.date).sort()[0] ?? '';
|
||||
}
|
||||
lastDate(show: Show): string {
|
||||
return (
|
||||
show.dayParts
|
||||
.map((p) => p.date)
|
||||
.sort()
|
||||
.at(-1) ?? ''
|
||||
);
|
||||
}
|
||||
dateRange(show: Show): string {
|
||||
const first = this.firstDate(show),
|
||||
last = this.lastDate(show);
|
||||
return !first ? 'No dates configured' : first === last ? first : `${first} – ${last}`;
|
||||
}
|
||||
scheduleValid(show: Show): boolean {
|
||||
const groups = this.data()?.groups ?? [];
|
||||
return (
|
||||
!!groups.length &&
|
||||
!!show.dayParts.length &&
|
||||
groups.every(
|
||||
(g) =>
|
||||
show.dayParts.filter((p) => p.danceGroupIds.includes(g.id)).length ===
|
||||
show.appearances[g.type],
|
||||
) &&
|
||||
show.dayParts.every(
|
||||
(p) =>
|
||||
new Set(p.danceGroupIds).size === p.danceGroupIds.length &&
|
||||
p.danceGroupIds.every((id) => groups.some((g) => g.id === id)),
|
||||
)
|
||||
);
|
||||
}
|
||||
groupNamesFor(ids: number[]): string {
|
||||
return (
|
||||
ids.map((id) => this.data()?.groups.find((g) => g.id === id)?.category ?? '?').join(', ') ||
|
||||
'Geen groepen'
|
||||
);
|
||||
}
|
||||
async schedule(show: Show): Promise<void> {
|
||||
const schoolId = this.data()?.school.id;
|
||||
if (
|
||||
schoolId === undefined ||
|
||||
this.scheduling().includes(show.id) ||
|
||||
!show.dayParts.length ||
|
||||
!this.data()?.groups.length
|
||||
)
|
||||
return;
|
||||
if (
|
||||
!window.confirm(
|
||||
`Groepen inplannen voor ${show.name}? Dit vervangt alle huidige toewijzingen.`,
|
||||
)
|
||||
)
|
||||
return;
|
||||
this.scheduling.update((ids) => [...ids, show.id]);
|
||||
this.error.set('');
|
||||
try {
|
||||
const result = await firstValueFrom(this.api.schedule(schoolId, show.id));
|
||||
this.notice.set(
|
||||
result.optimal ? 'Optimale balans.' : 'Geldige planning; optimale balans niet bewezen.',
|
||||
);
|
||||
await this.reload();
|
||||
} catch (error: unknown) {
|
||||
this.error.set(apiError(error));
|
||||
} finally {
|
||||
this.scheduling.update((ids) => ids.filter((id) => id !== show.id));
|
||||
}
|
||||
}
|
||||
async reload(): Promise<void> {
|
||||
const id = this.data()?.school.id;
|
||||
if (id === undefined) return;
|
||||
@@ -133,6 +207,17 @@ export class SchoolPage implements OnInit {
|
||||
}
|
||||
askDelete(target: DeleteTarget): void {
|
||||
this.deleteError.set('');
|
||||
if (target.kind === 'group') {
|
||||
const names =
|
||||
this.data()
|
||||
?.shows.filter((s) => s.dayParts.some((p) => p.danceGroupIds.length))
|
||||
.map((s) => s.name) ?? [];
|
||||
if (names.length)
|
||||
target = {
|
||||
...target,
|
||||
explanation: `${target.explanation} De planning kan ongeldig worden van: ${names.join(', ')}.`,
|
||||
};
|
||||
}
|
||||
this.deleting.set(target);
|
||||
}
|
||||
membershipChanged(ids: number[]): void {
|
||||
|
||||
Reference in New Issue
Block a user