diff --git a/README.md b/README.md index fcb5ef4..71cceb3 100644 --- a/README.md +++ b/README.md @@ -38,12 +38,10 @@ When creating a schedule, the user selects: * The start date * The target completion date -Attainly then distributes the selected elements across the available days. -When there are fewer assignments than days, the user can spread them across -the full range or pack them into consecutive days at the start, middle, or -end. When an uneven schedule has more than one assignment per day, the user -can spread the heavier days across the full range or keep them together at -the start, middle, or end. +Attainly then distributes the selected elements evenly across the available +days. When an uneven schedule has more than one assignment per day, the user +can place the consecutive heavier days at the start, middle, or end of the +schedule. For example, a user could choose to schedule: diff --git a/backend/app/Schedule/EvenDistributionScheduler.php b/backend/app/Schedule/EvenDistributionScheduler.php index 3cdb919..e19caf9 100644 --- a/backend/app/Schedule/EvenDistributionScheduler.php +++ b/backend/app/Schedule/EvenDistributionScheduler.php @@ -18,7 +18,15 @@ class EvenDistributionScheduler $differenceInDays = $startDate->diff($targetDate)->days; $dayCount = $differenceInDays + 1; - return $this->distributedDates( + if ($assignmentCount < $dayCount) { + return $this->sparseDates( + assignmentCount: $assignmentCount, + dayCount: $dayCount, + startDate: $startDate, + ); + } + + return $this->dailyDates( assignmentCount: $assignmentCount, dayCount: $dayCount, startDate: $startDate, @@ -29,26 +37,53 @@ class EvenDistributionScheduler /** * @return list */ - private function distributedDates( + private function sparseDates( + int $assignmentCount, + int $dayCount, + DateTimeImmutable $startDate, + ): array { + if ($assignmentCount === 1) { + return [$startDate]; + } + + $dates = []; + for ($assignmentIndex = 0; + $assignmentIndex < $assignmentCount; + $assignmentIndex++ + ) { + $scaledIndex = $assignmentIndex * ($dayCount - 1) + / ($assignmentCount - 1); + $dayIndex = (int) floor($scaledIndex + 0.5); + $dates[] = $startDate->modify("+{$dayIndex} days"); + } + + return $dates; + } + + /** + * @return list + */ + private function dailyDates( int $assignmentCount, int $dayCount, DateTimeImmutable $startDate, WorkloadPlacement $workloadPlacement, ): array { $assignmentsPerDay = intdiv($assignmentCount, $dayCount); - $remainderDayCount = $assignmentCount % $dayCount; - $remainderDayIndexes = $this->remainderDayIndexes( - assignmentsPerDay: $assignmentsPerDay, + $heavierDayCount = $assignmentCount % $dayCount; + $heavierBlockStart = $this->heavierBlockStart( dayCount: $dayCount, - remainderDayCount: $remainderDayCount, + heavierDayCount: $heavierDayCount, workloadPlacement: $workloadPlacement, ); - $remainderDays = array_fill_keys($remainderDayIndexes, true); $dates = []; for ($dayIndex = 0; $dayIndex < $dayCount; $dayIndex++) { $assignmentCountForDay = $assignmentsPerDay; - if (isset($remainderDays[$dayIndex])) { + if ( + $dayIndex >= $heavierBlockStart + && $dayIndex < $heavierBlockStart + $heavierDayCount + ) { $assignmentCountForDay++; } @@ -61,68 +96,9 @@ class EvenDistributionScheduler return $dates; } - /** - * @return list - */ - private function remainderDayIndexes( - int $assignmentsPerDay, + private function heavierBlockStart( int $dayCount, - int $remainderDayCount, - WorkloadPlacement $workloadPlacement, - ): array { - if ($remainderDayCount === 0) { - return []; - } - - if ($workloadPlacement === WorkloadPlacement::Spread) { - return $this->spreadRemainderDayIndexes( - assignmentsPerDay: $assignmentsPerDay, - dayCount: $dayCount, - remainderDayCount: $remainderDayCount, - ); - } - - $blockStart = $this->remainderBlockStart( - dayCount: $dayCount, - remainderDayCount: $remainderDayCount, - workloadPlacement: $workloadPlacement, - ); - - return range($blockStart, $blockStart + $remainderDayCount - 1); - } - - /** - * @return list - */ - private function spreadRemainderDayIndexes( - int $assignmentsPerDay, - int $dayCount, - int $remainderDayCount, - ): array { - if ($remainderDayCount === 1) { - if ($assignmentsPerDay === 0) { - return [0]; - } - - return [intdiv($dayCount - 1, 2)]; - } - - $dayIndexes = []; - for ($remainderIndex = 0; - $remainderIndex < $remainderDayCount; - $remainderIndex++ - ) { - $scaledIndex = $remainderIndex * ($dayCount - 1) - / ($remainderDayCount - 1); - $dayIndexes[] = (int) floor($scaledIndex + 0.5); - } - - return $dayIndexes; - } - - private function remainderBlockStart( - int $dayCount, - int $remainderDayCount, + int $heavierDayCount, WorkloadPlacement $workloadPlacement, ): int { if ($workloadPlacement === WorkloadPlacement::Start) { @@ -130,9 +106,9 @@ class EvenDistributionScheduler } if ($workloadPlacement === WorkloadPlacement::End) { - return $dayCount - $remainderDayCount; + return $dayCount - $heavierDayCount; } - return intdiv($dayCount - $remainderDayCount, 2); + return intdiv($dayCount - $heavierDayCount, 2); } } diff --git a/backend/app/Schedule/UseCases/CreateSchedule/CreateSchedule.php b/backend/app/Schedule/UseCases/CreateSchedule/CreateSchedule.php index fe9392a..7c02c9c 100644 --- a/backend/app/Schedule/UseCases/CreateSchedule/CreateSchedule.php +++ b/backend/app/Schedule/UseCases/CreateSchedule/CreateSchedule.php @@ -64,7 +64,7 @@ class CreateSchedule 'targetDate must not be before startDate', ); } - $requestedWorkloadPlacement = $this->workloadPlacement( + $workloadPlacement = $this->workloadPlacement( $request->workloadPlacement, ); @@ -78,13 +78,6 @@ class CreateSchedule throw new BadRequestException('level has no elements'); } - $workloadPlacement = $this->resolvedWorkloadPlacement( - requestedWorkloadPlacement: $requestedWorkloadPlacement, - assignmentCount: count($elements), - startDate: $startDate, - targetDate: $targetDate, - ); - $scheduledDates = $this->evenDistributionScheduler->scheduledDates( assignmentCount: count($elements), startDate: $startDate, @@ -232,38 +225,19 @@ class CreateSchedule /** * @throws BadRequestException */ - private function workloadPlacement(?string $value): ?WorkloadPlacement + private function workloadPlacement(?string $value): WorkloadPlacement { if ($value === null) { - return null; + return WorkloadPlacement::Middle; } $workloadPlacement = WorkloadPlacement::tryFrom($value); if ($workloadPlacement === null) { throw new BadRequestException( - 'workloadPlacement must be spread, start, middle, or end', + 'workloadPlacement must be start, middle, or end', ); } return $workloadPlacement; } - - private function resolvedWorkloadPlacement( - ?WorkloadPlacement $requestedWorkloadPlacement, - int $assignmentCount, - DateTimeImmutable $startDate, - DateTimeImmutable $targetDate, - ): WorkloadPlacement { - $differenceInDays = $startDate->diff($targetDate)->days; - $dayCount = $differenceInDays + 1; - $isSparse = $assignmentCount < $dayCount; - - if ($requestedWorkloadPlacement === null) { - return $isSparse - ? WorkloadPlacement::Spread - : WorkloadPlacement::Middle; - } - - return $requestedWorkloadPlacement; - } } diff --git a/backend/app/Schedule/WorkloadPlacement.php b/backend/app/Schedule/WorkloadPlacement.php index 3960874..607f4f6 100644 --- a/backend/app/Schedule/WorkloadPlacement.php +++ b/backend/app/Schedule/WorkloadPlacement.php @@ -4,7 +4,6 @@ namespace App\Schedule; enum WorkloadPlacement: string { - case Spread = 'spread'; case Start = 'start'; case Middle = 'middle'; case End = 'end'; diff --git a/backend/tests/Feature/Schedule/ScheduleEndpointTest.php b/backend/tests/Feature/Schedule/ScheduleEndpointTest.php index ef98f8c..5fed467 100644 --- a/backend/tests/Feature/Schedule/ScheduleEndpointTest.php +++ b/backend/tests/Feature/Schedule/ScheduleEndpointTest.php @@ -196,40 +196,6 @@ class ScheduleEndpointTest extends TestCase $this->assertDatabaseCount('schedules', 1); } - public function test_it_packs_sparse_assignments_at_the_requested_end(): void - { - $user = $this->createUser('reader@example.com'); - $set = $this->createSet($user, 'Course'); - $lessonLevel = $this->createLevel($set, 'lesson'); - $repository = app(ElementRepository::class); - - foreach (range(1, 4) as $number) { - $repository->create(new CreateElementDto( - name: "Lesson {$number}", - level: $lessonLevel, - parentElement: null, - )); - } - $this->createSession($user, 'valid-token'); - - $response = $this->credentialedPost('/api/schedules', [ - 'setId' => $set->getId(), - 'levelId' => $lessonLevel->getId(), - 'startDate' => '2026-08-10', - 'targetDate' => '2026-08-15', - 'workloadPlacement' => 'end', - ])->assertCreated(); - - $days = $response->json('schedule.days'); - $this->assertIsArray($days); - $this->assertSame( - [0, 0, 1, 1, 1, 1], - array_map(function (array $day): int { - return count($day['assignments']); - }, $days), - ); - } - public function test_it_rejects_an_unknown_workload_placement(): void { $user = $this->createUser('reader@example.com'); @@ -249,44 +215,10 @@ class ScheduleEndpointTest extends TestCase 'targetDate' => '2026-08-14', 'workloadPlacement' => 'sideways', ])->assertBadRequest()->assertExactJson([ - 'error' => 'workloadPlacement must be spread, start, middle, or end', + 'error' => 'workloadPlacement must be start, middle, or end', ]); } - public function test_it_spreads_heavier_days_across_the_full_range(): void - { - $user = $this->createUser('reader@example.com'); - $set = $this->createSet($user, 'Course'); - $lessonLevel = $this->createLevel($set, 'lesson'); - $repository = app(ElementRepository::class); - - foreach (range(1, 13) as $number) { - $repository->create(new CreateElementDto( - name: "Lesson {$number}", - level: $lessonLevel, - parentElement: null, - )); - } - $this->createSession($user, 'valid-token'); - - $response = $this->credentialedPost('/api/schedules', [ - 'setId' => $set->getId(), - 'levelId' => $lessonLevel->getId(), - 'startDate' => '2026-08-10', - 'targetDate' => '2026-08-14', - 'workloadPlacement' => 'spread', - ])->assertCreated(); - - $days = $response->json('schedule.days'); - $this->assertIsArray($days); - $this->assertSame( - [3, 2, 3, 2, 3], - array_map(function (array $day): int { - return count($day['assignments']); - }, $days), - ); - } - public function test_it_lists_only_the_users_schedules_newest_first(): void { $user = $this->createUser('reader@example.com'); diff --git a/backend/tests/Unit/Schedule/EvenDistributionSchedulerTest.php b/backend/tests/Unit/Schedule/EvenDistributionSchedulerTest.php index 8459252..f9e63a7 100644 --- a/backend/tests/Unit/Schedule/EvenDistributionSchedulerTest.php +++ b/backend/tests/Unit/Schedule/EvenDistributionSchedulerTest.php @@ -19,8 +19,7 @@ class EvenDistributionSchedulerTest extends TestCase WorkloadPlacement::End->value => [2, 2, 2, 2, 3], ]; - foreach ($expectedCounts as $placementValue => $expectedDailyCounts) { - $placement = WorkloadPlacement::from($placementValue); + foreach (WorkloadPlacement::cases() as $placement) { $dates = $scheduler->scheduledDates( assignmentCount: 11, startDate: $this->utc('2026-08-10'), @@ -29,7 +28,7 @@ class EvenDistributionSchedulerTest extends TestCase ); $this->assertSame( - $expectedDailyCounts, + $expectedCounts[$placement->value], $this->dailyCounts($dates), ); } @@ -44,8 +43,7 @@ class EvenDistributionSchedulerTest extends TestCase WorkloadPlacement::End->value => [2, 2, 2, 3, 3], ]; - foreach ($expectedCounts as $placementValue => $expectedDailyCounts) { - $placement = WorkloadPlacement::from($placementValue); + foreach (WorkloadPlacement::cases() as $placement) { $dates = $scheduler->scheduledDates( assignmentCount: 12, startDate: $this->utc('2026-08-10'), @@ -54,101 +52,25 @@ class EvenDistributionSchedulerTest extends TestCase ); $this->assertSame( - $expectedDailyCounts, + $expectedCounts[$placement->value], $this->dailyCounts($dates), ); } } - public function test_it_spreads_heavier_days_across_the_full_range(): void + public function test_it_preserves_sparse_distribution_across_the_full_range(): void { - $scheduler = new EvenDistributionScheduler; - $expectedCounts = [ - 11 => [2, 2, 3, 2, 2], - 12 => [3, 2, 2, 2, 3], - 13 => [3, 2, 3, 2, 3], - ]; + $dates = (new EvenDistributionScheduler)->scheduledDates( + assignmentCount: 3, + startDate: $this->utc('2026-08-10'), + targetDate: $this->utc('2026-08-16'), + workloadPlacement: WorkloadPlacement::Middle, + ); - foreach ($expectedCounts as $assignmentCount => $dailyCounts) { - $dates = $scheduler->scheduledDates( - assignmentCount: $assignmentCount, - startDate: $this->utc('2026-08-10'), - targetDate: $this->utc('2026-08-14'), - workloadPlacement: WorkloadPlacement::Spread, - ); - - $this->assertSame($dailyCounts, $this->dailyCounts($dates)); - } - } - - public function test_it_places_sparse_assignments_at_the_requested_position(): void - { - $scheduler = new EvenDistributionScheduler; - $expectedDates = [ - WorkloadPlacement::Spread->value => [ - '2026-08-10', - '2026-08-12', - '2026-08-13', - '2026-08-15', - ], - WorkloadPlacement::Start->value => [ - '2026-08-10', - '2026-08-11', - '2026-08-12', - '2026-08-13', - ], - WorkloadPlacement::Middle->value => [ - '2026-08-11', - '2026-08-12', - '2026-08-13', - '2026-08-14', - ], - WorkloadPlacement::End->value => [ - '2026-08-12', - '2026-08-13', - '2026-08-14', - '2026-08-15', - ], - ]; - - foreach (WorkloadPlacement::cases() as $placement) { - $dates = $scheduler->scheduledDates( - assignmentCount: 4, - startDate: $this->utc('2026-08-10'), - targetDate: $this->utc('2026-08-15'), - workloadPlacement: $placement, - ); - - $this->assertSame( - $expectedDates[$placement->value], - $this->formattedDates($dates), - ); - } - } - - public function test_it_places_one_assignment_at_the_requested_position(): void - { - $scheduler = new EvenDistributionScheduler; - $expectedDates = [ - WorkloadPlacement::Spread->value => '2026-08-10', - WorkloadPlacement::Start->value => '2026-08-10', - WorkloadPlacement::Middle->value => '2026-08-12', - WorkloadPlacement::End->value => '2026-08-15', - ]; - - foreach (WorkloadPlacement::cases() as $placement) { - $dates = $scheduler->scheduledDates( - assignmentCount: 1, - startDate: $this->utc('2026-08-10'), - targetDate: $this->utc('2026-08-15'), - workloadPlacement: $placement, - ); - - $this->assertSame( - [$expectedDates[$placement->value]], - $this->formattedDates($dates), - ); - } + $this->assertSame( + ['2026-08-10', '2026-08-13', '2026-08-16'], + $this->formattedDates($dates), + ); } public function test_it_distributes_even_and_single_day_schedules(): void diff --git a/backend/tests/Unit/Schedule/UseCases/CreateScheduleTest.php b/backend/tests/Unit/Schedule/UseCases/CreateScheduleTest.php index 0ade3e1..47cee8d 100644 --- a/backend/tests/Unit/Schedule/UseCases/CreateScheduleTest.php +++ b/backend/tests/Unit/Schedule/UseCases/CreateScheduleTest.php @@ -156,7 +156,7 @@ class CreateScheduleTest extends TestCase ); } - public function test_it_defaults_sparse_work_to_spread_across_the_full_range(): void + public function test_it_spreads_sparse_work_across_the_full_range(): void { $user = $this->user(); $setRepository = new FakeSetRepository; @@ -192,7 +192,7 @@ class CreateScheduleTest extends TestCase levelId: $lessonLevel->getId(), startDate: '2026-08-10', targetDate: '2026-08-16', - workloadPlacement: null, + workloadPlacement: 'middle', )); $this->assertSame( @@ -203,7 +203,7 @@ class CreateScheduleTest extends TestCase ); } - public function test_it_places_one_element_at_the_requested_middle(): void + public function test_it_places_one_element_on_the_start_date(): void { $user = $this->user(); $setRepository = new FakeSetRepository; @@ -240,7 +240,7 @@ class CreateScheduleTest extends TestCase )); $this->assertSame( - '2024-12-31', + '2020-01-01', $schedule->getAssignments()[0] ->getScheduledDate() ->format('Y-m-d'), @@ -324,7 +324,7 @@ class CreateScheduleTest extends TestCase $this->expectException(BadRequestException::class); $this->expectExceptionMessage( - 'workloadPlacement must be spread, start, middle, or end', + 'workloadPlacement must be start, middle, or end', ); (new CreateSchedule( diff --git a/frontend/website/cypress/e2e/set-scheduling.cy.ts b/frontend/website/cypress/e2e/set-scheduling.cy.ts index b799db0..25a5b4a 100644 --- a/frontend/website/cypress/e2e/set-scheduling.cy.ts +++ b/frontend/website/cypress/e2e/set-scheduling.cy.ts @@ -67,13 +67,6 @@ const overloadedBibleLayout = { ), } -const sparseBibleLayout = { - ...bibleLayout, - levels: bibleLayout.levels.map((level) => - level.id === 3 ? { ...level, elementCount: 4 } : level, - ), -} - const scheduleDetail = { schedule: { id: 73, @@ -208,7 +201,7 @@ describe('set scheduling', () => { levelId: 3, startDate: '2026-08-10', targetDate: '2026-08-12', - workloadPlacement: 'spread', + workloadPlacement: 'middle', }) request.reply({ statusCode: 201, body: scheduleDetail }) }).as('createSchedule') @@ -259,7 +252,7 @@ describe('set scheduling', () => { ) }) - it('spreads heavier days where the user chooses', () => { + it('places heavier days where the user chooses', () => { cy.intercept('GET', '**/api/sets/41', { statusCode: 200, body: overloadedBibleLayout, @@ -270,7 +263,7 @@ describe('set scheduling', () => { levelId: 3, startDate: '2026-08-10', targetDate: '2026-08-14', - workloadPlacement: 'spread', + workloadPlacement: 'end', }) request.reply({ statusCode: 201, body: scheduleDetail }) }).as('createSchedule') @@ -286,44 +279,7 @@ describe('set scheduling', () => { cy.get('[data-workload-placement]').should('be.visible').within(() => { cy.contains('legend', 'Heavier days').should('be.visible') - cy.contains('Spread evenly').should('be.visible') cy.get('input[value="middle"]').should('be.checked') - cy.get('input[value="spread"]').check() - }) - cy.get('form').submit() - cy.wait('@createSchedule') - }) - - it('packs sparse assignment days where the user chooses', () => { - cy.intercept('GET', '**/api/sets/41', { - statusCode: 200, - body: sparseBibleLayout, - }).as('layout') - cy.intercept('POST', '**/api/schedules', (request) => { - expect(request.body).to.deep.equal({ - setId: 41, - levelId: 3, - startDate: '2026-08-10', - targetDate: '2026-08-15', - workloadPlacement: 'end', - }) - request.reply({ statusCode: 201, body: scheduleDetail }) - }).as('createSchedule') - - cy.visit('/sets/41/schedules/new') - cy.wait('@me') - cy.wait('@layout') - cy.get('#schedule-level').select('3') - cy.get('#schedule-start-date').type('2026-08-10') - cy.get('#schedule-target-date').type('2026-08-15') - - cy.get('[data-workload-placement]').should('be.visible').within(() => { - cy.contains('legend', 'Assignment days').should('be.visible') - cy.contains('Spread evenly').should('be.visible') - cy.contains('At the start').should('be.visible') - cy.contains('In the middle').should('be.visible') - cy.contains('At the end').should('be.visible') - cy.get('input[value="spread"]').should('be.checked') cy.get('input[value="end"]').check() }) cy.get('form').submit() diff --git a/frontend/website/src/stores/schedules.ts b/frontend/website/src/stores/schedules.ts index c6c3fde..3715d0e 100644 --- a/frontend/website/src/stores/schedules.ts +++ b/frontend/website/src/stores/schedules.ts @@ -6,7 +6,7 @@ import { API_BASE } from '@/utils/apiBase' const isoDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/) -export const workloadPlacementSchema = z.enum(['spread', 'start', 'middle', 'end']) +export const workloadPlacementSchema = z.enum(['start', 'middle', 'end']) export const scheduleSummarySchema = z.object({ id: z.number().int().positive(), diff --git a/frontend/website/src/views/CreateScheduleView.vue b/frontend/website/src/views/CreateScheduleView.vue index 91fdb29..912268d 100644 --- a/frontend/website/src/views/CreateScheduleView.vue +++ b/frontend/website/src/views/CreateScheduleView.vue @@ -14,7 +14,6 @@ import { useSetLayoutStore } from '@/stores/setLayout' type ScheduleField = 'levelId' | 'startDate' | 'targetDate' type ScheduleFieldErrors = Partial> -type PlacementContext = 'unavailable' | 'sparse' | 'heavy' | 'balanced' const route = useRoute() const router = useRouter() @@ -27,9 +26,8 @@ const form = reactive({ levelId: null as number | null, startDate: '', targetDate: '', + workloadPlacement: 'middle' as WorkloadPlacement, }) -const sparseWorkloadPlacement = ref('spread') -const heavyWorkloadPlacement = ref('middle') const fieldErrors = ref({}) const levels = computed(() => @@ -40,61 +38,31 @@ const workloadPlacementOptions: Array<{ value: WorkloadPlacement label: string }> = [ - { value: 'spread', label: 'Spread evenly' }, { value: 'start', label: 'At the start' }, { value: 'middle', label: 'In the middle' }, { value: 'end', label: 'At the end' }, ] -const placementContext = computed(() => { +const showWorkloadPlacement = computed(() => { if ( selectedLevel.value === undefined || form.startDate === '' || form.targetDate === '' || form.targetDate < form.startDate ) { - return 'unavailable' + return false } const startTime = Date.parse(`${form.startDate}T00:00:00Z`) const targetTime = Date.parse(`${form.targetDate}T00:00:00Z`) if (Number.isNaN(startTime) || Number.isNaN(targetTime)) { - return 'unavailable' + return false } const millisecondsPerDay = 24 * 60 * 60 * 1000 const dayCount = Math.round((targetTime - startTime) / millisecondsPerDay) + 1 const assignmentCount = selectedLevel.value.elementCount - if (assignmentCount < dayCount) { - return 'sparse' - } - - if (assignmentCount > dayCount && assignmentCount % dayCount !== 0) { - return 'heavy' - } - - return 'balanced' -}) -const showWorkloadPlacement = computed( - () => placementContext.value === 'sparse' || placementContext.value === 'heavy', -) -const selectedWorkloadPlacement = computed({ - get() { - if (placementContext.value === 'sparse') { - return sparseWorkloadPlacement.value - } - - return heavyWorkloadPlacement.value - }, - set(value) { - if (placementContext.value === 'sparse') { - sparseWorkloadPlacement.value = value - - return - } - - heavyWorkloadPlacement.value = value - }, + return assignmentCount > dayCount && assignmentCount % dayCount !== 0 }) const scheduleFormSchema = z @@ -130,10 +98,7 @@ watch( async function submit(): Promise { fieldErrors.value = {} - const result = scheduleFormSchema.safeParse({ - ...form, - workloadPlacement: selectedWorkloadPlacement.value, - }) + const result = scheduleFormSchema.safeParse(form) if (!result.success) { const errors: ScheduleFieldErrors = {} for (const issue of result.error.issues) { @@ -272,15 +237,9 @@ async function retry(): Promise { data-workload-placement :disabled="creating" > - - {{ placementContext === 'sparse' ? 'Assignment days' : 'Heavier days' }} - -

- Spread assignments across the schedule or keep them on consecutive days. -

-

- Spread heavier days across the schedule or keep them together at the start, middle, or - end. + Heavier days +

+ Some days need one extra assignment. Choose where those days appear in the schedule.