diff --git a/backend/src/Scolarite/Application/Port/ScheduleDisplayReader.php b/backend/src/Scolarite/Application/Port/ScheduleDisplayReader.php index c59a211..b89dbb3 100644 --- a/backend/src/Scolarite/Application/Port/ScheduleDisplayReader.php +++ b/backend/src/Scolarite/Application/Port/ScheduleDisplayReader.php @@ -15,9 +15,9 @@ interface ScheduleDisplayReader /** * @param string ...$subjectIds Identifiants des matières * - * @return array Map subjectId => nom de la matière + * @return array Map subjectId => {name, color} */ - public function subjectNames(string $tenantId, string ...$subjectIds): array; + public function subjectDisplay(string $tenantId, string ...$subjectIds): array; /** * @param string ...$teacherIds Identifiants des enseignants diff --git a/backend/src/Scolarite/Application/Query/GetChildrenSchedule/GetChildrenScheduleHandler.php b/backend/src/Scolarite/Application/Query/GetChildrenSchedule/GetChildrenScheduleHandler.php index 52fbd50..1a6c152 100644 --- a/backend/src/Scolarite/Application/Query/GetChildrenSchedule/GetChildrenScheduleHandler.php +++ b/backend/src/Scolarite/Application/Query/GetChildrenSchedule/GetChildrenScheduleHandler.php @@ -111,13 +111,14 @@ final readonly class GetChildrenScheduleHandler array_map(static fn (ResolvedScheduleSlot $s): string => (string) $s->teacherId, $slots), )); - $subjectNames = $this->displayReader->subjectNames($tenantId, ...$subjectIds); + $subjects = $this->displayReader->subjectDisplay($tenantId, ...$subjectIds); $teacherNames = $this->displayReader->teacherNames($tenantId, ...$teacherIds); return array_map( static fn (ResolvedScheduleSlot $s): StudentScheduleSlotDto => StudentScheduleSlotDto::fromResolved( $s, - $subjectNames[(string) $s->subjectId] ?? '', + $subjects[(string) $s->subjectId]['name'] ?? '', + $subjects[(string) $s->subjectId]['color'] ?? null, $teacherNames[(string) $s->teacherId] ?? '', ), $slots, diff --git a/backend/src/Scolarite/Application/Query/GetStudentSchedule/GetStudentScheduleHandler.php b/backend/src/Scolarite/Application/Query/GetStudentSchedule/GetStudentScheduleHandler.php index fb148fe..8bede05 100644 --- a/backend/src/Scolarite/Application/Query/GetStudentSchedule/GetStudentScheduleHandler.php +++ b/backend/src/Scolarite/Application/Query/GetStudentSchedule/GetStudentScheduleHandler.php @@ -74,13 +74,14 @@ final readonly class GetStudentScheduleHandler array_map(static fn (ResolvedScheduleSlot $s): string => (string) $s->teacherId, $slots), )); - $subjectNames = $this->displayReader->subjectNames($tenantId, ...$subjectIds); + $subjects = $this->displayReader->subjectDisplay($tenantId, ...$subjectIds); $teacherNames = $this->displayReader->teacherNames($tenantId, ...$teacherIds); return array_map( static fn (ResolvedScheduleSlot $s): StudentScheduleSlotDto => StudentScheduleSlotDto::fromResolved( $s, - $subjectNames[(string) $s->subjectId] ?? '', + $subjects[(string) $s->subjectId]['name'] ?? '', + $subjects[(string) $s->subjectId]['color'] ?? null, $teacherNames[(string) $s->teacherId] ?? '', ), $slots, diff --git a/backend/src/Scolarite/Application/Query/GetStudentSchedule/StudentScheduleSlotDto.php b/backend/src/Scolarite/Application/Query/GetStudentSchedule/StudentScheduleSlotDto.php index 398b07b..5504185 100644 --- a/backend/src/Scolarite/Application/Query/GetStudentSchedule/StudentScheduleSlotDto.php +++ b/backend/src/Scolarite/Application/Query/GetStudentSchedule/StudentScheduleSlotDto.php @@ -16,6 +16,7 @@ final readonly class StudentScheduleSlotDto public string $endTime, public string $subjectId, public string $subjectName, + public ?string $subjectColor, public string $teacherId, public string $teacherName, public ?string $room, @@ -27,6 +28,7 @@ final readonly class StudentScheduleSlotDto public static function fromResolved( ResolvedScheduleSlot $slot, string $subjectName, + ?string $subjectColor, string $teacherName, ): self { return new self( @@ -37,6 +39,7 @@ final readonly class StudentScheduleSlotDto endTime: $slot->timeSlot->endTime, subjectId: (string) $slot->subjectId, subjectName: $subjectName, + subjectColor: $subjectColor, teacherId: (string) $slot->teacherId, teacherName: $teacherName, room: $slot->room, diff --git a/backend/src/Scolarite/Infrastructure/Api/Controller/ParentScheduleController.php b/backend/src/Scolarite/Infrastructure/Api/Controller/ParentScheduleController.php index 94d3062..2e09f06 100644 --- a/backend/src/Scolarite/Infrastructure/Api/Controller/ParentScheduleController.php +++ b/backend/src/Scolarite/Infrastructure/Api/Controller/ParentScheduleController.php @@ -29,9 +29,6 @@ use function usort; /** * Endpoints de consultation de l'emploi du temps des enfants pour le parent connecté. - * - * @see Story 4.4 - Consultation EDT par le Parent - * @see FR30 - Consulter emploi du temps enfant (parent) */ #[IsGranted(ScheduleSlotVoter::VIEW)] final readonly class ParentScheduleController @@ -190,6 +187,7 @@ final readonly class ParentScheduleController 'endTime' => $slot->endTime, 'subjectId' => $slot->subjectId, 'subjectName' => $slot->subjectName, + 'subjectColor' => $slot->subjectColor, 'teacherId' => $slot->teacherId, 'teacherName' => $slot->teacherName, 'room' => $slot->room, diff --git a/backend/src/Scolarite/Infrastructure/Api/Controller/StudentScheduleController.php b/backend/src/Scolarite/Infrastructure/Api/Controller/StudentScheduleController.php index b48e73f..7590a3c 100644 --- a/backend/src/Scolarite/Infrastructure/Api/Controller/StudentScheduleController.php +++ b/backend/src/Scolarite/Infrastructure/Api/Controller/StudentScheduleController.php @@ -29,9 +29,6 @@ use function usort; /** * Endpoints de consultation de l'emploi du temps pour l'élève connecté. - * - * @see Story 4.3 - Consultation EDT par l'Élève - * @see FR29 - Consulter emploi du temps (élève) */ #[IsGranted(ScheduleSlotVoter::VIEW)] final readonly class StudentScheduleController @@ -178,6 +175,7 @@ final readonly class StudentScheduleController 'endTime' => $slot->endTime, 'subjectId' => $slot->subjectId, 'subjectName' => $slot->subjectName, + 'subjectColor' => $slot->subjectColor, 'teacherId' => $slot->teacherId, 'teacherName' => $slot->teacherName, 'room' => $slot->room, diff --git a/backend/src/Scolarite/Infrastructure/Service/DoctrineScheduleDisplayReader.php b/backend/src/Scolarite/Infrastructure/Service/DoctrineScheduleDisplayReader.php index 90115d5..a53af54 100644 --- a/backend/src/Scolarite/Infrastructure/Service/DoctrineScheduleDisplayReader.php +++ b/backend/src/Scolarite/Infrastructure/Service/DoctrineScheduleDisplayReader.php @@ -20,29 +20,31 @@ final readonly class DoctrineScheduleDisplayReader implements ScheduleDisplayRea } #[Override] - public function subjectNames(string $tenantId, string ...$subjectIds): array + public function subjectDisplay(string $tenantId, string ...$subjectIds): array { if ($subjectIds === []) { return []; } $rows = $this->connection->fetchAllAssociative( - 'SELECT id, name FROM subjects WHERE id IN (:ids) AND tenant_id = :tenantId', + 'SELECT id, name, color FROM subjects WHERE id IN (:ids) AND tenant_id = :tenantId', ['ids' => $subjectIds, 'tenantId' => $tenantId], ['ids' => ArrayParameterType::STRING], ); - $names = []; + $display = []; foreach ($rows as $row) { /** @var string $id */ $id = $row['id']; /** @var string $name */ $name = $row['name']; - $names[$id] = $name; + /** @var string|null $color */ + $color = $row['color']; + $display[$id] = ['name' => $name, 'color' => $color]; } - return $names; + return $display; } #[Override] diff --git a/backend/tests/Unit/Scolarite/Application/Query/GetChildrenSchedule/GetChildrenScheduleHandlerTest.php b/backend/tests/Unit/Scolarite/Application/Query/GetChildrenSchedule/GetChildrenScheduleHandlerTest.php index 00c4bf4..3f54a8f 100644 --- a/backend/tests/Unit/Scolarite/Application/Query/GetChildrenSchedule/GetChildrenScheduleHandlerTest.php +++ b/backend/tests/Unit/Scolarite/Application/Query/GetChildrenSchedule/GetChildrenScheduleHandlerTest.php @@ -265,9 +265,14 @@ final class GetChildrenScheduleHandlerTest extends TestCase ) { } - public function subjectNames(string $tenantId, string ...$subjectIds): array + public function subjectDisplay(string $tenantId, string ...$subjectIds): array { - return $this->subjects; + $display = []; + foreach ($this->subjects as $id => $name) { + $display[$id] = ['name' => $name, 'color' => null]; + } + + return $display; } public function teacherNames(string $tenantId, string ...$teacherIds): array diff --git a/backend/tests/Unit/Scolarite/Application/Query/GetStudentSchedule/GetStudentScheduleHandlerTest.php b/backend/tests/Unit/Scolarite/Application/Query/GetStudentSchedule/GetStudentScheduleHandlerTest.php index 00b2f07..c598848 100644 --- a/backend/tests/Unit/Scolarite/Application/Query/GetStudentSchedule/GetStudentScheduleHandlerTest.php +++ b/backend/tests/Unit/Scolarite/Application/Query/GetStudentSchedule/GetStudentScheduleHandlerTest.php @@ -210,9 +210,14 @@ final class GetStudentScheduleHandlerTest extends TestCase ) { } - public function subjectNames(string $tenantId, string ...$subjectIds): array + public function subjectDisplay(string $tenantId, string ...$subjectIds): array { - return $this->subjects; + $display = []; + foreach ($this->subjects as $id => $name) { + $display[$id] = ['name' => $name, 'color' => null]; + } + + return $display; } public function teacherNames(string $tenantId, string ...$teacherIds): array diff --git a/frontend/e2e/child-selector.spec.ts b/frontend/e2e/child-selector.spec.ts index 1f71f7b..9cc8c47 100644 --- a/frontend/e2e/child-selector.spec.ts +++ b/frontend/e2e/child-selector.spec.ts @@ -147,11 +147,11 @@ test.describe('Child Selector', () => { // Should display the label await expect(childSelector.locator('.child-selector-label')).toHaveText('Enfant :'); - // Should have 2 child buttons + // Should have 3 buttons: "Tous" + 2 children const buttons = childSelector.locator('.child-button'); - await expect(buttons).toHaveCount(2); + await expect(buttons).toHaveCount(3); - // First child should be auto-selected + // "Tous" button should be selected initially (no child auto-selected) await expect(buttons.first()).toHaveClass(/selected/); }); @@ -162,18 +162,18 @@ test.describe('Child Selector', () => { await expect(childSelector).toBeVisible({ timeout: 10000 }); const buttons = childSelector.locator('.child-button'); - await expect(buttons).toHaveCount(2); + await expect(buttons).toHaveCount(3); - // First button should be selected initially - await expect(buttons.first()).toHaveClass(/selected/); + // "Tous" button (index 0) should be selected initially + await expect(buttons.nth(0)).toHaveClass(/selected/); await expect(buttons.nth(1)).not.toHaveClass(/selected/); - // Click second button + // Click first child button (index 1) await buttons.nth(1).click(); - // Second button should now be selected, first should not + // First child should now be selected, "Tous" should not await expect(buttons.nth(1)).toHaveClass(/selected/); - await expect(buttons.first()).not.toHaveClass(/selected/); + await expect(buttons.nth(0)).not.toHaveClass(/selected/); }); test('[P1] parent with single child should see static child name', async ({ browser, page }) => { diff --git a/frontend/e2e/parent-schedule.spec.ts b/frontend/e2e/parent-schedule.spec.ts index bd2f766..4179b98 100644 --- a/frontend/e2e/parent-schedule.spec.ts +++ b/frontend/e2e/parent-schedule.spec.ts @@ -114,6 +114,21 @@ async function loginAsParent(page: import('@playwright/test').Page) { ]); } +/** + * Multi-child parents land on the summary view (no child auto-selected). + * This helper selects the first actual child (skipping the "Tous" button). + */ +async function selectFirstChild(page: import('@playwright/test').Page) { + const childButtons = page.locator('.child-button'); + await expect(childButtons.first()).toBeVisible({ timeout: 15000 }); + const count = await childButtons.count(); + if (count > 2) { + // Multi-child: "Tous" is at index 0, first child at index 1 + await childButtons.nth(1).click(); + } + // Single child is auto-selected, nothing to do +} + test.describe('Parent Schedule Consultation (Story 4.4)', () => { test.describe.configure({ mode: 'serial' }); @@ -305,6 +320,7 @@ test.describe('Parent Schedule Consultation (Story 4.4)', () => { await expect( page.getByRole('heading', { name: /emploi du temps des enfants/i }) ).toBeVisible({ timeout: 15000 }); + await selectFirstChild(page); await navigateToSeededDay(page); // Wait for slots to load @@ -324,19 +340,29 @@ test.describe('Parent Schedule Consultation (Story 4.4)', () => { page.getByRole('heading', { name: /emploi du temps des enfants/i }) ).toBeVisible({ timeout: 15000 }); - // If multiple children, buttons should be visible + // Multi-child: "Tous" + N children buttons const childButtons = page.locator('.child-button'); const count = await childButtons.count(); - if (count > 1) { - // Click second child + if (count > 2) { + // Select first child (index 1, after "Tous") await childButtons.nth(1).click(); - await page.waitForTimeout(500); - - // Should update schedule data await navigateToSeededDay(page); - const slots = page.locator('[data-testid="schedule-slot"]'); - await expect(slots.first()).toBeVisible({ timeout: 20000 }); + await expect(page.locator('[data-testid="schedule-slot"]').first()).toBeVisible({ + timeout: 20000 + }); + + // Switch to second child (index 2) + await childButtons.nth(2).click(); + await page.waitForTimeout(500); + await navigateToSeededDay(page); + await expect(page.locator('[data-testid="schedule-slot"]').first()).toBeVisible({ + timeout: 20000 + }); + + // Switch back to "Tous" summary view + await childButtons.nth(0).click(); + await expect(page.locator('.multi-child-summary')).toBeVisible({ timeout: 10000 }); } }); }); @@ -351,6 +377,7 @@ test.describe('Parent Schedule Consultation (Story 4.4)', () => { await expect( page.getByRole('heading', { name: /emploi du temps des enfants/i }) ).toBeVisible({ timeout: 15000 }); + await selectFirstChild(page); const dayButton = page.locator('.view-toggle button', { hasText: 'Jour' }); await expect(dayButton).toHaveClass(/active/, { timeout: 5000 }); @@ -362,6 +389,7 @@ test.describe('Parent Schedule Consultation (Story 4.4)', () => { await expect( page.getByRole('heading', { name: /emploi du temps des enfants/i }) ).toBeVisible({ timeout: 15000 }); + await selectFirstChild(page); await navigateToSeededDay(page); // Wait for slots to load @@ -384,6 +412,7 @@ test.describe('Parent Schedule Consultation (Story 4.4)', () => { await expect( page.getByRole('heading', { name: /emploi du temps des enfants/i }) ).toBeVisible({ timeout: 15000 }); + await selectFirstChild(page); await navigateToSeededDay(page); await expect(page.locator('[data-testid="schedule-slot"]').first()).toBeVisible({ @@ -419,6 +448,7 @@ test.describe('Parent Schedule Consultation (Story 4.4)', () => { await expect( page.getByRole('heading', { name: /emploi du temps des enfants/i }) ).toBeVisible({ timeout: 15000 }); + await selectFirstChild(page); // Wait for schedule slots to load await expect(page.locator('[data-testid="schedule-slot"]').first()).toBeVisible({ @@ -448,9 +478,11 @@ test.describe('Parent Schedule Consultation (Story 4.4)', () => { const childButtons = page.locator('.child-button'); const count = await childButtons.count(); - test.skip(count < 2, 'Need at least 2 children for this test'); + // "Tous" + at least 2 children = 3 buttons minimum + test.skip(count < 3, 'Need at least 2 children for this test'); - // Navigate to seeded day to see slots + // Select first child (index 1, after "Tous") + await childButtons.nth(1).click(); await navigateToSeededDay(page); // First child (6A) should show Maths @@ -461,8 +493,8 @@ test.describe('Parent Schedule Consultation (Story 4.4)', () => { timeout: 5000 }); - // Switch to second child (5B) — should show SVT - await childButtons.nth(1).click(); + // Switch to second child (index 2) (5B) — should show SVT + await childButtons.nth(2).click(); await navigateToSeededDay(page); await expect(page.locator('[data-testid="schedule-slot"]').first()).toBeVisible({ timeout: 20000 @@ -480,6 +512,7 @@ test.describe('Parent Schedule Consultation (Story 4.4)', () => { test('shows offline banner when network is lost', async ({ page, context }) => { await loginAsParent(page); await page.goto(`${ALPHA_URL}/dashboard/parent-schedule`); + await selectFirstChild(page); await navigateToSeededDay(page); // Wait for schedule to load diff --git a/frontend/src/lib/components/organisms/ChildSelector/ChildSelector.svelte b/frontend/src/lib/components/organisms/ChildSelector/ChildSelector.svelte index 8eee988..215946c 100644 --- a/frontend/src/lib/components/organisms/ChildSelector/ChildSelector.svelte +++ b/frontend/src/lib/components/organisms/ChildSelector/ChildSelector.svelte @@ -14,7 +14,7 @@ let { onChildSelected }: { - onChildSelected?: (childId: string) => void; + onChildSelected?: (childId: string | null) => void; } = $props(); let children = $state([]); @@ -40,8 +40,9 @@ const data = await response.json(); children = data['hydra:member'] ?? data['member'] ?? (Array.isArray(data) ? data : []); + // Auto-select only when there's exactly 1 child const first = children[0]; - if (first && !selectedChildId) { + if (first && !selectedChildId && children.length === 1) { selectedChildId = first.studentId; onChildSelected?.(first.studentId); } @@ -52,7 +53,7 @@ } } - function selectChild(childId: string) { + function selectChild(childId: string | null) { selectedChildId = childId; onChildSelected?.(childId); } @@ -75,6 +76,13 @@
Enfant :
+ {#each children as child (child.id)}