S18 Added OR Tools rules for scheduling show
All checks were successful
Build, test and publish / verify (push) Successful in 6m12s
All checks were successful
Build, test and publish / verify (push) Successful in 6m12s
This commit is contained in:
@@ -151,6 +151,7 @@
|
||||
</section>
|
||||
}
|
||||
@case ('dancers') {
|
||||
<app-coach-editor [data]="context" (changed)="reload()" />
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>Dansers</h2>
|
||||
@@ -350,6 +351,15 @@
|
||||
</div>
|
||||
<div class="show-details">
|
||||
<h3>{{ show.name }}</h3>
|
||||
<p>
|
||||
{{ show.finalized ? 'Definitief' : 'Concept' }} · x = {{ show.preferredDayParts }}
|
||||
</p>
|
||||
@for (violation of show.violations; track $index) {
|
||||
<p class="alert error">{{ violation }}</p>
|
||||
}
|
||||
<button type="button" class="button secondary" (click)="scheduleEditing.set(show)">
|
||||
Indeling en volgorde bewerken
|
||||
</button>
|
||||
<p>{{ dateRange(show) }}</p>
|
||||
@for (part of show.dayParts; track $index) {
|
||||
<p>{{ part.date }} · {{ part.type }}: {{ groupNamesFor(part.danceGroupIds) }}</p>
|
||||
@@ -357,7 +367,7 @@
|
||||
<p role="status">
|
||||
{{
|
||||
scheduleValid(show)
|
||||
? 'Planning voldoet aan alle optredensaantallen.'
|
||||
? 'Planning voldoet aan alle harde regels.'
|
||||
: 'Geen geldige planning. Plan de groepen opnieuw in.'
|
||||
}}
|
||||
</p>
|
||||
@@ -365,7 +375,10 @@
|
||||
type="button"
|
||||
class="button primary"
|
||||
[disabled]="
|
||||
scheduling().includes(show.id) || !context.groups.length || !show.dayParts.length
|
||||
scheduling().includes(show.id) ||
|
||||
!context.groups.length ||
|
||||
!show.dayParts.length ||
|
||||
show.preferredDayParts > show.dayParts.length
|
||||
"
|
||||
(click)="schedule(show)"
|
||||
>
|
||||
@@ -426,6 +439,15 @@
|
||||
(saved)="saved()"
|
||||
/>
|
||||
}
|
||||
@if (scheduleEditing(); as show) {
|
||||
<app-schedule-editor
|
||||
[show]="show"
|
||||
[groups]="context.groups"
|
||||
[schoolId]="context.school.id"
|
||||
(closed)="scheduleEditing.set(null)"
|
||||
(saved)="reload()"
|
||||
/>
|
||||
}
|
||||
@if (members(); as group) {
|
||||
<app-group-members
|
||||
[group]="group"
|
||||
|
||||
@@ -17,6 +17,8 @@ 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 { ScheduleEditor } from '../shared/schedule-editor';
|
||||
import { CoachEditor } from '../shared/coach-editor';
|
||||
import { GroupMembers } from '../shared/group-members';
|
||||
|
||||
interface DeleteTarget {
|
||||
@@ -29,6 +31,8 @@ interface DeleteTarget {
|
||||
@Component({
|
||||
selector: 'app-school-page',
|
||||
imports: [
|
||||
ScheduleEditor,
|
||||
CoachEditor,
|
||||
SchoolTransfer,
|
||||
DatePipe,
|
||||
RouterLink,
|
||||
@@ -50,6 +54,7 @@ export class SchoolPage implements OnInit {
|
||||
readonly error = signal('');
|
||||
readonly notice = signal('');
|
||||
readonly editor = signal<EditorTarget | null>(null);
|
||||
readonly scheduleEditing = signal<Show | null>(null);
|
||||
readonly members = signal<DanceGroup | null>(null);
|
||||
readonly deleting = signal<DeleteTarget | null>(null);
|
||||
readonly scheduling = signal<number[]>([]);
|
||||
@@ -86,6 +91,7 @@ export class SchoolPage implements OnInit {
|
||||
this.search.set('');
|
||||
this.editor.set(null);
|
||||
this.members.set(null);
|
||||
this.scheduleEditing.set(null);
|
||||
this.deleting.set(null);
|
||||
this.section.set(params.get('section') ?? 'overview');
|
||||
return this.api.schoolData(Number(params.get('schoolId'))).pipe(
|
||||
@@ -121,22 +127,9 @@ export class SchoolPage implements OnInit {
|
||||
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)),
|
||||
)
|
||||
);
|
||||
return show.violations?.length === 0;
|
||||
}
|
||||
|
||||
groupNamesFor(ids: number[]): string {
|
||||
return (
|
||||
ids.map((id) => this.data()?.groups.find((g) => g.id === id)?.category ?? '?').join(', ') ||
|
||||
@@ -163,7 +156,9 @@ export class SchoolPage implements OnInit {
|
||||
try {
|
||||
const result = await firstValueFrom(this.api.schedule(schoolId, show.id));
|
||||
this.notice.set(
|
||||
result.optimal ? 'Optimale balans.' : 'Geldige planning; optimale balans niet bewezen.',
|
||||
result.optimal
|
||||
? 'Optimaal conceptrooster.'
|
||||
: 'Geldig conceptrooster; optimaliteit niet bewezen.',
|
||||
);
|
||||
await this.reload();
|
||||
} catch (error: unknown) {
|
||||
@@ -221,6 +216,7 @@ export class SchoolPage implements OnInit {
|
||||
this.deleting.set(target);
|
||||
}
|
||||
membershipChanged(ids: number[]): void {
|
||||
void this.reload();
|
||||
const groupId = this.members()?.id;
|
||||
this.data.update((data) =>
|
||||
data
|
||||
|
||||
Reference in New Issue
Block a user