From 7bc7ad207bc6291ceaf8067a53f9ff06eefa66df Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Tue, 18 Aug 2026 00:13:24 +0300 Subject: [PATCH 1/2] test overdue dashboard assignments --- .../Feature/Schedule/ScheduleEndpointTest.php | 37 ++++++++++--------- .../UseCases/ListAssignmentsForDateTest.php | 12 +++--- .../cypress/e2e/today-assignments.cy.ts | 19 ++++++++-- 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/backend/tests/Feature/Schedule/ScheduleEndpointTest.php b/backend/tests/Feature/Schedule/ScheduleEndpointTest.php index b68e2ad..295a245 100644 --- a/backend/tests/Feature/Schedule/ScheduleEndpointTest.php +++ b/backend/tests/Feature/Schedule/ScheduleEndpointTest.php @@ -225,7 +225,7 @@ class ScheduleEndpointTest extends TestCase ]); } - public function test_it_lists_the_users_assignments_for_a_date(): void + public function test_it_lists_the_users_assignments_due_through_a_date(): void { $user = $this->createUser('reader@example.com'); $otherUser = $this->createUser('other@example.com'); @@ -255,8 +255,8 @@ class ScheduleEndpointTest extends TestCase $this->credentialedPost('/api/schedules', [ 'setId' => $olderSet->getId(), 'levelId' => $olderLevel->getId(), - 'startDate' => '2026-08-15', - 'targetDate' => '2026-08-15', + 'startDate' => '2026-08-14', + 'targetDate' => '2026-08-14', ])->assertCreated(); $this->credentialedPost('/api/schedules', [ 'setId' => $newerSet->getId(), @@ -286,22 +286,9 @@ class ScheduleEndpointTest extends TestCase ->assertExactJson([ 'date' => '2026-08-15', 'assignments' => [ - [ - 'id' => 3, - 'schedule' => [ - 'id' => 2, - 'set' => [ - 'name' => 'Newer course', - ], - ], - 'element' => [ - 'name' => 'Only chapter', - 'kind' => 'chapter', - 'path' => ['Only chapter'], - ], - ], [ 'id' => 1, + 'scheduledDate' => '2026-08-14', 'schedule' => [ 'id' => 1, 'set' => [ @@ -316,6 +303,7 @@ class ScheduleEndpointTest extends TestCase ], [ 'id' => 2, + 'scheduledDate' => '2026-08-14', 'schedule' => [ 'id' => 1, 'set' => [ @@ -328,6 +316,21 @@ class ScheduleEndpointTest extends TestCase 'path' => ['Second lesson'], ], ], + [ + 'id' => 3, + 'scheduledDate' => '2026-08-15', + 'schedule' => [ + 'id' => 2, + 'set' => [ + 'name' => 'Newer course', + ], + ], + 'element' => [ + 'name' => 'Only chapter', + 'kind' => 'chapter', + 'path' => ['Only chapter'], + ], + ], ], ]); } diff --git a/backend/tests/Unit/Schedule/UseCases/ListAssignmentsForDateTest.php b/backend/tests/Unit/Schedule/UseCases/ListAssignmentsForDateTest.php index c6bc95f..4fca1fc 100644 --- a/backend/tests/Unit/Schedule/UseCases/ListAssignmentsForDateTest.php +++ b/backend/tests/Unit/Schedule/UseCases/ListAssignmentsForDateTest.php @@ -17,7 +17,7 @@ use Tests\Fakes\FakeScheduleRepository; class ListAssignmentsForDateTest extends TestCase { - public function test_it_lists_the_users_assignments_for_the_date(): void + public function test_it_lists_the_users_assignments_due_through_the_date(): void { $user = $this->user(1, 'reader@example.com'); $otherUser = $this->user(2, 'other@example.com'); @@ -25,7 +25,7 @@ class ListAssignmentsForDateTest extends TestCase $olderSchedule = $repository->create($this->schedule( user: $user, setName: 'Older plan', - date: '2026-08-15', + date: '2026-08-14', assignmentNames: ['First', 'Second'], )); $repository->create($this->schedule( @@ -37,7 +37,7 @@ class ListAssignmentsForDateTest extends TestCase $repository->create($this->schedule( user: $otherUser, setName: 'Private plan', - date: '2026-08-15', + date: '2026-08-13', assignmentNames: ['Hidden'], )); $repository->create($this->schedule( @@ -61,18 +61,18 @@ class ListAssignmentsForDateTest extends TestCase ); $this->assertSame( - ['Newer plan', 'Older plan'], + ['Older plan', 'Newer plan'], array_map(function ($assignment): string { return $assignment->getSetName(); }, $assignments), ); $this->assertSame( - ['Third', 'Second'], + ['Second', 'Third'], array_map(function ($assignment): string { return $assignment->getAssignment()->getName(); }, $assignments), ); - $this->assertSame(2, $assignments[0]->getScheduleId()); + $this->assertSame(1, $assignments[0]->getScheduleId()); } public function test_it_rejects_a_missing_date(): void diff --git a/frontend/website/cypress/e2e/today-assignments.cy.ts b/frontend/website/cypress/e2e/today-assignments.cy.ts index 77d2ba5..553c57c 100644 --- a/frontend/website/cypress/e2e/today-assignments.cy.ts +++ b/frontend/website/cypress/e2e/today-assignments.cy.ts @@ -36,6 +36,7 @@ describe("today's assignments", () => { assignments: [ { id: 12, + scheduledDate: '2026-08-13', schedule: { id: 73, set: { name: 'Bible' }, @@ -48,6 +49,7 @@ describe("today's assignments", () => { }, { id: 13, + scheduledDate: browserToday, schedule: { id: 81, set: { name: 'Course' }, @@ -77,6 +79,16 @@ describe("today's assignments", () => { 'have.length', 2, ) + cy.get('[data-today-assignment]') + .first() + .should('have.attr', 'data-today-assignment', '12') + cy.get('[data-today-assignment="12"] .today-assignment__overdue').should( + 'have.text', + 'Overdue · Due Aug 13, 2026', + ) + cy.get('[data-today-assignment="13"] .today-assignment__overdue').should( + 'not.exist', + ) cy.contains('a', 'Genesis / Creation / Chapter 1') .should('contain.text', 'Bible') .and('have.attr', 'href', '/schedules/73') @@ -109,7 +121,7 @@ describe("today's assignments", () => { cy.get('.today-assignments [role="status"]').should( 'contain.text', - 'Nothing is assigned for today.', + 'Nothing is due today.', ) }) @@ -121,6 +133,7 @@ describe("today's assignments", () => { assignments: [ { id: 12, + scheduledDate: browserToday, schedule: { id: 73, set: { name: 'Bible' }, @@ -177,7 +190,7 @@ describe("today's assignments", () => { cy.get('[data-today-assignment="12"]').should('not.exist') cy.get('.today-assignments [role="status"]').should( 'contain.text', - 'Nothing is assigned for today.', + 'Nothing is due today.', ) }) @@ -219,7 +232,7 @@ describe("today's assignments", () => { cy.get('.today-assignments [role="status"]').should( 'contain.text', - 'Nothing is assigned for today.', + 'Nothing is due today.', ) }) }) From 93d2f4ebd660e0139ddb3467847eca4bedc3e5bd Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Tue, 18 Aug 2026 00:16:57 +0300 Subject: [PATCH 2/2] show overdue assignments today --- .../Http/Controllers/ScheduleController.php | 2 ++ .../Schedule/EloquentScheduleRepository.php | 11 +++--- backend/app/Schedule/ScheduleRepository.php | 2 +- .../ListAssignmentsForDate.php | 9 ++--- .../tests/Fakes/FakeScheduleRepository.php | 36 +++++++++++++++++-- frontend/website/src/stores/schedules.ts | 1 + frontend/website/src/views/DashboardView.vue | 29 ++++++++++++++- 7 files changed, 77 insertions(+), 13 deletions(-) diff --git a/backend/app/Http/Controllers/ScheduleController.php b/backend/app/Http/Controllers/ScheduleController.php index f80461a..33a6316 100644 --- a/backend/app/Http/Controllers/ScheduleController.php +++ b/backend/app/Http/Controllers/ScheduleController.php @@ -249,6 +249,8 @@ class ScheduleController extends Controller return [ 'id' => $assignment->getId(), + 'scheduledDate' => $assignment->getScheduledDate() + ->format('Y-m-d'), 'schedule' => [ 'id' => $assignmentForDate->getScheduleId(), 'set' => [ diff --git a/backend/app/Schedule/EloquentScheduleRepository.php b/backend/app/Schedule/EloquentScheduleRepository.php index 214e8a5..8576591 100644 --- a/backend/app/Schedule/EloquentScheduleRepository.php +++ b/backend/app/Schedule/EloquentScheduleRepository.php @@ -63,7 +63,7 @@ class EloquentScheduleRepository implements ScheduleRepository return $schedules; } - public function findAssignmentsForUserOnDate( + public function findIncompleteAssignmentsForUserDueOnOrBefore( User $user, DateTimeImmutable $date, ): array { @@ -76,11 +76,14 @@ class EloquentScheduleRepository implements ScheduleRepository 'schedule_assignments.schedule_id', ) ->where('schedules.user_id', $user->getId()) - ->where('schedule_assignments.scheduled_date', $date->format( - 'Y-m-d', - )) + ->where( + 'schedule_assignments.scheduled_date', + '<=', + $date->format('Y-m-d'), + ) ->whereNull('schedule_assignments.completed_at') ->with('schedule') + ->orderBy('schedule_assignments.scheduled_date') ->orderByDesc('schedules.id') ->orderBy('schedule_assignments.position') ->orderBy('schedule_assignments.id') diff --git a/backend/app/Schedule/ScheduleRepository.php b/backend/app/Schedule/ScheduleRepository.php index 3222311..20b5f9f 100644 --- a/backend/app/Schedule/ScheduleRepository.php +++ b/backend/app/Schedule/ScheduleRepository.php @@ -19,7 +19,7 @@ interface ScheduleRepository /** * @return list */ - public function findAssignmentsForUserOnDate( + public function findIncompleteAssignmentsForUserDueOnOrBefore( User $user, DateTimeImmutable $date, ): array; diff --git a/backend/app/Schedule/UseCases/ListAssignmentsForDate/ListAssignmentsForDate.php b/backend/app/Schedule/UseCases/ListAssignmentsForDate/ListAssignmentsForDate.php index 9e3017d..23f388d 100644 --- a/backend/app/Schedule/UseCases/ListAssignmentsForDate/ListAssignmentsForDate.php +++ b/backend/app/Schedule/UseCases/ListAssignmentsForDate/ListAssignmentsForDate.php @@ -22,10 +22,11 @@ class ListAssignmentsForDate { $date = $this->parseDate($request->date); - return $this->scheduleRepository->findAssignmentsForUserOnDate( - $request->user, - $date, - ); + return $this->scheduleRepository + ->findIncompleteAssignmentsForUserDueOnOrBefore( + $request->user, + $date, + ); } /** diff --git a/backend/tests/Fakes/FakeScheduleRepository.php b/backend/tests/Fakes/FakeScheduleRepository.php index e2f35bc..e12d126 100644 --- a/backend/tests/Fakes/FakeScheduleRepository.php +++ b/backend/tests/Fakes/FakeScheduleRepository.php @@ -78,7 +78,7 @@ class FakeScheduleRepository implements ScheduleRepository }, array_values($schedules)); } - public function findAssignmentsForUserOnDate( + public function findIncompleteAssignmentsForUserDueOnOrBefore( User $user, DateTimeImmutable $date, ): array { @@ -86,8 +86,7 @@ class FakeScheduleRepository implements ScheduleRepository foreach ($this->findAllForUser($user) as $schedule) { foreach ($schedule->getAssignments() as $assignment) { - if ($assignment->getScheduledDate()->format('Y-m-d') - !== $date->format('Y-m-d') + if ($assignment->getScheduledDate() > $date || $assignment->getCompletedAt() !== null ) { continue; @@ -101,6 +100,37 @@ class FakeScheduleRepository implements ScheduleRepository } } + usort( + $assignments, + function ( + AssignmentForDate $first, + AssignmentForDate $second, + ): int { + $firstAssignment = $first->getAssignment(); + $secondAssignment = $second->getAssignment(); + $dateComparison = $firstAssignment->getScheduledDate() + <=> $secondAssignment->getScheduledDate(); + if ($dateComparison !== 0) { + return $dateComparison; + } + + $scheduleComparison = $second->getScheduleId() + <=> $first->getScheduleId(); + if ($scheduleComparison !== 0) { + return $scheduleComparison; + } + + $positionComparison = $firstAssignment->getPosition() + <=> $secondAssignment->getPosition(); + if ($positionComparison !== 0) { + return $positionComparison; + } + + return $firstAssignment->getId() + <=> $secondAssignment->getId(); + }, + ); + return $assignments; } diff --git a/frontend/website/src/stores/schedules.ts b/frontend/website/src/stores/schedules.ts index 2471d66..825bbfb 100644 --- a/frontend/website/src/stores/schedules.ts +++ b/frontend/website/src/stores/schedules.ts @@ -31,6 +31,7 @@ const scheduleAssignmentSchema = assignmentIdentitySchema.extend({ }) export const assignmentForDateSchema = assignmentIdentitySchema.extend({ + scheduledDate: isoDateSchema, schedule: z.object({ id: z.number().int().positive(), set: z.object({ diff --git a/frontend/website/src/views/DashboardView.vue b/frontend/website/src/views/DashboardView.vue index 822da86..7d1eeaf 100644 --- a/frontend/website/src/views/DashboardView.vue +++ b/frontend/website/src/views/DashboardView.vue @@ -36,6 +36,14 @@ function formatDate(value: string): string { }).format(new Date(`${value}T00:00:00Z`)) } +function isOverdue(scheduledDate: string): boolean { + return scheduledDate < todayDate.value +} + +function overdueLabel(scheduledDate: string): string { + return `Overdue · Due ${formatDate(scheduledDate)}` +} + async function completeAssignment(assignmentId: number): Promise { await schedulesStore.setAssignmentCompleted(assignmentId, true) } @@ -76,7 +84,7 @@ async function completeAssignment(assignmentId: number): Promise {

- Nothing is assigned for today. + Nothing is due today.

    @@ -97,6 +105,12 @@ async function completeAssignment(assignmentId: number): Promise {

    {{ assignment.schedule.set.name }}

    {{ assignment.element.kind }} +

    {{ assignment.element.path.join(' / ') }}

    @@ -390,6 +404,19 @@ async function completeAssignment(assignmentId: number): Promise { text-transform: none; } +.today-assignment__overdue { + display: inline-flex; + width: fit-content; + margin-top: 0.85rem; + padding: 0.3rem 0.55rem; + border-radius: 999px; + color: #923f31; + background: #f7e5dc; + font-size: 0.68rem; + font-weight: 800; + letter-spacing: 0.025em; +} + .today-assignment__path { margin: 1.75rem 0 0; color: #183029;