some renaming of variables
All checks were successful
Build, test and publish / verify (push) Successful in 5m55s
All checks were successful
Build, test and publish / verify (push) Successful in 5m55s
This commit is contained in:
10
README.md
10
README.md
@@ -154,8 +154,8 @@ dancers; deleting a school also deletes its dancers, groups and shows.
|
|||||||
|
|
||||||
Shows have a name, zero to six dated dayparts (`MORNING`, `AFTERNOON`, `EVENING`),
|
Shows have a name, zero to six dated dayparts (`MORNING`, `AFTERNOON`, `EVENING`),
|
||||||
with unique date/type combinations. The displayed date range comes from these
|
with unique date/type combinations. The displayed date range comes from these
|
||||||
dayparts. Each show stores `preferredDayParts` (x), defaulting to two or one for
|
dayparts. Each show stores `preferredDayParts`, defaulting to two or one for
|
||||||
one daypart. Ordinary groups, including PRE_EDUCATION, perform in exactly x
|
one daypart. Ordinary groups, including PRE_EDUCATION, perform in exactly preferredDayParts
|
||||||
dayparts; preschool groups once; showcases every daypart. See
|
dayparts; preschool groups once; showcases every daypart. See
|
||||||
[show-scheduling-rules.md](show-scheduling-rules.md) for the full hard constraints,
|
[show-scheduling-rules.md](show-scheduling-rules.md) for the full hard constraints,
|
||||||
coach exceptions, ordering and optimization rules.
|
coach exceptions, ordering and optimization rules.
|
||||||
@@ -178,10 +178,10 @@ status. The UI supports ordering and moving performances, and displays violation
|
|||||||
the same body to link an existing coach. `DELETE .../coaches/{coachId}/dancer`
|
the same body to link an existing coach. `DELETE .../coaches/{coachId}/dancer`
|
||||||
removes the identity link. The dancers screen supports maintaining these links.
|
removes the identity link. The dancers screen supports maintaining these links.
|
||||||
|
|
||||||
School JSON exports use format version **3**, including x, schedule status and
|
School JSON exports use format version **3**, including preferredDayParts, schedule status and
|
||||||
coach-to-dancer links. Version 2 remains importable; its schedules migrate to
|
coach-to-dancer links. Version 2 remains importable; its schedules migrate to
|
||||||
drafts with default x. Version 1 is unsupported. Legacy `appearances` fields are
|
drafts with default preferredDayParts. Version 1 is unsupported. Legacy `appearances` fields are
|
||||||
retained for file compatibility; S18 scheduling uses x and fixed type exceptions.
|
retained for file compatibility; S18 scheduling uses preferredDayParts and fixed type exceptions.
|
||||||
The REST `appearances` input is deprecated; use `preferredDayParts`.
|
The REST `appearances` input is deprecated; use `preferredDayParts`.
|
||||||
|
|
||||||
Run the HTTP integration tests with:
|
Run the HTTP integration tests with:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public final class ScheduleRules {
|
|||||||
public static List<String> violations(final SchedulingInput input, final List<List<Long>> parts) {
|
public static List<String> violations(final SchedulingInput input, final List<List<Long>> parts) {
|
||||||
final List<String> errors = new ArrayList<>();
|
final List<String> errors = new ArrayList<>();
|
||||||
if (input.dayParts() == 0 || input.preferredDayParts() < 1 || input.preferredDayParts() > input.dayParts()) {
|
if (input.dayParts() == 0 || input.preferredDayParts() < 1 || input.preferredDayParts() > input.dayParts()) {
|
||||||
errors.add("Configure dayparts and x between 1 and the number of dayparts.");
|
errors.add("Configure dayparts and preferredDayParts between 1 and the number of dayparts.");
|
||||||
}
|
}
|
||||||
if (parts == null || parts.size() != input.dayParts() || parts.stream().anyMatch(java.util.Objects::isNull)) {
|
if (parts == null || parts.size() != input.dayParts() || parts.stream().anyMatch(java.util.Objects::isNull)) {
|
||||||
errors.add("Assignments must contain one list for every daypart.");
|
errors.add("Assignments must contain one list for every daypart.");
|
||||||
@@ -86,7 +86,7 @@ public final class ScheduleRules {
|
|||||||
|
|
||||||
private static void attendance(
|
private static void attendance(
|
||||||
final SchedulingInput input, final List<List<Long>> parts, final List<String> errors) {
|
final SchedulingInput input, final List<List<Long>> parts, final List<String> errors) {
|
||||||
final Set<Long> exempt = new HashSet<>(input.coachIds());
|
final Set<Long> exempt = new HashSet<>(input.coachDancerIds());
|
||||||
input.groups().stream()
|
input.groups().stream()
|
||||||
.filter(GroupRequirement::showcase)
|
.filter(GroupRequirement::showcase)
|
||||||
.forEach((final GroupRequirement g) -> exempt.addAll(g.dancerIds()));
|
.forEach((final GroupRequirement g) -> exempt.addAll(g.dancerIds()));
|
||||||
@@ -96,7 +96,8 @@ public final class ScheduleRules {
|
|||||||
}
|
}
|
||||||
attendance.forEach((final Long dancer, final Set<Integer> dayparts) -> {
|
attendance.forEach((final Long dancer, final Set<Integer> dayparts) -> {
|
||||||
if (!exempt.contains(dancer) && dayparts.size() > input.preferredDayParts()) {
|
if (!exempt.contains(dancer) && dayparts.size() > input.preferredDayParts()) {
|
||||||
errors.add("Dancer " + dancer + " exceeds x=" + input.preferredDayParts() + " dayparts.");
|
errors.add(
|
||||||
|
"Dancer " + dancer + " exceeds preferredDayParts=" + input.preferredDayParts() + " dayparts.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,21 @@ package nl.agilecc.agiledancers.model.scheduling;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable solver snapshot.
|
||||||
|
*
|
||||||
|
* @param coachDancerIds dancer registration IDs linked to coaches, used for participation exemptions
|
||||||
|
*/
|
||||||
public record SchedulingInput(
|
public record SchedulingInput(
|
||||||
long showId,
|
long showId,
|
||||||
long revision,
|
long revision,
|
||||||
int dayParts,
|
int dayParts,
|
||||||
List<GroupRequirement> groups,
|
List<GroupRequirement> groups,
|
||||||
int preferredDayParts,
|
int preferredDayParts,
|
||||||
Set<Long> coachIds) {
|
Set<Long> coachDancerIds) {
|
||||||
public SchedulingInput {
|
public SchedulingInput {
|
||||||
groups = List.copyOf(groups);
|
groups = List.copyOf(groups);
|
||||||
coachIds = Set.copyOf(coachIds);
|
coachDancerIds = Set.copyOf(coachDancerIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SchedulingInput(
|
public SchedulingInput(
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public final class SchedulingInputs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static SchedulingInput of(final DanceSchool school, final Show show, final long revision) {
|
public static SchedulingInput of(final DanceSchool school, final Show show, final long revision) {
|
||||||
final int x = preferred(show);
|
final int preferredDayParts = preferred(show);
|
||||||
final LocalDate date = show.getDayParts().stream()
|
final LocalDate date = show.getDayParts().stream()
|
||||||
.map(DayPart::getDate)
|
.map(DayPart::getDate)
|
||||||
.min(LocalDate::compareTo)
|
.min(LocalDate::compareTo)
|
||||||
@@ -40,7 +40,8 @@ public final class SchedulingInputs {
|
|||||||
.filter(java.util.Objects::nonNull)
|
.filter(java.util.Objects::nonNull)
|
||||||
.map(Dancer::getId)
|
.map(Dancer::getId)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
return new SchedulingInput(show.getId(), revision, show.getDayParts().size(), groups, x, coaches);
|
return new SchedulingInput(
|
||||||
|
show.getId(), revision, show.getDayParts().size(), groups, preferredDayParts, coaches);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GroupRequirement requirement(final DanceGroup group, final Show show, final LocalDate date) {
|
private static GroupRequirement requirement(final DanceGroup group, final Show show, final LocalDate date) {
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ public class SchoolManagementService {
|
|||||||
"Scheduling requires at least one school group and one daypart.");
|
"Scheduling requires at least one school group and one daypart.");
|
||||||
}
|
}
|
||||||
if (SchedulingInputs.preferred(show) > show.getDayParts().size()) {
|
if (SchedulingInputs.preferred(show) > show.getDayParts().size()) {
|
||||||
throw new ManagementException(ManagementExceptionKind.INVALID_INPUT, "Correct x before scheduling.");
|
throw new ManagementException(
|
||||||
|
ManagementExceptionKind.INVALID_INPUT, "Correct preferredDayParts before scheduling.");
|
||||||
}
|
}
|
||||||
running.add(showId);
|
running.add(showId);
|
||||||
return SchedulingInputs.of(school(schoolId), show, revisions.getOrDefault(showId, 0L));
|
return SchedulingInputs.of(school(schoolId), show, revisions.getOrDefault(showId, 0L));
|
||||||
|
|||||||
@@ -21,25 +21,26 @@ public class ShowSchedules {
|
|||||||
this.schools = schools;
|
this.schools = schools;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Retains assignments by date/type; removing dayparts may retain an invalid x in the draft. */
|
/** Retains assignments by date/type; removing dayparts may retain an invalid preferredDayParts in the draft. */
|
||||||
public Show configure(final long schoolId, final Long showId, final Show settings) {
|
public Show configure(final long schoolId, final Long showId, final Show settings) {
|
||||||
synchronized (schools) {
|
synchronized (schools) {
|
||||||
ShowConfiguration.validateParts(settings.getDayParts());
|
ShowConfiguration.validateParts(settings.getDayParts());
|
||||||
final Show existing = showId == null ? null : schools.show(schoolId, showId);
|
final Show existing = showId == null ? null : schools.show(schoolId, showId);
|
||||||
final int x = settings.getPreferredDayParts() == null
|
final int preferredDayParts = settings.getPreferredDayParts() == null
|
||||||
? (existing == null
|
? (existing == null
|
||||||
? (settings.getDayParts().size() == 1 ? 1 : 2)
|
? (settings.getDayParts().size() == 1 ? 1 : 2)
|
||||||
: SchedulingInputs.preferred(existing))
|
: SchedulingInputs.preferred(existing))
|
||||||
: settings.getPreferredDayParts();
|
: settings.getPreferredDayParts();
|
||||||
validateX(x, existing, settings.getDayParts().size());
|
validatePreferredDayParts(
|
||||||
|
preferredDayParts, existing, settings.getDayParts().size());
|
||||||
final Show show = existing == null
|
final Show show = existing == null
|
||||||
? schools.createShow(schoolId, settings.getName(), settings.getDayParts(), null)
|
? schools.createShow(schoolId, settings.getName(), settings.getDayParts(), null)
|
||||||
: existing;
|
: existing;
|
||||||
final boolean changed = x != SchedulingInputs.preferred(show)
|
final boolean changed = preferredDayParts != SchedulingInputs.preferred(show)
|
||||||
|| !keys(show.getDayParts()).equals(keys(settings.getDayParts()));
|
|| !keys(show.getDayParts()).equals(keys(settings.getDayParts()));
|
||||||
show.setDayParts(ShowConfiguration.retainAssignments(show, settings.getDayParts()));
|
show.setDayParts(ShowConfiguration.retainAssignments(show, settings.getDayParts()));
|
||||||
show.setName(settings.getName());
|
show.setName(settings.getName());
|
||||||
show.setPreferredDayParts(x);
|
show.setPreferredDayParts(preferredDayParts);
|
||||||
if (changed) {
|
if (changed) {
|
||||||
schools.touch(show);
|
schools.touch(show);
|
||||||
}
|
}
|
||||||
@@ -53,13 +54,16 @@ public class ShowSchedules {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void validateX(final int x, final Show existing, final int parts) {
|
private static void validatePreferredDayParts(final int preferredDayParts, final Show existing, final int parts) {
|
||||||
if (x < 1
|
if (preferredDayParts < 1
|
||||||
|| x > 6
|
|| preferredDayParts > 6
|
||||||
|| (parts > 0 && x > parts && (existing == null || x != SchedulingInputs.preferred(existing)))) {
|
|| (parts > 0
|
||||||
|
&& preferredDayParts > parts
|
||||||
|
&& (existing == null || preferredDayParts != SchedulingInputs.preferred(existing)))) {
|
||||||
throw new ManagementException(
|
throw new ManagementException(
|
||||||
ManagementExceptionKind.INVALID_INPUT,
|
ManagementExceptionKind.INVALID_INPUT,
|
||||||
"x must be between 1 and the number of dayparts. Retained x must be corrected before scheduling.");
|
"preferredDayParts must be between 1 and the number of dayparts. Retained preferredDayParts must"
|
||||||
|
+ " be corrected before scheduling.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -205,8 +205,9 @@ public record SchoolSnapshot(
|
|||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
ShowConfiguration.validateParts(parts);
|
ShowConfiguration.validateParts(parts);
|
||||||
final int x = show.preferredDayParts() == null ? (parts.size() == 1 ? 1 : 2) : show.preferredDayParts();
|
final int preferredDayParts =
|
||||||
check(x >= 1 && x <= 6, "x moet tussen 1 en 6 liggen");
|
show.preferredDayParts() == null ? (parts.size() == 1 ? 1 : 2) : show.preferredDayParts();
|
||||||
|
check(preferredDayParts >= 1 && preferredDayParts <= 6, "preferredDayParts moet tussen 1 en 6 liggen");
|
||||||
final Show entity = Show.builder()
|
final Show entity = Show.builder()
|
||||||
.name(show.name())
|
.name(show.name())
|
||||||
.dayParts(parts)
|
.dayParts(parts)
|
||||||
@@ -214,7 +215,7 @@ public record SchoolSnapshot(
|
|||||||
show.appearances() == null
|
show.appearances() == null
|
||||||
? ShowConfiguration.defaults(parts.size())
|
? ShowConfiguration.defaults(parts.size())
|
||||||
: Map.copyOf(show.appearances()))
|
: Map.copyOf(show.appearances()))
|
||||||
.preferredDayParts(x)
|
.preferredDayParts(preferredDayParts)
|
||||||
.finalized(false)
|
.finalized(false)
|
||||||
.build();
|
.build();
|
||||||
entity.setId((long) showIds.size());
|
entity.setId((long) showIds.size());
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public final class SchedulingObjectives {
|
|||||||
input.groups().stream()
|
input.groups().stream()
|
||||||
.filter(GroupRequirement::showcase)
|
.filter(GroupRequirement::showcase)
|
||||||
.forEach((final GroupRequirement g) -> showcases.addAll(g.dancerIds()));
|
.forEach((final GroupRequirement g) -> showcases.addAll(g.dancerIds()));
|
||||||
dancers.removeAll(input.coachIds());
|
dancers.removeAll(input.coachDancerIds());
|
||||||
final LinearExprBuilder penalty = LinearExpr.newBuilder();
|
final LinearExprBuilder penalty = LinearExpr.newBuilder();
|
||||||
for (final long dancer : dancers) {
|
for (final long dancer : dancers) {
|
||||||
final IntVar[] parts = dancerParts(model, input, vars, dancer);
|
final IntVar[] parts = dancerParts(model, input, vars, dancer);
|
||||||
|
|||||||
@@ -40,11 +40,11 @@ class S18LifecycleTest {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Show settings(final int n, final Integer x) {
|
private Show settings(final int n, final Integer preferredDayParts) {
|
||||||
return Show.builder()
|
return Show.builder()
|
||||||
.name("Show")
|
.name("Show")
|
||||||
.dayParts(parts(n))
|
.dayParts(parts(n))
|
||||||
.preferredDayParts(x)
|
.preferredDayParts(preferredDayParts)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ test('links a coach and finalizes performances spread across multiple dates', as
|
|||||||
data: { assignments: groupIds.map((id) => [id]) },
|
data: { assignments: groupIds.map((id) => [id]) },
|
||||||
});
|
});
|
||||||
await page.goto(`/schools/${school.id}/shows`);
|
await page.goto(`/schools/${school.id}/shows`);
|
||||||
await expect(page.getByText(/exceeds x=1/)).toBeVisible();
|
await expect(page.getByText(/exceeds preferredDayParts=1/)).toBeVisible();
|
||||||
await tab(page, 'Dansers');
|
await tab(page, 'Dansers');
|
||||||
await page.getByText('Coaches koppelen aan dansers', { exact: true }).click();
|
await page.getByText('Coaches koppelen aan dansers', { exact: true }).click();
|
||||||
await page.getByLabel('Danser als coach registreren').selectOption({ label: 'Dancing coach' });
|
await page.getByLabel('Danser als coach registreren').selectOption({ label: 'Dancing coach' });
|
||||||
|
|||||||
@@ -352,7 +352,8 @@
|
|||||||
<div class="show-details">
|
<div class="show-details">
|
||||||
<h3>{{ show.name }}</h3>
|
<h3>{{ show.name }}</h3>
|
||||||
<p>
|
<p>
|
||||||
{{ show.finalized ? 'Definitief' : 'Concept' }} · x = {{ show.preferredDayParts }}
|
{{ show.finalized ? 'Definitief' : 'Concept' }} · preferredDayParts =
|
||||||
|
{{ show.preferredDayParts }}
|
||||||
</p>
|
</p>
|
||||||
@for (violation of show.violations; track $index) {
|
@for (violation of show.violations; track $index) {
|
||||||
<p class="alert error">{{ violation }}</p>
|
<p class="alert error">{{ violation }}</p>
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ describe('EntityEditor', () => {
|
|||||||
appearances: { REGULAR: 2, SELECTION: 2, PRE_SCHOOLER: 1, SHOW_CASE: 2, PRE_EDUCATION: 2 },
|
appearances: { REGULAR: 2, SELECTION: 2, PRE_SCHOOLER: 1, SHOW_CASE: 2, PRE_EDUCATION: 2 },
|
||||||
};
|
};
|
||||||
|
|
||||||
it('defaults x to two and rejects duplicate dated dayparts and fractional x', () => {
|
it('defaults preferredDayParts to two and rejects duplicate dated dayparts and fractional preferredDayParts', () => {
|
||||||
const component = editor({ kind: 'show' });
|
const component = editor({ kind: 'show' });
|
||||||
component.name = 'Show';
|
component.name = 'Show';
|
||||||
expect(component.validDetails()).toBe(true);
|
expect(component.validDetails()).toBe(true);
|
||||||
@@ -99,7 +99,27 @@ describe('EntityEditor', () => {
|
|||||||
expect(component.validDetails()).toBe(false);
|
expect(component.validDetails()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('retains x when dayparts are removed so it can be explicitly corrected', () => {
|
it('preserves an explicitly chosen preferredDayParts when adding dayparts to a new show', async () => {
|
||||||
|
api.saveShow.mockReturnValue(of(show));
|
||||||
|
const component = editor({ kind: 'show' });
|
||||||
|
component.name = 'Show';
|
||||||
|
component.addPart();
|
||||||
|
component.dayParts[0].date = '2027-06-01';
|
||||||
|
component.addPart();
|
||||||
|
component.dayParts[1].date = '2027-06-02';
|
||||||
|
component.preferredDayParts = 1;
|
||||||
|
component.addPart();
|
||||||
|
component.dayParts[2].date = '2027-06-03';
|
||||||
|
expect(component.preferredDayParts).toBe(1);
|
||||||
|
await component.save();
|
||||||
|
expect(api.saveShow).toHaveBeenCalledWith(
|
||||||
|
7,
|
||||||
|
expect.objectContaining({ preferredDayParts: 1 }),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retains preferredDayParts when dayparts are removed so it can be explicitly corrected', () => {
|
||||||
const component = editor({ kind: 'show', value: show });
|
const component = editor({ kind: 'show', value: show });
|
||||||
component.addPart();
|
component.addPart();
|
||||||
component.dayParts[2].date = '2027-06-03';
|
component.dayParts[2].date = '2027-06-03';
|
||||||
@@ -144,7 +164,7 @@ describe('EntityEditor', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays one editable x field with a default of one for a one-daypart show', async () => {
|
it('displays one editable preferredDayParts field with a default of one for a one-daypart show', async () => {
|
||||||
// jsdom does not implement native dialog opening.
|
// jsdom does not implement native dialog opening.
|
||||||
const descriptor = Object.getOwnPropertyDescriptor(HTMLDialogElement.prototype, 'showModal');
|
const descriptor = Object.getOwnPropertyDescriptor(HTMLDialogElement.prototype, 'showModal');
|
||||||
Object.defineProperty(HTMLDialogElement.prototype, 'showModal', {
|
Object.defineProperty(HTMLDialogElement.prototype, 'showModal', {
|
||||||
|
|||||||
@@ -185,7 +185,16 @@ export class EntityEditor implements OnInit {
|
|||||||
readonly today = new Date().toLocaleDateString('en-CA');
|
readonly today = new Date().toLocaleDateString('en-CA');
|
||||||
private readonly api = inject(SchoolApi);
|
private readonly api = inject(SchoolApi);
|
||||||
dayParts: DayPart[] = [];
|
dayParts: DayPart[] = [];
|
||||||
preferredDayParts = 2;
|
private preferredDayPartsValue = 2;
|
||||||
|
private preferredDayPartsEdited = false;
|
||||||
|
|
||||||
|
get preferredDayParts(): number {
|
||||||
|
return this.preferredDayPartsValue;
|
||||||
|
}
|
||||||
|
set preferredDayParts(value: number) {
|
||||||
|
this.preferredDayPartsValue = value;
|
||||||
|
this.preferredDayPartsEdited = true;
|
||||||
|
}
|
||||||
name = '';
|
name = '';
|
||||||
date = '';
|
date = '';
|
||||||
type: GroupType = 'REGULAR';
|
type: GroupType = 'REGULAR';
|
||||||
@@ -218,7 +227,8 @@ export class EntityEditor implements OnInit {
|
|||||||
addPart(): void {
|
addPart(): void {
|
||||||
if (this.dayParts.length >= 6) return;
|
if (this.dayParts.length >= 6) return;
|
||||||
this.dayParts.push({ date: '', type: 'AFTERNOON', danceGroupIds: [] });
|
this.dayParts.push({ date: '', type: 'AFTERNOON', danceGroupIds: [] });
|
||||||
if (!this.target().value) this.preferredDayParts = Math.min(2, this.dayParts.length);
|
if (!this.target().value && !this.preferredDayPartsEdited)
|
||||||
|
this.preferredDayPartsValue = Math.min(2, this.dayParts.length);
|
||||||
}
|
}
|
||||||
removePart(index: number): void {
|
removePart(index: number): void {
|
||||||
this.dayParts.splice(index, 1);
|
this.dayParts.splice(index, 1);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { DialogFrame } from './dialog-frame';
|
|||||||
(dismissed)="closed.emit()"
|
(dismissed)="closed.emit()"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
{{ !dirty() && current()?.finalized ? 'Definitief' : 'Concept' }} · x =
|
{{ !dirty() && current()?.finalized ? 'Definitief' : 'Concept' }} · preferredDayParts =
|
||||||
{{ show().preferredDayParts }}
|
{{ show().preferredDayParts }}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Validated on 2026-11-03 against the confirmed S18 specification.
|
|||||||
Focused solver tests cover showcase bookends (including extra showcases), single
|
Focused solver tests cover showcase bookends (including extra showcases), single
|
||||||
and absent showcases, changing gaps, coach exemptions, infeasibility, the strict
|
and absent showcases, changing gaps, coach exemptions, infeasibility, the strict
|
||||||
priority of showcase attendance over balance, and style/age scores. Lifecycle
|
priority of showcase attendance over balance, and style/age scores. Lifecycle
|
||||||
and HTTP tests cover skipped empty groups, first-show-date ages, retained x after
|
and HTTP tests cover skipped empty groups, first-show-date ages, retained preferredDayParts after
|
||||||
daypart removal, atomic structural validation, invalid drafts, final status
|
daypart removal, atomic structural validation, invalid drafts, final status
|
||||||
invalidation, stale solver results, timeout preservation, coach identity links,
|
invalidation, stale solver results, timeout preservation, coach identity links,
|
||||||
and ordered JSON round trips including version-2 migration.
|
and ordered JSON round trips including version-2 migration.
|
||||||
@@ -36,7 +36,7 @@ assessed against `CODING_STANDARDS.md` and `docs/backend-review.md`:
|
|||||||
- `ScheduleRules` has cyclomatic signals for independent configuration checks
|
- `ScheduleRules` has cyclomatic signals for independent configuration checks
|
||||||
and showcase cases. Each rule family is isolated in a short method and shared
|
and showcase cases. Each rule family is isolated in a short method and shared
|
||||||
between finalization and solver-result validation.
|
between finalization and solver-result validation.
|
||||||
- `ShowSchedules` retains branching for nullable initial settings, retained x,
|
- `ShowSchedules` retains branching for nullable initial settings, retained preferredDayParts,
|
||||||
and structural draft validation. It stages complete replacement lists before
|
and structural draft validation. It stages complete replacement lists before
|
||||||
mutation and uses the existing aggregate lock. It separates schedule lifecycle
|
mutation and uses the existing aggregate lock. It separates schedule lifecycle
|
||||||
from the already broad `SchoolManagementService` aggregate owner.
|
from the already broad `SchoolManagementService` aggregate owner.
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ calendar dates. Counts do not restart on a new date.
|
|||||||
|
|
||||||
## Required performances
|
## Required performances
|
||||||
|
|
||||||
The manager chooses **x**, the desired number of distinct dayparts, per show.
|
The manager chooses **preferredDayParts**, the desired number of distinct dayparts, per show.
|
||||||
It defaults to two, or one for a show with only one daypart. A show can be saved
|
It defaults to two, or one for a show with only one daypart. A show can be saved
|
||||||
without dayparts but cannot be scheduled. If dayparts are removed, x is retained
|
without dayparts but cannot be scheduled. If dayparts are removed, preferredDayParts is retained
|
||||||
and must be corrected before scheduling when it exceeds the remaining count.
|
and must be corrected before scheduling when it exceeds the remaining count.
|
||||||
|
|
||||||
Every nonempty regular, selection and pre-education group performs in exactly x
|
Every nonempty regular, selection and pre-education group performs in exactly preferredDayParts
|
||||||
dayparts, once in each. Each preschool group performs exactly once in the whole
|
dayparts, once in each. Each preschool group performs exactly once in the whole
|
||||||
show. Every showcase performs once in every daypart. Groups without dancers are
|
show. Every showcase performs once in every daypart. Groups without dancers are
|
||||||
skipped completely, including for showcase positions and balancing.
|
skipped completely, including for showcase positions and balancing.
|
||||||
@@ -22,17 +22,17 @@ twice in the same daypart. Without showcases there is no opening/closing rule.
|
|||||||
|
|
||||||
## People and changing clothes
|
## People and changing clothes
|
||||||
|
|
||||||
An ordinary dancer must perform in no more than x distinct dayparts across all
|
An ordinary dancer must perform in no more than preferredDayParts distinct dayparts across all
|
||||||
of their groups. The same dancer registration must be used in every group.
|
of their groups. The same dancer registration must be used in every group.
|
||||||
|
|
||||||
Showcase participants necessarily perform in every daypart. For their other
|
Showcase participants necessarily perform in every daypart. For their other
|
||||||
performances, exceeding x dayparts is allowed but discouraged. We count their
|
performances, exceeding preferredDayParts dayparts is allowed but discouraged. We count their
|
||||||
non-showcase dayparts, subtract x, and count only positive excesses. The planner's
|
non-showcase dayparts, subtract preferredDayParts, and count only positive excesses. The planner's
|
||||||
first preference is to minimize the sum of these excesses across dancers.
|
first preference is to minimize the sum of these excesses across dancers.
|
||||||
|
|
||||||
A coach registration must be explicitly linked to the corresponding dancer;
|
A coach registration must be explicitly linked to the corresponding dancer;
|
||||||
names are not used to guess identity. Coaches are available throughout the show
|
names are not used to guess identity. Coaches are available throughout the show
|
||||||
and are exempt from both the x limit and the excess penalty. A coach only performs
|
and are exempt from both the preferredDayParts limit and the excess penalty. A coach only performs
|
||||||
with a group if included in its dancer membership. Being a group's coach alone
|
with a group if included in its dancer membership. Being a group's coach alone
|
||||||
never adds a performance or forces that group into extra dayparts.
|
never adds a performance or forces that group into extra dayparts.
|
||||||
|
|
||||||
@@ -85,5 +85,5 @@ Preferences alone never block finalization.
|
|||||||
Manual edits and relevant membership, dancer-age, coach-link or show-setting
|
Manual edits and relevant membership, dancer-age, coach-link or show-setting
|
||||||
changes return final schedules to draft. Existing schedules and imported schedules
|
changes return final schedules to draft. Existing schedules and imported schedules
|
||||||
are drafts. Import retains assignment order and recalculates violations. Export
|
are drafts. Import retains assignment order and recalculates violations. Export
|
||||||
is available for both statuses and records status, x and coach identity links;
|
is available for both statuses and records status, preferredDayParts and coach identity links;
|
||||||
reimport always requires a fresh finalization.
|
reimport always requires a fresh finalization.
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ See [validation evidence](../../docs/s18-validation.md) and
|
|||||||
|
|
||||||
- A show consists of dayparts and can span multiple dates, typically two days.
|
- A show consists of dayparts and can span multiple dates, typically two days.
|
||||||
All counts below apply across the entire show, not separately per date.
|
All counts below apply across the entire show, not separately per date.
|
||||||
- Each show has one configurable integer `x`, between 1 and the number of
|
- Each show has one configurable integer `preferredDayParts`, between 1 and the number of
|
||||||
dayparts inclusive. Its default is 2, or 1 for a one-daypart show.
|
dayparts inclusive. Its default is 2, or 1 for a one-daypart show.
|
||||||
- This replaces the existing configurable appearance counts per group type.
|
- This replaces the existing configurable appearance counts per group type.
|
||||||
- A show without dayparts can be stored but cannot be scheduled.
|
- A show without dayparts can be stored but cannot be scheduled.
|
||||||
- If reducing the number of dayparts makes `x` invalid, require the user to
|
- If reducing the number of dayparts makes `preferredDayParts` invalid, require the user to
|
||||||
correct it before scheduling again; do not silently change it.
|
correct it before scheduling again; do not silently change it.
|
||||||
- Groups without dancers are excluded from scheduling and from all performance
|
- Groups without dancers are excluded from scheduling and from all performance
|
||||||
counts and showcase-position requirements.
|
counts and showcase-position requirements.
|
||||||
@@ -28,9 +28,9 @@ See [validation evidence](../../docs/s18-validation.md) and
|
|||||||
## Required group appearances
|
## Required group appearances
|
||||||
|
|
||||||
- Each nonempty REGULAR, SELECTION and PRE_EDUCATION group performs in exactly
|
- Each nonempty REGULAR, SELECTION and PRE_EDUCATION group performs in exactly
|
||||||
`x` distinct dayparts, once within each selected daypart.
|
`preferredDayParts` distinct dayparts, once within each selected daypart.
|
||||||
- Each nonempty PRE_SCHOOLER group performs exactly once in the complete show,
|
- Each nonempty PRE_SCHOOLER group performs exactly once in the complete show,
|
||||||
regardless of `x`. At most two such groups are expected. They can be assigned
|
regardless of `preferredDayParts`. At most two such groups are expected. They can be assigned
|
||||||
freely to dayparts; no additional placement restriction applies.
|
freely to dayparts; no additional placement restriction applies.
|
||||||
- Each nonempty SHOW_CASE group performs exactly once in every daypart.
|
- Each nonempty SHOW_CASE group performs exactly once in every daypart.
|
||||||
- With at least two showcase groups, each daypart starts with one showcase and
|
- With at least two showcase groups, each daypart starts with one showcase and
|
||||||
@@ -42,12 +42,12 @@ See [validation evidence](../../docs/s18-validation.md) and
|
|||||||
|
|
||||||
## Dancer participation and coaches
|
## Dancer participation and coaches
|
||||||
|
|
||||||
- A dancer without a showcase membership performs in at most `x` distinct
|
- A dancer without a showcase membership performs in at most `preferredDayParts` distinct
|
||||||
dayparts across the complete show. This is a hard constraint.
|
dayparts across the complete show. This is a hard constraint.
|
||||||
- A showcase participant necessarily performs in all dayparts. Their other
|
- A showcase participant necessarily performs in all dayparts. Their other
|
||||||
group performances may span more than `x` dayparts, but this is undesirable.
|
group performances may span more than `preferredDayParts` dayparts, but this is undesirable.
|
||||||
- For each non-coach showcase participant, count the distinct dayparts in which
|
- For each non-coach showcase participant, count the distinct dayparts in which
|
||||||
they perform outside showcases. Their excess is `max(0, count - x)`.
|
they perform outside showcases. Their excess is `max(0, count - preferredDayParts)`.
|
||||||
Minimize the sum of these excesses as the first optimization priority.
|
Minimize the sum of these excesses as the first optimization priority.
|
||||||
- A person dancing in several groups must use the same dancer registration in
|
- A person dancing in several groups must use the same dancer registration in
|
||||||
every group, including when accompanying preschoolers. Up to five group
|
every group, including when accompanying preschoolers. Up to five group
|
||||||
|
|||||||
Reference in New Issue
Block a user