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:
178
agiledancers-fe/src/app/shared/schedule-editor.ts
Normal file
178
agiledancers-fe/src/app/shared/schedule-editor.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
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' }} · x =
|
||||
{{ 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 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.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.api.saveAssignments(this.schoolId(), this.show().id, this.assignments),
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user