diff --git a/backend/app/Http/Controllers/ScheduleController.php b/backend/app/Http/Controllers/ScheduleController.php index 9ec6588..cc8cd60 100644 --- a/backend/app/Http/Controllers/ScheduleController.php +++ b/backend/app/Http/Controllers/ScheduleController.php @@ -4,7 +4,6 @@ namespace App\Http\Controllers; use App\Exceptions\BadRequestException; use App\Exceptions\NotFoundException; -use App\Schedule\AssignmentForDate; use App\Schedule\Schedule; use App\Schedule\ScheduleAssignment; use App\Schedule\UseCases\CreateSchedule\CreateSchedule; @@ -12,8 +11,6 @@ use App\Schedule\UseCases\CreateSchedule\CreateScheduleRequest; use App\Schedule\UseCases\GetSchedule\GetSchedule; use App\Schedule\UseCases\GetSchedule\GetScheduleRequest; use App\Schedule\UseCases\ListSchedules\ListSchedules; -use App\Schedule\UseCases\ListAssignmentsForDate\ListAssignmentsForDate; -use App\Schedule\UseCases\ListAssignmentsForDate\ListAssignmentsForDateRequest; use App\Shared\Http\RequestInput; use App\User\User; use Illuminate\Http\JsonResponse; @@ -25,7 +22,6 @@ class ScheduleController extends Controller private CreateSchedule $createSchedule, private ListSchedules $listSchedules, private GetSchedule $getSchedule, - private ListAssignmentsForDate $listAssignmentsForDate, ) {} public function store(Request $request): JsonResponse @@ -94,36 +90,6 @@ class ScheduleController extends Controller ]); } - public function assignments(Request $request): JsonResponse - { - $input = new RequestInput($request); - $date = $input->string('date'); - - try { - $assignments = $this->listAssignmentsForDate->execute( - new ListAssignmentsForDateRequest( - user: $this->user($request), - date: $date, - ), - ); - } catch (BadRequestException $exception) { - return new JsonResponse( - ['error' => $exception->getMessage()], - 400, - ); - } - - return new JsonResponse([ - 'date' => $date, - 'assignments' => array_map( - function (AssignmentForDate $assignment): array { - return $this->assignmentForDatePayload($assignment); - }, - $assignments, - ), - ]); - } - /** * @return array */ @@ -199,30 +165,6 @@ class ScheduleController extends Controller ]; } - /** - * @return array - */ - private function assignmentForDatePayload( - AssignmentForDate $assignmentForDate, - ): array { - $assignment = $assignmentForDate->getAssignment(); - - return [ - 'id' => $assignment->getId(), - 'schedule' => [ - 'id' => $assignmentForDate->getScheduleId(), - 'set' => [ - 'name' => $assignmentForDate->getSetName(), - ], - ], - 'element' => [ - 'name' => $assignment->getName(), - 'kind' => $assignment->getKind(), - 'path' => $assignment->getPath(), - ], - ]; - } - private function user(Request $request): User { /** @var User $user */ diff --git a/backend/app/Schedule/AssignmentForDate.php b/backend/app/Schedule/AssignmentForDate.php deleted file mode 100644 index b877c39..0000000 --- a/backend/app/Schedule/AssignmentForDate.php +++ /dev/null @@ -1,27 +0,0 @@ -scheduleId; - } - - public function getSetName(): string - { - return $this->setName; - } - - public function getAssignment(): ScheduleAssignment - { - return $this->assignment; - } -} diff --git a/backend/app/Schedule/EloquentScheduleRepository.php b/backend/app/Schedule/EloquentScheduleRepository.php index 7aec036..31bb859 100644 --- a/backend/app/Schedule/EloquentScheduleRepository.php +++ b/backend/app/Schedule/EloquentScheduleRepository.php @@ -61,40 +61,6 @@ class EloquentScheduleRepository implements ScheduleRepository return $schedules; } - public function findAssignmentsForUserOnDate( - User $user, - DateTimeImmutable $date, - ): array { - $assignmentModels = ScheduleAssignmentModel::query() - ->select('schedule_assignments.*') - ->join( - 'schedules', - 'schedules.id', - '=', - 'schedule_assignments.schedule_id', - ) - ->where('schedules.user_id', $user->getId()) - ->where('schedule_assignments.scheduled_date', $date->format( - 'Y-m-d', - )) - ->with('schedule') - ->orderByDesc('schedules.id') - ->orderBy('schedule_assignments.position') - ->orderBy('schedule_assignments.id') - ->get(); - $assignments = []; - - foreach ($assignmentModels as $assignmentModel) { - $assignments[] = new AssignmentForDate( - scheduleId: $assignmentModel->schedule->id, - setName: $assignmentModel->schedule->set_name, - assignment: $this->assignmentToDomain($assignmentModel), - ); - } - - return $assignments; - } - private function toDomain(ScheduleModel $model, User $user): Schedule { $assignmentModels = ScheduleAssignmentModel::query() @@ -105,7 +71,14 @@ class EloquentScheduleRepository implements ScheduleRepository $assignments = []; foreach ($assignmentModels as $assignmentModel) { - $assignments[] = $this->assignmentToDomain($assignmentModel); + $assignments[] = new ScheduleAssignment( + id: $assignmentModel->id, + name: $assignmentModel->element_name, + kind: $assignmentModel->element_kind, + path: $assignmentModel->element_path, + scheduledDate: $this->date($assignmentModel->scheduled_date), + position: $assignmentModel->position, + ); } return new Schedule( @@ -119,19 +92,6 @@ class EloquentScheduleRepository implements ScheduleRepository ); } - private function assignmentToDomain( - ScheduleAssignmentModel $model, - ): ScheduleAssignment { - return new ScheduleAssignment( - id: $model->id, - name: $model->element_name, - kind: $model->element_kind, - path: $model->element_path, - scheduledDate: $this->date($model->scheduled_date), - position: $model->position, - ); - } - private function date(string $value): DateTimeImmutable { return new DateTimeImmutable($value, new DateTimeZone('UTC')); diff --git a/backend/app/Schedule/ScheduleAssignmentModel.php b/backend/app/Schedule/ScheduleAssignmentModel.php index 3cd5577..f0f33df 100644 --- a/backend/app/Schedule/ScheduleAssignmentModel.php +++ b/backend/app/Schedule/ScheduleAssignmentModel.php @@ -5,7 +5,6 @@ namespace App\Schedule; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\BelongsTo; /** * @property int $id @@ -15,7 +14,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; * @property list $element_path * @property string $scheduled_date * @property int $position - * @property-read ScheduleModel $schedule * * @method static Builder|ScheduleAssignmentModel newModelQuery() * @method static Builder|ScheduleAssignmentModel newQuery() @@ -48,12 +46,4 @@ class ScheduleAssignmentModel extends Model 'position' => 'integer', ]; } - - /** - * @return BelongsTo - */ - public function schedule(): BelongsTo - { - return $this->belongsTo(ScheduleModel::class, 'schedule_id'); - } } diff --git a/backend/app/Schedule/ScheduleRepository.php b/backend/app/Schedule/ScheduleRepository.php index fcdcb8c..bd02515 100644 --- a/backend/app/Schedule/ScheduleRepository.php +++ b/backend/app/Schedule/ScheduleRepository.php @@ -3,7 +3,6 @@ namespace App\Schedule; use App\User\User; -use DateTimeImmutable; interface ScheduleRepository { @@ -15,12 +14,4 @@ interface ScheduleRepository * @return list */ public function findAllForUser(User $user): array; - - /** - * @return list - */ - public function findAssignmentsForUserOnDate( - User $user, - DateTimeImmutable $date, - ): array; } diff --git a/backend/app/Schedule/UseCases/ListAssignmentsForDate/ListAssignmentsForDate.php b/backend/app/Schedule/UseCases/ListAssignmentsForDate/ListAssignmentsForDate.php deleted file mode 100644 index 9e3017d..0000000 --- a/backend/app/Schedule/UseCases/ListAssignmentsForDate/ListAssignmentsForDate.php +++ /dev/null @@ -1,59 +0,0 @@ - - * @throws BadRequestException - */ - public function execute(ListAssignmentsForDateRequest $request): array - { - $date = $this->parseDate($request->date); - - return $this->scheduleRepository->findAssignmentsForUserOnDate( - $request->user, - $date, - ); - } - - /** - * @throws BadRequestException - */ - private function parseDate(?string $value): DateTimeImmutable - { - if ($value === null || $value === '') { - throw new BadRequestException('date is required'); - } - - $date = DateTimeImmutable::createFromFormat( - '!Y-m-d', - $value, - new DateTimeZone('UTC'), - ); - $errors = DateTimeImmutable::getLastErrors(); - if ( - $date === false - || $date->format('Y-m-d') !== $value - || ($errors !== false - && ($errors['warning_count'] > 0 || $errors['error_count'] > 0)) - ) { - throw new BadRequestException( - 'date must be a valid date in YYYY-MM-DD format', - ); - } - - return $date; - } -} diff --git a/backend/app/Schedule/UseCases/ListAssignmentsForDate/ListAssignmentsForDateRequest.php b/backend/app/Schedule/UseCases/ListAssignmentsForDate/ListAssignmentsForDateRequest.php deleted file mode 100644 index f0db6a9..0000000 --- a/backend/app/Schedule/UseCases/ListAssignmentsForDate/ListAssignmentsForDateRequest.php +++ /dev/null @@ -1,13 +0,0 @@ -group(function (): void { Route::get('/sets', [SetController::class, 'index']); Route::get('/sets/{setId}', [SetController::class, 'show']) ->whereNumber('setId'); - Route::get('/assignments', [ScheduleController::class, 'assignments']); Route::post('/schedules', [ScheduleController::class, 'store']); Route::get('/schedules', [ScheduleController::class, 'index']); Route::get('/schedules/{scheduleId}', [ScheduleController::class, 'show']) diff --git a/backend/tests/Fakes/FakeScheduleRepository.php b/backend/tests/Fakes/FakeScheduleRepository.php index e30caf2..de1ae76 100644 --- a/backend/tests/Fakes/FakeScheduleRepository.php +++ b/backend/tests/Fakes/FakeScheduleRepository.php @@ -3,12 +3,10 @@ namespace Tests\Fakes; use App\Schedule\CreateScheduleDto; -use App\Schedule\AssignmentForDate; use App\Schedule\Schedule; use App\Schedule\ScheduleAssignment; use App\Schedule\ScheduleRepository; use App\User\User; -use DateTimeImmutable; class FakeScheduleRepository implements ScheduleRepository { @@ -74,31 +72,6 @@ class FakeScheduleRepository implements ScheduleRepository }, array_values($schedules)); } - public function findAssignmentsForUserOnDate( - User $user, - DateTimeImmutable $date, - ): array { - $assignments = []; - - foreach ($this->findAllForUser($user) as $schedule) { - foreach ($schedule->getAssignments() as $assignment) { - if ($assignment->getScheduledDate()->format('Y-m-d') - !== $date->format('Y-m-d') - ) { - continue; - } - - $assignments[] = new AssignmentForDate( - scheduleId: $schedule->getId(), - setName: $schedule->getSetName(), - assignment: $assignment, - ); - } - } - - return $assignments; - } - private function copy(Schedule $schedule): Schedule { $assignments = array_map( diff --git a/backend/tests/Feature/Schedule/ScheduleEndpointTest.php b/backend/tests/Feature/Schedule/ScheduleEndpointTest.php index a0e9cf8..0303dfe 100644 --- a/backend/tests/Feature/Schedule/ScheduleEndpointTest.php +++ b/backend/tests/Feature/Schedule/ScheduleEndpointTest.php @@ -221,136 +221,6 @@ class ScheduleEndpointTest extends TestCase ]); } - public function test_it_lists_the_users_assignments_for_a_date(): void - { - $user = $this->createUser('reader@example.com'); - $otherUser = $this->createUser('other@example.com'); - $olderSet = $this->createSet($user, 'Older course'); - $olderLevel = $this->createLevel($olderSet, 'lesson'); - $olderRepository = app(ElementRepository::class); - $olderRepository->create(new CreateElementDto( - name: 'First lesson', - level: $olderLevel, - parentElement: null, - )); - $olderRepository->create(new CreateElementDto( - name: 'Second lesson', - level: $olderLevel, - parentElement: null, - )); - $newerSet = $this->createSet($user, 'Newer course'); - $newerLevel = $this->createLevel($newerSet, 'chapter'); - app(ElementRepository::class)->create(new CreateElementDto( - name: 'Only chapter', - level: $newerLevel, - parentElement: null, - )); - $this->createSession($user, 'valid-token'); - $this->createSession($otherUser, 'other-token'); - - $this->credentialedPost('/api/schedules', [ - 'setId' => $olderSet->getId(), - 'levelId' => $olderLevel->getId(), - 'startDate' => '2026-08-15', - 'targetDate' => '2026-08-15', - ])->assertCreated(); - $this->credentialedPost('/api/schedules', [ - 'setId' => $newerSet->getId(), - 'levelId' => $newerLevel->getId(), - 'startDate' => '2026-08-15', - 'targetDate' => '2026-08-15', - ])->assertCreated(); - $this->withCredentials() - ->withUnencryptedCookie( - AuthMiddleware::COOKIE_NAME, - 'other-token', - )->postJson('/api/schedules', [ - 'setId' => $newerSet->getId(), - 'levelId' => $newerLevel->getId(), - 'startDate' => '2026-08-15', - 'targetDate' => '2026-08-15', - ])->assertCreated(); - $this->credentialedPost('/api/schedules', [ - 'setId' => $olderSet->getId(), - 'levelId' => $olderLevel->getId(), - 'startDate' => '2026-08-16', - 'targetDate' => '2026-08-16', - ])->assertCreated(); - - $this->credentialedGet('/api/assignments?date=2026-08-15') - ->assertOk() - ->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, - 'schedule' => [ - 'id' => 1, - 'set' => [ - 'name' => 'Older course', - ], - ], - 'element' => [ - 'name' => 'First lesson', - 'kind' => 'lesson', - 'path' => ['First lesson'], - ], - ], - [ - 'id' => 2, - 'schedule' => [ - 'id' => 1, - 'set' => [ - 'name' => 'Older course', - ], - ], - 'element' => [ - 'name' => 'Second lesson', - 'kind' => 'lesson', - 'path' => ['Second lesson'], - ], - ], - ], - ]); - } - - public function test_it_returns_an_empty_assignment_list_for_a_date(): void - { - $user = $this->createUser('reader@example.com'); - $this->createSession($user, 'valid-token'); - - $this->credentialedGet('/api/assignments?date=2026-08-15') - ->assertOk() - ->assertExactJson([ - 'date' => '2026-08-15', - 'assignments' => [], - ]); - } - - public function test_it_rejects_invalid_assignment_dates(): void - { - $user = $this->createUser('reader@example.com'); - $this->createSession($user, 'valid-token'); - - $this->credentialedGet('/api/assignments') - ->assertBadRequest() - ->assertExactJson(['error' => 'date is required']); - } - public function test_it_is_stable_after_sources_change_or_are_deleted(): void { $user = $this->createUser('reader@example.com'); @@ -453,8 +323,6 @@ class ScheduleEndpointTest extends TestCase public function test_schedule_endpoints_require_authentication(): void { - $this->getJson('/api/assignments?date=2026-08-15') - ->assertStatus(401); $this->getJson('/api/schedules')->assertStatus(401); $this->getJson('/api/schedules/1')->assertStatus(401); $this->postJson('/api/schedules', [])->assertStatus(401); diff --git a/backend/tests/Unit/Schedule/UseCases/ListAssignmentsForDateTest.php b/backend/tests/Unit/Schedule/UseCases/ListAssignmentsForDateTest.php deleted file mode 100644 index 1d1e3fd..0000000 --- a/backend/tests/Unit/Schedule/UseCases/ListAssignmentsForDateTest.php +++ /dev/null @@ -1,143 +0,0 @@ -user(1, 'reader@example.com'); - $otherUser = $this->user(2, 'other@example.com'); - $repository = new FakeScheduleRepository; - $repository->create($this->schedule( - user: $user, - setName: 'Older plan', - date: '2026-08-15', - assignmentNames: ['First', 'Second'], - )); - $repository->create($this->schedule( - user: $user, - setName: 'Newer plan', - date: '2026-08-15', - assignmentNames: ['Third'], - )); - $repository->create($this->schedule( - user: $otherUser, - setName: 'Private plan', - date: '2026-08-15', - assignmentNames: ['Hidden'], - )); - $repository->create($this->schedule( - user: $user, - setName: 'Tomorrow plan', - date: '2026-08-16', - assignmentNames: ['Later'], - )); - - $assignments = (new ListAssignmentsForDate($repository))->execute( - new ListAssignmentsForDateRequest( - user: $user, - date: '2026-08-15', - ), - ); - - $this->assertSame( - ['Newer plan', 'Older plan', 'Older plan'], - array_map(function ($assignment): string { - return $assignment->getSetName(); - }, $assignments), - ); - $this->assertSame( - ['Third', 'First', 'Second'], - array_map(function ($assignment): string { - return $assignment->getAssignment()->getName(); - }, $assignments), - ); - $this->assertSame(2, $assignments[0]->getScheduleId()); - } - - public function test_it_rejects_a_missing_date(): void - { - $this->expectException(BadRequestException::class); - $this->expectExceptionMessage('date is required'); - - (new ListAssignmentsForDate(new FakeScheduleRepository))->execute( - new ListAssignmentsForDateRequest( - user: $this->user(1, 'reader@example.com'), - date: null, - ), - ); - } - - public function test_it_rejects_an_invalid_date(): void - { - $this->expectException(BadRequestException::class); - $this->expectExceptionMessage( - 'date must be a valid date in YYYY-MM-DD format', - ); - - (new ListAssignmentsForDate(new FakeScheduleRepository))->execute( - new ListAssignmentsForDateRequest( - user: $this->user(1, 'reader@example.com'), - date: '2026-02-30', - ), - ); - } - - private function user(int $id, string $email): User - { - return new User( - id: $id, - email: new EmailAddress($email), - passwordHash: 'hashed-password', - ); - } - - /** - * @param list $assignmentNames - */ - private function schedule( - User $user, - string $setName, - string $date, - array $assignmentNames, - ): CreateScheduleDto { - $scheduledDate = new DateTimeImmutable( - $date, - new DateTimeZone('UTC'), - ); - $assignments = []; - - foreach ($assignmentNames as $index => $name) { - $assignments[] = new CreateScheduleAssignmentDto( - name: $name, - kind: 'lesson', - path: [$setName, $name], - scheduledDate: $scheduledDate, - position: $index + 1, - ); - } - - return new CreateScheduleDto( - user: $user, - setName: $setName, - elementKind: 'lesson', - startDate: $scheduledDate, - targetDate: $scheduledDate, - assignments: $assignments, - ); - } -} diff --git a/frontend/website/cypress/e2e/confirm-email.cy.ts b/frontend/website/cypress/e2e/confirm-email.cy.ts index 92bdf79..2ef0274 100644 --- a/frontend/website/cypress/e2e/confirm-email.cy.ts +++ b/frontend/website/cypress/e2e/confirm-email.cy.ts @@ -17,12 +17,6 @@ describe('email confirmation', () => { statusCode: 200, body: { schedules: [] }, }) - cy.intercept('GET', '**/api/assignments?date=*', (request) => { - request.reply({ - statusCode: 200, - body: { date: request.query.date, assignments: [] }, - }) - }) }) it('chooses a password, confirms the account, and opens the dashboard', () => { @@ -45,7 +39,7 @@ describe('email confirmation', () => { cy.wait('@confirmEmail') cy.location('pathname').should('equal', '/dashboard') - cy.get('#sets-heading').should('have.text', 'Available sets') + cy.get('h1').should('have.text', 'Available sets') }) it('validates password length and confirmation before submitting', () => { diff --git a/frontend/website/cypress/e2e/session-auth.cy.ts b/frontend/website/cypress/e2e/session-auth.cy.ts index d4b863a..eb88490 100644 --- a/frontend/website/cypress/e2e/session-auth.cy.ts +++ b/frontend/website/cypress/e2e/session-auth.cy.ts @@ -47,12 +47,6 @@ describe('session authentication', () => { statusCode: 200, body: { schedules: [] }, }) - cy.intercept('GET', '**/api/assignments?date=*', (request) => { - request.reply({ - statusCode: 200, - body: { date: request.query.date, assignments: [] }, - }) - }) }) it('restores an authenticated session on a protected route', () => { @@ -65,7 +59,7 @@ describe('session authentication', () => { cy.wait('@me') cy.location('pathname').should('equal', '/dashboard') - cy.get('#sets-heading').should('have.text', 'Available sets') + cy.get('h1').should('have.text', 'Available sets') }) it('redirects an unauthenticated protected route to login', () => { diff --git a/frontend/website/cypress/e2e/set-layout.cy.ts b/frontend/website/cypress/e2e/set-layout.cy.ts index 0e5a04b..aad6b24 100644 --- a/frontend/website/cypress/e2e/set-layout.cy.ts +++ b/frontend/website/cypress/e2e/set-layout.cy.ts @@ -51,7 +51,7 @@ describe('set element layout', () => { interceptAuthenticatedUser() }) - it('opens a set with a foldable element hierarchy', () => { + it('opens a set from the dashboard and shows its full hierarchy', () => { cy.intercept('GET', '**/api/sets', { statusCode: 200, body: { sets: [{ id: 41, name: 'Bible' }] }, @@ -79,35 +79,13 @@ describe('set element layout', () => { cy.get('[data-element-id="1"] > .element-node__card') .should('contain.text', 'Genesis') .and('contain.text', 'book') - .and('have.attr', 'aria-expanded', 'false') - .and('have.attr', 'aria-controls', 'element-children-1') - cy.get('[data-element-id="2"] > .element-node__card').should( - 'not.match', - 'button', - ) - cy.get('[data-element-id="3"]').should('not.exist') - - cy.get('[data-element-id="1"] > button.element-node__card').click() - cy.get('[data-element-id="1"] > .element-node__card').should( - 'have.attr', - 'aria-expanded', - 'true', - ) cy.get('[data-element-id="1"] > ol > li').then(($nodes) => { expect([...$nodes].map((node) => node.dataset.elementId)).to.deep.equal([ '3', '5', ]) }) - cy.get('[data-element-id="4"]').should('not.exist') - - cy.get('[data-element-id="3"] > button.element-node__card').click() - cy.get('[data-element-id="3"] > .element-node__card').should( - 'have.attr', - 'aria-expanded', - 'true', - ) - cy.get('#element-children-3 > li') + cy.get('[data-element-id="3"] > ol > li') .should('have.length', 1) .and('have.attr', 'data-element-id', '4') cy.contains('.element-node__card', 'Chapter 1') @@ -115,15 +93,6 @@ describe('set element layout', () => { .find('.element-node__kind') .should('have.text', 'Chapter_sections-v2') .and('have.css', 'text-transform', 'none') - - cy.get('[data-element-id="1"] > button.element-node__card').click() - cy.get('[data-element-id="3"]').should('not.exist') - - cy.get('[data-element-id="1"] > button.element-node__card').click() - cy.get('[data-element-id="4"] > .element-node__card').should( - 'contain.text', - 'Chapter 1', - ) }) it('shows loading and empty layout states', () => { @@ -214,8 +183,6 @@ describe('set element layout', () => { cy.wait('@me') cy.wait('@layout') - cy.get('[data-element-id="1"] > button.element-node__card').click() - cy.get('[data-element-id="3"] > button.element-node__card').click() cy.contains('.element-node__card', 'Chapter 1').should('be.visible') cy.document().then((document) => { expect(document.documentElement.scrollWidth).to.be.at.most( diff --git a/frontend/website/cypress/e2e/set-scheduling.cy.ts b/frontend/website/cypress/e2e/set-scheduling.cy.ts index a12f672..5747d6b 100644 --- a/frontend/website/cypress/e2e/set-scheduling.cy.ts +++ b/frontend/website/cypress/e2e/set-scheduling.cy.ts @@ -250,12 +250,6 @@ describe('set scheduling', () => { }, }) }).as('schedules') - cy.intercept('GET', '**/api/assignments?date=*', (request) => { - request.reply({ - statusCode: 200, - body: { date: request.query.date, assignments: [] }, - }) - }) cy.visit('/dashboard') cy.wait('@me') diff --git a/frontend/website/cypress/e2e/sets-dashboard.cy.ts b/frontend/website/cypress/e2e/sets-dashboard.cy.ts index 5c9b28e..2557ded 100644 --- a/frontend/website/cypress/e2e/sets-dashboard.cy.ts +++ b/frontend/website/cypress/e2e/sets-dashboard.cy.ts @@ -17,12 +17,6 @@ describe('sets dashboard', () => { statusCode: 200, body: { schedules: [] }, }) - cy.intercept('GET', '**/api/assignments?date=*', (request) => { - request.reply({ - statusCode: 200, - body: { date: request.query.date, assignments: [] }, - }) - }) }) it('shows every available set as a detail link', () => { @@ -44,7 +38,7 @@ describe('sets dashboard', () => { cy.wait('@me') cy.wait('@sets') - cy.get('#sets-heading').should('have.text', 'Available sets') + cy.get('h1').should('have.text', 'Available sets') cy.get('ul[aria-label="Available sets"] h2').then(($headings) => { expect([...$headings].map((heading) => heading.textContent)).to.deep.equal([ 'Bible', diff --git a/frontend/website/cypress/e2e/today-assignments.cy.ts b/frontend/website/cypress/e2e/today-assignments.cy.ts deleted file mode 100644 index 9d31dde..0000000 --- a/frontend/website/cypress/e2e/today-assignments.cy.ts +++ /dev/null @@ -1,157 +0,0 @@ -const authenticatedUser = { - id: 7, - email: 'user@example.com', -} - -const browserToday = '2026-08-15' - -function interceptDashboardRequests(): void { - cy.intercept('GET', '**/api/me', { - statusCode: 200, - body: { user: authenticatedUser }, - }).as('me') - cy.intercept('GET', '**/api/sets', { - statusCode: 200, - body: { sets: [] }, - }).as('sets') - cy.intercept('GET', '**/api/schedules', { - statusCode: 200, - body: { schedules: [] }, - }).as('schedules') -} - -describe("today's assignments", () => { - beforeEach(() => { - cy.clock(new Date(2026, 7, 15, 0, 30).getTime()) - interceptDashboardRequests() - }) - - it('uses the browser date and links every assignment to its schedule', () => { - cy.intercept('GET', `**/api/assignments?date=${browserToday}`, (request) => { - expect(request.headers.accept).to.equal('application/json') - request.reply({ - statusCode: 200, - body: { - date: browserToday, - assignments: [ - { - id: 12, - schedule: { - id: 73, - set: { name: 'Bible' }, - }, - element: { - name: 'Chapter 1', - kind: 'Chapter_sections-v2', - path: ['Genesis', 'Creation', 'Chapter 1'], - }, - }, - { - id: 13, - schedule: { - id: 81, - set: { name: 'Course' }, - }, - element: { - name: 'Introduction', - kind: 'lesson', - path: ['Introduction'], - }, - }, - ], - }, - }) - }).as('todayAssignments') - - cy.visit('/dashboard') - cy.wait('@me') - cy.wait('@todayAssignments') - - cy.get('#today-heading').should('have.text', 'Today') - cy.get('[data-today-date]').should( - 'have.attr', - 'data-today-date', - browserToday, - ) - cy.get('ul[aria-label="Today\'s assignments"] > li').should( - 'have.length', - 2, - ) - cy.contains('a', 'Genesis / Creation / Chapter 1') - .should('contain.text', 'Bible') - .and('have.attr', 'href', '/schedules/73') - cy.contains('a', 'Introduction') - .should('contain.text', 'Course') - .and('have.attr', 'href', '/schedules/81') - cy.get('.today-assignment__kind') - .first() - .should('have.text', 'Chapter_sections-v2') - .and('have.css', 'text-transform', 'none') - }) - - it('shows loading and empty states', () => { - cy.intercept('GET', `**/api/assignments?date=${browserToday}`, { - delay: 2500, - statusCode: 200, - body: { - date: browserToday, - assignments: [], - }, - }).as('todayAssignments') - - cy.visit('/dashboard') - cy.wait('@me') - cy.get('.today-assignments [role="status"]').should( - 'contain.text', - "Loading today's assignments...", - ) - cy.wait('@todayAssignments') - - cy.get('.today-assignments [role="status"]').should( - 'contain.text', - 'Nothing is assigned for today.', - ) - }) - - it('retries independently after the request fails', () => { - let requestCount = 0 - cy.intercept( - 'GET', - `**/api/assignments?date=${browserToday}`, - (request) => { - requestCount += 1 - request.alias = `todayAssignments${requestCount}` - - if (requestCount === 1) { - request.reply({ statusCode: 500 }) - return - } - - request.reply({ - statusCode: 200, - body: { - date: browserToday, - assignments: [], - }, - }) - }, - ) - - cy.visit('/dashboard') - cy.wait('@me') - cy.wait('@todayAssignments1') - - cy.get('.today-assignments [role="alert"]') - .should('contain.text', "We couldn't load today's assignments.") - .within(() => { - cy.contains('button', 'Try again').click() - }) - cy.get('#schedules-heading').should('have.text', 'Your schedules') - cy.wait('@todayAssignments2') - - cy.get('.today-assignments [role="status"]').should( - 'contain.text', - 'Nothing is assigned for today.', - ) - }) -}) diff --git a/frontend/website/src/components/ElementTree.vue b/frontend/website/src/components/ElementTree.vue index e0ca8a4..54d7e39 100644 --- a/frontend/website/src/components/ElementTree.vue +++ b/frontend/website/src/components/ElementTree.vue @@ -1,23 +1,10 @@ @@ -109,23 +69,6 @@ function isExpanded(elementId: number): boolean { box-shadow: 0 0.55rem 1.5rem rgb(40 62 52 / 6%); } -.element-node__card--toggle { - width: 100%; - color: inherit; - text-align: left; - cursor: pointer; -} - -.element-node__card--toggle:hover { - border-color: rgb(40 92 78 / 34%); - background: #fffdf7; -} - -.element-node__card--toggle:focus-visible { - outline: 3px solid rgb(86 127 112 / 34%); - outline-offset: 0.2rem; -} - .element-node__name { min-width: 0; color: #183029; @@ -135,13 +78,6 @@ function isExpanded(elementId: number): boolean { overflow-wrap: anywhere; } -.element-node__metadata { - display: flex; - flex: 0 0 auto; - align-items: center; - gap: 0.8rem; -} - .element-node__kind { flex: 0 0 auto; padding: 0.35rem 0.55rem; @@ -154,18 +90,6 @@ function isExpanded(elementId: number): boolean { text-transform: none; } -.element-node__chevron { - width: 0.55rem; - height: 0.55rem; - border-right: 2px solid #5e7067; - border-bottom: 2px solid #5e7067; - transform: rotate(-45deg); -} - -.element-node__chevron--expanded { - transform: rotate(45deg); -} - @media (max-width: 37.5rem) { .element-tree { margin-left: 0.45rem; @@ -182,10 +106,6 @@ function isExpanded(elementId: number): boolean { padding: 0.75rem 0.7rem; } - .element-node__metadata { - gap: 0.6rem; - } - .element-node__kind { padding-inline: 0.42rem; font-size: 0.56rem; diff --git a/frontend/website/src/stores/schedules.ts b/frontend/website/src/stores/schedules.ts index 090db7c..f8844e6 100644 --- a/frontend/website/src/stores/schedules.ts +++ b/frontend/website/src/stores/schedules.ts @@ -26,15 +26,6 @@ const scheduleAssignmentSchema = z.object({ }), }) -export const assignmentForDateSchema = scheduleAssignmentSchema.extend({ - schedule: z.object({ - id: z.number().int().positive(), - set: z.object({ - name: z.string().min(1), - }), - }), -}) - export const scheduleDetailSchema = scheduleSummarySchema.extend({ days: z.array( z.object({ @@ -52,18 +43,12 @@ const scheduleResponseSchema = z.object({ schedule: scheduleDetailSchema, }) -const assignmentsForDateResponseSchema = z.object({ - date: isoDateSchema, - assignments: z.array(assignmentForDateSchema), -}) - const errorResponseSchema = z.object({ error: z.string().min(1), }) export type ScheduleSummary = z.infer export type ScheduleDetail = z.infer -export type AssignmentForDate = z.infer export type CreateScheduleInput = { setId: number levelId: number @@ -74,7 +59,6 @@ export type CreateScheduleInput = { const LIST_ERROR = "We couldn't load your schedules." const DETAIL_ERROR = "We couldn't load this schedule." const CREATE_ERROR = "We couldn't create this schedule." -const ASSIGNMENTS_ERROR = "We couldn't load today's assignments." export const useSchedulesStore = defineStore('schedules', () => { const schedules = ref([]) @@ -86,11 +70,7 @@ export const useSchedulesStore = defineStore('schedules', () => { const detailNotFound = ref(false) const creating = ref(false) const createError = ref(null) - const assignmentsForDate = ref([]) - const assignmentsLoading = ref(false) - const assignmentsError = ref(null) let activeDetailRequestId = 0 - let assignmentsRequestId = 0 async function fetchSchedules(): Promise { listLoading.value = true @@ -222,58 +202,6 @@ export const useSchedulesStore = defineStore('schedules', () => { } } - async function fetchAssignmentsForDate(date: string): Promise { - const requestId = ++assignmentsRequestId - assignmentsForDate.value = [] - assignmentsLoading.value = true - assignmentsError.value = null - - try { - const query = new URLSearchParams({ date }) - const response = await fetch(`${API_BASE}/api/assignments?${query.toString()}`, { - method: 'GET', - credentials: 'include', - headers: { - Accept: 'application/json', - }, - }) - - if (requestId !== assignmentsRequestId) { - return false - } - - if (response.status !== 200) { - assignmentsError.value = ASSIGNMENTS_ERROR - - return false - } - - const responseBody: unknown = await response.json() - if (requestId !== assignmentsRequestId) { - return false - } - - const parsedResponse = assignmentsForDateResponseSchema.parse(responseBody) - if (parsedResponse.date !== date) { - throw new Error('assignment response did not match requested date') - } - assignmentsForDate.value = parsedResponse.assignments - - return true - } catch { - if (requestId === assignmentsRequestId) { - assignmentsForDate.value = [] - assignmentsError.value = ASSIGNMENTS_ERROR - } - - return false - } finally { - if (requestId === assignmentsRequestId) { - assignmentsLoading.value = false - } - } - } - return { schedules, listLoading, @@ -284,12 +212,8 @@ export const useSchedulesStore = defineStore('schedules', () => { detailNotFound, creating, createError, - assignmentsForDate, - assignmentsLoading, - assignmentsError, fetchSchedules, fetchSchedule, createSchedule, - fetchAssignmentsForDate, } }) diff --git a/frontend/website/src/views/DashboardView.vue b/frontend/website/src/views/DashboardView.vue index c99ccf4..12464ee 100644 --- a/frontend/website/src/views/DashboardView.vue +++ b/frontend/website/src/views/DashboardView.vue @@ -1,6 +1,6 @@