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:
70
agiledancers-fe/src/app/shared/coach-editor.ts
Normal file
70
agiledancers-fe/src/app/shared/coach-editor.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Component, inject, input, output, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { SchoolData } from '../core/api-models';
|
||||
import { SchoolApi } from '../core/school-api';
|
||||
import { apiError } from '../core/api-error';
|
||||
|
||||
@Component({
|
||||
selector: 'app-coach-editor',
|
||||
imports: [FormsModule],
|
||||
template: `
|
||||
<details>
|
||||
<summary>Coaches koppelen aan dansers</summary>
|
||||
<p>
|
||||
Coaches mogen in alle dagdelen dansen. Voeg ze als danser toe aan elke groep waarin ze
|
||||
meedansen. Ook voor coaches blijven twee andere optredens tussen hun dansen verplicht.
|
||||
</p>
|
||||
@if (error()) {
|
||||
<p role="alert" class="alert error">{{ error() }}</p>
|
||||
}
|
||||
<fieldset [disabled]="busy()">
|
||||
@for (coach of data().coaches; track coach.id) {
|
||||
<label
|
||||
>{{ coach.name }}
|
||||
<select [ngModel]="coach.dancerId" (ngModelChange)="link($event, coach.id)">
|
||||
<option [ngValue]="null">Geen danser gekoppeld</option>
|
||||
@for (dancer of data().dancers; track dancer.id) {
|
||||
<option [ngValue]="dancer.id">{{ dancer.name }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
}
|
||||
<label
|
||||
>Danser als coach registreren
|
||||
<select [ngModel]="null" (ngModelChange)="link($event)">
|
||||
<option [ngValue]="null">Kies een danser</option>
|
||||
@for (dancer of data().dancers; track dancer.id) {
|
||||
<option [ngValue]="dancer.id">{{ dancer.name }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
</fieldset>
|
||||
</details>
|
||||
`,
|
||||
})
|
||||
export class CoachEditor {
|
||||
readonly data = input.required<SchoolData>();
|
||||
readonly changed = output<void>();
|
||||
readonly busy = signal(false);
|
||||
readonly error = signal('');
|
||||
private readonly api = inject(SchoolApi);
|
||||
|
||||
async link(dancerId: number | null, coachId?: number): Promise<void> {
|
||||
if (this.busy() || (dancerId === null && coachId === undefined)) return;
|
||||
this.busy.set(true);
|
||||
this.error.set('');
|
||||
try {
|
||||
await firstValueFrom<unknown>(
|
||||
dancerId === null
|
||||
? this.api.unlinkCoach(this.data().school.id, coachId!)
|
||||
: this.api.linkCoach(this.data().school.id, dancerId, coachId),
|
||||
);
|
||||
this.changed.emit();
|
||||
} catch (error: unknown) {
|
||||
this.error.set(apiError(error));
|
||||
} finally {
|
||||
this.busy.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,9 @@ describe('EntityEditor', () => {
|
||||
const show: Show = {
|
||||
id: 3,
|
||||
name: 'Summer',
|
||||
preferredDayParts: 2,
|
||||
finalized: false,
|
||||
violations: [],
|
||||
dayParts: [
|
||||
{ date: '2027-06-01', type: 'AFTERNOON', danceGroupIds: [4] },
|
||||
{ date: '2027-06-02', type: 'AFTERNOON', danceGroupIds: [4] },
|
||||
@@ -77,7 +80,7 @@ describe('EntityEditor', () => {
|
||||
appearances: { REGULAR: 2, SELECTION: 2, PRE_SCHOOLER: 1, SHOW_CASE: 2, PRE_EDUCATION: 2 },
|
||||
};
|
||||
|
||||
it('initializes defaults for every type and rejects duplicate dated dayparts and fractional counts', () => {
|
||||
it('defaults x to two and rejects duplicate dated dayparts and fractional x', () => {
|
||||
const component = editor({ kind: 'show' });
|
||||
component.name = 'Show';
|
||||
expect(component.validDetails()).toBe(true);
|
||||
@@ -87,31 +90,25 @@ describe('EntityEditor', () => {
|
||||
component.dayParts[1].date = '2027-06-02';
|
||||
component.addPart();
|
||||
component.dayParts[2].date = '2027-06-03';
|
||||
expect(component.appearances).toEqual({
|
||||
REGULAR: 2,
|
||||
SELECTION: 2,
|
||||
PRE_SCHOOLER: 1,
|
||||
SHOW_CASE: 3,
|
||||
PRE_EDUCATION: 3,
|
||||
});
|
||||
expect(component.preferredDayParts).toBe(2);
|
||||
expect(component.validDetails()).toBe(true);
|
||||
component.dayParts[2].date = '2027-06-02';
|
||||
expect(component.validDetails()).toBe(false);
|
||||
component.dayParts[2].date = '2027-06-03';
|
||||
component.appearances.PRE_SCHOOLER = 1.5;
|
||||
component.preferredDayParts = 1.5;
|
||||
expect(component.validDetails()).toBe(false);
|
||||
});
|
||||
|
||||
it('preserves saved counts and requires explicit correction when reducing to one daypart', () => {
|
||||
it('retains x when dayparts are removed so it can be explicitly corrected', () => {
|
||||
const component = editor({ kind: 'show', value: show });
|
||||
component.addPart();
|
||||
component.dayParts[2].date = '2027-06-03';
|
||||
expect(component.appearances.SHOW_CASE).toBe(2);
|
||||
expect(component.preferredDayParts).toBe(2);
|
||||
component.removePart(2);
|
||||
component.removePart(1);
|
||||
expect(component.appearances.REGULAR).toBe(2);
|
||||
expect(component.validDetails()).toBe(false);
|
||||
component.setSingleCounts();
|
||||
expect(component.preferredDayParts).toBe(2);
|
||||
expect(component.validDetails()).toBe(true);
|
||||
component.preferredDayParts = 1;
|
||||
expect(component.validDetails()).toBe(true);
|
||||
});
|
||||
|
||||
@@ -120,11 +117,11 @@ describe('EntityEditor', () => {
|
||||
const confirm = vi.spyOn(window, 'confirm').mockReturnValue(false);
|
||||
try {
|
||||
const component = editor({ kind: 'show', value: show });
|
||||
component.appearances.REGULAR = 1;
|
||||
component.preferredDayParts = 1;
|
||||
await component.save();
|
||||
expect(confirm).toHaveBeenCalledWith(expect.stringContaining('Summer'));
|
||||
expect(api.saveShow).not.toHaveBeenCalled();
|
||||
component.appearances.REGULAR = 2;
|
||||
component.preferredDayParts = 2;
|
||||
confirm.mockClear();
|
||||
component.name = 'Renamed';
|
||||
await component.save();
|
||||
@@ -147,7 +144,7 @@ describe('EntityEditor', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('displays one disabled appearance field for every type with one daypart', async () => {
|
||||
it('displays one editable x field with a default of one for a one-daypart show', async () => {
|
||||
// jsdom does not implement native dialog opening.
|
||||
const descriptor = Object.getOwnPropertyDescriptor(HTMLDialogElement.prototype, 'showModal');
|
||||
Object.defineProperty(HTMLDialogElement.prototype, 'showModal', {
|
||||
@@ -163,8 +160,8 @@ describe('EntityEditor', () => {
|
||||
fixture.detectChanges();
|
||||
const element = fixture.nativeElement as HTMLElement;
|
||||
const inputs = Array.from(element.querySelectorAll<HTMLInputElement>('input[type="number"]'));
|
||||
expect(inputs).toHaveLength(5);
|
||||
expect(inputs.every((input) => input.disabled && input.value === '1')).toBe(true);
|
||||
expect(inputs).toHaveLength(1);
|
||||
expect(inputs.every((input) => !input.disabled && input.value === '1')).toBe(true);
|
||||
} finally {
|
||||
if (descriptor) Object.defineProperty(HTMLDialogElement.prototype, 'showModal', descriptor);
|
||||
else Reflect.deleteProperty(HTMLDialogElement.prototype, 'showModal');
|
||||
|
||||
@@ -130,33 +130,22 @@ import { DialogFrame } from './dialog-frame';
|
||||
</button>
|
||||
</fieldset>
|
||||
@if (dayParts.length) {
|
||||
<fieldset>
|
||||
<legend>Optredens per groepstype</legend>
|
||||
@for (option of types; track option.value) {
|
||||
<label
|
||||
>{{ option.label
|
||||
}}<input
|
||||
type="number"
|
||||
[name]="'count' + option.value"
|
||||
[ngModel]="dayParts.length === 1 ? 1 : appearances[option.value]"
|
||||
(ngModelChange)="appearances[option.value] = $event"
|
||||
required
|
||||
min="1"
|
||||
[max]="dayParts.length"
|
||||
step="1"
|
||||
[disabled]="dayParts.length === 1"
|
||||
/></label>
|
||||
}
|
||||
<p class="form-hint">
|
||||
Bestaande aantallen blijven behouden bij wijzigingen. Corrigeer te hoge aantallen
|
||||
vóór opslaan.
|
||||
</p>
|
||||
@if (dayParts.length === 1 && !validCounts()) {
|
||||
<button type="button" class="button secondary" (click)="setSingleCounts()">
|
||||
Alle aantallen op één zetten
|
||||
</button>
|
||||
}
|
||||
</fieldset>
|
||||
<label
|
||||
>Gewenst aantal dagdelen (x)
|
||||
<input
|
||||
type="number"
|
||||
name="preferredDayParts"
|
||||
[(ngModel)]="preferredDayParts"
|
||||
min="1"
|
||||
max="6"
|
||||
step="1"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<p class="form-hint">
|
||||
Gewone groepen dansen in precies x dagdelen. Kleuters eenmaal; showcases ieder
|
||||
dagdeel. Is x groter dan het aantal dagdelen, corrigeer x vóór inplannen.
|
||||
</p>
|
||||
} @else {
|
||||
<p class="form-hint">Zonder dagdelen blijft deze show ongepland.</p>
|
||||
}
|
||||
@@ -196,8 +185,7 @@ export class EntityEditor implements OnInit {
|
||||
readonly today = new Date().toLocaleDateString('en-CA');
|
||||
private readonly api = inject(SchoolApi);
|
||||
dayParts: DayPart[] = [];
|
||||
appearances: Partial<Record<GroupType, number>> = {};
|
||||
private countsInitialized = false;
|
||||
preferredDayParts = 2;
|
||||
name = '';
|
||||
date = '';
|
||||
type: GroupType = 'REGULAR';
|
||||
@@ -223,44 +211,23 @@ export class EntityEditor implements OnInit {
|
||||
if (target.kind === 'dancer') this.date = target.value.birthDate;
|
||||
if (target.kind === 'show') {
|
||||
this.dayParts = target.value.dayParts.map((p) => ({ ...p }));
|
||||
this.appearances = { ...target.value.appearances };
|
||||
this.countsInitialized = Object.keys(this.appearances).length > 0;
|
||||
this.preferredDayParts = target.value.preferredDayParts;
|
||||
}
|
||||
}
|
||||
}
|
||||
addPart(): void {
|
||||
if (this.dayParts.length >= 6) return;
|
||||
const previous = this.dayParts.length;
|
||||
this.dayParts.push({ date: '', type: 'AFTERNOON', danceGroupIds: [] });
|
||||
if (!this.countsInitialized) {
|
||||
for (const { value } of this.types) {
|
||||
const defaultCount = (n: number) =>
|
||||
value === 'PRE_SCHOOLER'
|
||||
? 1
|
||||
: value === 'SHOW_CASE' || value === 'PRE_EDUCATION'
|
||||
? n
|
||||
: Math.min(2, n);
|
||||
if (
|
||||
this.appearances[value] === undefined ||
|
||||
this.appearances[value] === defaultCount(previous)
|
||||
)
|
||||
this.appearances[value] = defaultCount(this.dayParts.length);
|
||||
}
|
||||
}
|
||||
if (!this.target().value) this.preferredDayParts = Math.min(2, this.dayParts.length);
|
||||
}
|
||||
removePart(index: number): void {
|
||||
this.dayParts.splice(index, 1);
|
||||
if (!this.countsInitialized && !this.dayParts.length) this.appearances = {};
|
||||
}
|
||||
setSingleCounts(): void {
|
||||
for (const option of this.types) this.appearances[option.value] = 1;
|
||||
}
|
||||
validCounts(): boolean {
|
||||
return this.types.every(
|
||||
(o) =>
|
||||
Number.isInteger(this.appearances[o.value]) &&
|
||||
this.appearances[o.value]! >= 1 &&
|
||||
this.appearances[o.value]! <= this.dayParts.length,
|
||||
return (
|
||||
Number.isInteger(this.preferredDayParts) &&
|
||||
this.preferredDayParts >= 1 &&
|
||||
this.preferredDayParts <= 6
|
||||
);
|
||||
}
|
||||
toggleStyle(style: GroupStyle, checked: boolean): void {
|
||||
@@ -300,7 +267,7 @@ export class EntityEditor implements OnInit {
|
||||
current.value &&
|
||||
(JSON.stringify(current.value.dayParts.map((p) => ({ date: p.date, type: p.type }))) !==
|
||||
JSON.stringify(this.dayParts.map((p) => ({ date: p.date, type: p.type }))) ||
|
||||
JSON.stringify(current.value.appearances) !== JSON.stringify(this.appearances))
|
||||
current.value.preferredDayParts !== this.preferredDayParts)
|
||||
)
|
||||
affected = [current.value];
|
||||
const names = affected
|
||||
@@ -349,7 +316,7 @@ export class EntityEditor implements OnInit {
|
||||
{
|
||||
name: this.name.trim(),
|
||||
dayParts: this.dayParts.map((p) => ({ date: p.date, type: p.type })),
|
||||
appearances: this.appearances,
|
||||
preferredDayParts: this.preferredDayParts,
|
||||
},
|
||||
id,
|
||||
);
|
||||
|
||||
88
agiledancers-fe/src/app/shared/schedule-editor.spec.ts
Normal file
88
agiledancers-fe/src/app/shared/schedule-editor.spec.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Show, DanceGroup } from '../core/api-models';
|
||||
import { SchoolApi } from '../core/school-api';
|
||||
import { ScheduleEditor } from './schedule-editor';
|
||||
|
||||
const groups: DanceGroup[] = [1, 2, 3].map((id) => ({
|
||||
id,
|
||||
category: `Group ${id}`,
|
||||
type: 'REGULAR',
|
||||
startAge: 0,
|
||||
endAge: 99,
|
||||
styles: [],
|
||||
dancerIds: [id],
|
||||
}));
|
||||
const show: Show = {
|
||||
id: 4,
|
||||
name: 'Show',
|
||||
preferredDayParts: 1,
|
||||
finalized: true,
|
||||
violations: [],
|
||||
appearances: {},
|
||||
dayParts: [
|
||||
{ date: '2027-01-01', type: 'MORNING', danceGroupIds: [1, 2] },
|
||||
{ date: '2027-01-02', type: 'MORNING', danceGroupIds: [3] },
|
||||
],
|
||||
};
|
||||
|
||||
describe('ScheduleEditor', () => {
|
||||
const api = { saveAssignments: vi.fn(), finalize: vi.fn() };
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
TestBed.configureTestingModule({ providers: [{ provide: SchoolApi, useValue: api }] });
|
||||
});
|
||||
function editor() {
|
||||
const fixture = TestBed.createComponent(ScheduleEditor);
|
||||
fixture.componentRef.setInput('show', show);
|
||||
fixture.componentRef.setInput('groups', groups);
|
||||
fixture.componentRef.setInput('schoolId', 7);
|
||||
fixture.componentInstance.ngOnInit();
|
||||
return fixture.componentInstance;
|
||||
}
|
||||
it('edits order and dayparts without changing the saved input', () => {
|
||||
const component = editor();
|
||||
component.reorder(0, 1, -1);
|
||||
component.move(0, 0, 1);
|
||||
component.add(0, 3);
|
||||
component.remove(1, 0);
|
||||
expect(component.assignments).toEqual([[1, 3], [2]]);
|
||||
expect(show.dayParts[0].danceGroupIds).toEqual([1, 2]);
|
||||
expect(component.dirty()).toBe(true);
|
||||
});
|
||||
it('saves invalid drafts and displays server violations without finalizing', async () => {
|
||||
const component = editor();
|
||||
component.remove(0, 0);
|
||||
const draft = { ...show, finalized: false, violations: ['Group 1 requires 1 dayparts.'] };
|
||||
api.saveAssignments.mockReturnValue(of(draft));
|
||||
await component.save();
|
||||
expect(api.saveAssignments).toHaveBeenCalledWith(7, 4, [[2], [3]]);
|
||||
expect(component.current()).toEqual(draft);
|
||||
expect(component.dirty()).toBe(false);
|
||||
expect(api.finalize).not.toHaveBeenCalled();
|
||||
});
|
||||
it('keeps unsaved edits when saving fails', async () => {
|
||||
const component = editor();
|
||||
component.remove(0, 0);
|
||||
api.saveAssignments.mockReturnValue(throwError(() => new HttpErrorResponse({ status: 0 })));
|
||||
await component.save();
|
||||
expect(component.assignments).toEqual([[2], [3]]);
|
||||
expect(component.dirty()).toBe(true);
|
||||
expect(component.error()).toContain('niet bereikbaar');
|
||||
expect(component.busy()).toBe(false);
|
||||
});
|
||||
it('finalizes through the server and reports a stale validation failure', async () => {
|
||||
const component = editor();
|
||||
api.finalize.mockReturnValue(of(show));
|
||||
await component.finalize();
|
||||
expect(component.current()?.finalized).toBe(true);
|
||||
api.finalize.mockReturnValue(
|
||||
throwError(
|
||||
() => new HttpErrorResponse({ status: 400, error: { message: 'Changing gap violated' } }),
|
||||
),
|
||||
);
|
||||
await component.finalize();
|
||||
expect(component.error()).toContain('Changing gap violated');
|
||||
});
|
||||
});
|
||||
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