From 39f8650b92a1190aa04e634db89ea579dd8f2805 Mon Sep 17 00:00:00 2001 From: Mathias STRASSER Date: Fri, 6 Mar 2026 12:07:28 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20Filtrer=20enseignants=20et=20mati=C3=A8r?= =?UTF-8?q?es=20par=20affectation=20lors=20de=20la=20cr=C3=A9ation=20de=20?= =?UTF-8?q?cr=C3=A9neaux?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quand une classe n'avait aucune affectation enseignant-matière, les selects de la modale de création de créneau affichaient tous les enseignants et toutes les matières au lieu d'une liste vide. Cela permettait de soumettre des combinaisons invalides, produisant un message d'erreur avec des UUID incompréhensibles. Les dropdowns n'affichent plus que les enseignants/matières effectivement affectés à la classe sélectionnée. Le message d'erreur backend est reformulé sans UUID pour le cas où la validation frontend serait contournée. --- .../Processor/CreateScheduleSlotProcessor.php | 7 +- .../Processor/UpdateScheduleSlotProcessor.php | 7 +- docs/stories/story-4.6.md | 56 +++++++ frontend/e2e/schedule-advanced.spec.ts | 64 ++++---- frontend/e2e/schedule.spec.ts | 142 +++++++++++++----- .../src/routes/admin/schedule/+page.svelte | 22 ++- 6 files changed, 211 insertions(+), 87 deletions(-) create mode 100644 docs/stories/story-4.6.md diff --git a/backend/src/Scolarite/Infrastructure/Api/Processor/CreateScheduleSlotProcessor.php b/backend/src/Scolarite/Infrastructure/Api/Processor/CreateScheduleSlotProcessor.php index d1921a9..9a7c93a 100644 --- a/backend/src/Scolarite/Infrastructure/Api/Processor/CreateScheduleSlotProcessor.php +++ b/backend/src/Scolarite/Infrastructure/Api/Processor/CreateScheduleSlotProcessor.php @@ -96,8 +96,11 @@ final readonly class CreateScheduleSlotProcessor implements ProcessorInterface } return $resource; - } catch (EnseignantNonAffecteException $e) { - throw new UnprocessableEntityHttpException($e->getMessage()); + } catch (EnseignantNonAffecteException) { + throw new UnprocessableEntityHttpException( + "L'enseignant sélectionné n'est pas affecté à cette classe pour cette matière. " + . 'Veuillez vérifier les affectations enseignant-classe-matière.', + ); } catch (CreneauHoraireInvalideException|ValueError $e) { throw new BadRequestHttpException($e->getMessage()); } diff --git a/backend/src/Scolarite/Infrastructure/Api/Processor/UpdateScheduleSlotProcessor.php b/backend/src/Scolarite/Infrastructure/Api/Processor/UpdateScheduleSlotProcessor.php index dbadaa9..c398ada 100644 --- a/backend/src/Scolarite/Infrastructure/Api/Processor/UpdateScheduleSlotProcessor.php +++ b/backend/src/Scolarite/Infrastructure/Api/Processor/UpdateScheduleSlotProcessor.php @@ -105,8 +105,11 @@ final readonly class UpdateScheduleSlotProcessor implements ProcessorInterface } return $resource; - } catch (EnseignantNonAffecteException $e) { - throw new UnprocessableEntityHttpException($e->getMessage()); + } catch (EnseignantNonAffecteException) { + throw new UnprocessableEntityHttpException( + "L'enseignant sélectionné n'est pas affecté à cette classe pour cette matière. " + . 'Veuillez vérifier les affectations enseignant-classe-matière.', + ); } catch (ScheduleSlotNotFoundException|InvalidUuidStringException) { throw new NotFoundHttpException('Créneau non trouvé.'); } catch (CreneauHoraireInvalideException|ValueError $e) { diff --git a/docs/stories/story-4.6.md b/docs/stories/story-4.6.md new file mode 100644 index 0000000..402b20f --- /dev/null +++ b/docs/stories/story-4.6.md @@ -0,0 +1,56 @@ +# Story 4.6: Recherche autocomplete pour lier un parent à un élève + +Status: ready-for-dev + +## Story + +As an administrateur, +I want to search for a parent by name or email when linking them to a student, +so that I don't need to know or copy-paste a UUID. + +## Acceptance Criteria + +1. **AC1 - Champ de recherche autocomplete** : Le champ "ID du parent" dans la modale "Ajouter un parent/tuteur" est remplacé par un champ de recherche avec autocomplete. L'admin tape au moins 2 caractères et voit une liste de suggestions de parents correspondants (nom, prénom, email). + +2. **AC2 - Résultats de recherche** : Les suggestions affichent le prénom, nom et email de chaque parent trouvé. Seuls les utilisateurs ayant le rôle `ROLE_PARENT` du même tenant sont proposés. + +3. **AC3 - Sélection** : L'admin clique sur une suggestion pour la sélectionner. Le parent sélectionné est affiché clairement dans le champ (nom + email). L'admin peut le désélectionner pour chercher à nouveau. + +4. **AC4 - Debounce** : Les requêtes de recherche sont debounced (300ms minimum) pour éviter de surcharger l'API. + +5. **AC5 - Feedback** : Un indicateur de chargement s'affiche pendant la recherche. Un message "Aucun parent trouvé" s'affiche si la recherche ne retourne aucun résultat. + +## Tasks / Subtasks + +- [ ] Task 1 - Backend : endpoint de recherche parents (AC: 1, 2) + - [ ] Créer un endpoint GET `/api/parents/search?q={query}` qui retourne les utilisateurs ROLE_PARENT du tenant courant, filtrés par nom/prénom/email + - [ ] Limiter les résultats à 10 suggestions maximum + - [ ] Protéger l'endpoint avec les autorisations admin + +- [ ] Task 2 - Frontend : composant autocomplete (AC: 1, 3, 4, 5) + - [ ] Remplacer le champ UUID dans `GuardianList.svelte` par un composant de recherche autocomplete + - [ ] Implémenter le debounce (300ms) sur la saisie + - [ ] Afficher les résultats dans un dropdown avec prénom, nom, email + - [ ] Permettre la sélection/désélection d'un parent + - [ ] Afficher le loading et l'état vide + +- [ ] Task 3 - Tests E2E (AC: 1-5) + - [ ] Tester la recherche autocomplete sur la fiche élève + - [ ] Tester la liaison parent-élève via le nouveau flux + +## Dev Notes + +### Composants existants à modifier + +- `frontend/src/lib/components/organisms/GuardianList/GuardianList.svelte` : remplacer le champ `` par un composant autocomplete +- Backend : ajouter un provider/endpoint pour la recherche de parents + +### Contexte + +Actuellement, pour lier un parent à un élève depuis la fiche élève (`/admin/students/{id}`), l'admin doit saisir manuellement l'UUID du compte parent. C'est inutilisable en pratique car personne ne connait les UUID par coeur. + +### Contraintes + +- La recherche doit être scoped au tenant courant +- Seuls les utilisateurs avec `ROLE_PARENT` doivent être retournés +- Ne pas proposer les parents déjà liés à cet élève diff --git a/frontend/e2e/schedule-advanced.spec.ts b/frontend/e2e/schedule-advanced.spec.ts index 904b702..26a0a78 100644 --- a/frontend/e2e/schedule-advanced.spec.ts +++ b/frontend/e2e/schedule-advanced.spec.ts @@ -42,10 +42,12 @@ function resolveDeterministicIds(): { schoolId: string; academicYearId: string } const output = execSync( `docker compose -f "${composeFile}" exec -T php php -r '` + `require "/app/vendor/autoload.php"; ` + - `$t="${TENANT_ID}"; $ns="6ba7b814-9dad-11d1-80b4-00c04fd430c8"; ` + - `echo Ramsey\\Uuid\\Uuid::uuid5($ns,"school-$t")->toString()."\\n"; ` + + `$t="${TENANT_ID}"; ` + + `$dns="6ba7b810-9dad-11d1-80b4-00c04fd430c8"; ` + + `$ay="6ba7b814-9dad-11d1-80b4-00c04fd430c8"; ` + + `echo Ramsey\\Uuid\\Uuid::uuid5($dns,"school-$t")->toString()."\\n"; ` + `$m=(int)date("n"); $s=$m>=9?(int)date("Y"):(int)date("Y")-1; $e=$s+1; ` + - `echo Ramsey\\Uuid\\Uuid::uuid5($ns,"$t:$s-$e")->toString();` + + `echo Ramsey\\Uuid\\Uuid::uuid5($ay,"$t:$s-$e")->toString();` + `' 2>&1`, { encoding: 'utf-8' } ).trim(); @@ -147,11 +149,13 @@ async function fillSlotForm( if (className) { await dialog.locator('#slot-class').selectOption({ label: className }); } - // Wait for assignments to load — only the test teacher is assigned, - // so the teacher dropdown filters down to 1 option + // Wait for assignments to load, then select subject first (filters teachers) + const subjectOptions = dialog.locator('#slot-subject option:not([value=""])'); + await expect(subjectOptions.first()).toBeAttached({ timeout: 10000 }); + await dialog.locator('#slot-subject').selectOption({ index: 1 }); + // After subject selection, wait for teacher dropdown to be filtered const teacherOptions = dialog.locator('#slot-teacher option:not([value=""])'); await expect(teacherOptions).toHaveCount(1, { timeout: 10000 }); - await dialog.locator('#slot-subject').selectOption({ index: 1 }); await dialog.locator('#slot-teacher').selectOption({ index: 1 }); await dialog.locator('#slot-day').selectOption(dayValue); await dialog.locator('#slot-start').fill(startTime); @@ -180,39 +184,31 @@ test.describe('Schedule Management - Modification & Conflicts & Calendar (Story const { schoolId, academicYearId } = resolveDeterministicIds(); - // Ensure test classes exist + // Clean up stale test data (e.g. from previous runs with wrong school_id) try { - runSql( - `INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, level, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', '${academicYearId}', 'E2E-Schedule-6A', '6ème', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING` - ); + runSql(`DELETE FROM schedule_slots WHERE class_id IN (SELECT id FROM school_classes WHERE name IN ('E2E-Schedule-6A','E2E-Schedule-5A') AND tenant_id = '${TENANT_ID}')`); + runSql(`DELETE FROM teacher_assignments WHERE school_class_id IN (SELECT id FROM school_classes WHERE name IN ('E2E-Schedule-6A','E2E-Schedule-5A') AND tenant_id = '${TENANT_ID}')`); + runSql(`DELETE FROM school_classes WHERE name IN ('E2E-Schedule-6A','E2E-Schedule-5A') AND tenant_id = '${TENANT_ID}'`); + runSql(`DELETE FROM subjects WHERE code IN ('E2SMATH','E2SFRA') AND tenant_id = '${TENANT_ID}'`); } catch { - // May already exist + // Tables may not exist } - try { - runSql( - `INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, level, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', '${academicYearId}', 'E2E-Schedule-5A', '5ème', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING` - ); - } catch { - // May already exist - } + // Create test classes + runSql( + `INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, level, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', '${academicYearId}', 'E2E-Schedule-6A', '6ème', 'active', NOW(), NOW())` + ); + runSql( + `INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, level, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', '${academicYearId}', 'E2E-Schedule-5A', '5ème', 'active', NOW(), NOW())` + ); - // Ensure test subjects exist - try { - runSql( - `INSERT INTO subjects (id, tenant_id, school_id, name, code, color, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', 'E2E-Schedule-Maths', 'E2ESCHEDMATH', '#3b82f6', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING` - ); - } catch { - // May already exist - } - - try { - runSql( - `INSERT INTO subjects (id, tenant_id, school_id, name, code, color, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', 'E2E-Schedule-Français', 'E2ESCHEDFRA', '#ef4444', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING` - ); - } catch { - // May already exist - } + // Create test subjects + runSql( + `INSERT INTO subjects (id, tenant_id, school_id, name, code, color, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', 'E2E-Schedule-Maths', 'E2SMATH', '#3b82f6', 'active', NOW(), NOW())` + ); + runSql( + `INSERT INTO subjects (id, tenant_id, school_id, name, code, color, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', 'E2E-Schedule-Français', 'E2SFRA', '#ef4444', 'active', NOW(), NOW())` + ); cleanupScheduleData(); cleanupCalendarEntries(); diff --git a/frontend/e2e/schedule.spec.ts b/frontend/e2e/schedule.spec.ts index 3cbb8e0..32386be 100644 --- a/frontend/e2e/schedule.spec.ts +++ b/frontend/e2e/schedule.spec.ts @@ -42,10 +42,12 @@ function resolveDeterministicIds(): { schoolId: string; academicYearId: string } const output = execSync( `docker compose -f "${composeFile}" exec -T php php -r '` + `require "/app/vendor/autoload.php"; ` + - `$t="${TENANT_ID}"; $ns="6ba7b814-9dad-11d1-80b4-00c04fd430c8"; ` + - `echo Ramsey\\Uuid\\Uuid::uuid5($ns,"school-$t")->toString()."\\n"; ` + + `$t="${TENANT_ID}"; ` + + `$dns="6ba7b810-9dad-11d1-80b4-00c04fd430c8"; ` + + `$ay="6ba7b814-9dad-11d1-80b4-00c04fd430c8"; ` + + `echo Ramsey\\Uuid\\Uuid::uuid5($dns,"school-$t")->toString()."\\n"; ` + `$m=(int)date("n"); $s=$m>=9?(int)date("Y"):(int)date("Y")-1; $e=$s+1; ` + - `echo Ramsey\\Uuid\\Uuid::uuid5($ns,"$t:$s-$e")->toString();` + + `echo Ramsey\\Uuid\\Uuid::uuid5($ay,"$t:$s-$e")->toString();` + `' 2>&1`, { encoding: 'utf-8' } ).trim(); @@ -114,11 +116,13 @@ async function fillSlotForm( if (className) { await dialog.locator('#slot-class').selectOption({ label: className }); } - // Wait for assignments to load — only the test teacher is assigned, - // so the teacher dropdown filters down to 1 option + // Wait for assignments to load, then select subject first (filters teachers) + const subjectOptions = dialog.locator('#slot-subject option:not([value=""])'); + await expect(subjectOptions.first()).toBeAttached({ timeout: 10000 }); + await dialog.locator('#slot-subject').selectOption({ index: 1 }); + // After subject selection, wait for teacher dropdown to be filtered const teacherOptions = dialog.locator('#slot-teacher option:not([value=""])'); await expect(teacherOptions).toHaveCount(1, { timeout: 10000 }); - await dialog.locator('#slot-subject').selectOption({ index: 1 }); await dialog.locator('#slot-teacher').selectOption({ index: 1 }); await dialog.locator('#slot-day').selectOption(dayValue); await dialog.locator('#slot-start').fill(startTime); @@ -147,40 +151,31 @@ test.describe('Schedule Management - Navigation & Grid & Creation (Story 4.1)', const { schoolId, academicYearId } = resolveDeterministicIds(); - // Ensure test class exists + // Clean up stale test data (e.g. from previous runs with wrong school_id) try { - runSql( - `INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, level, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', '${academicYearId}', 'E2E-Schedule-6A', '6ème', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING` - ); + runSql(`DELETE FROM schedule_slots WHERE class_id IN (SELECT id FROM school_classes WHERE name IN ('E2E-Schedule-6A','E2E-Schedule-5A') AND tenant_id = '${TENANT_ID}')`); + runSql(`DELETE FROM teacher_assignments WHERE school_class_id IN (SELECT id FROM school_classes WHERE name IN ('E2E-Schedule-6A','E2E-Schedule-5A') AND tenant_id = '${TENANT_ID}')`); + runSql(`DELETE FROM school_classes WHERE name IN ('E2E-Schedule-6A','E2E-Schedule-5A') AND tenant_id = '${TENANT_ID}'`); + runSql(`DELETE FROM subjects WHERE code IN ('E2SMATH','E2SFRA') AND tenant_id = '${TENANT_ID}'`); } catch { - // May already exist + // Tables may not exist } - // Ensure second test class exists (for conflict tests across classes) - try { - runSql( - `INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, level, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', '${academicYearId}', 'E2E-Schedule-5A', '5ème', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING` - ); - } catch { - // May already exist - } + // Create test classes + runSql( + `INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, level, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', '${academicYearId}', 'E2E-Schedule-6A', '6ème', 'active', NOW(), NOW())` + ); + runSql( + `INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, level, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', '${academicYearId}', 'E2E-Schedule-5A', '5ème', 'active', NOW(), NOW())` + ); - // Ensure test subjects exist - try { - runSql( - `INSERT INTO subjects (id, tenant_id, school_id, name, code, color, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', 'E2E-Schedule-Maths', 'E2ESCHEDMATH', '#3b82f6', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING` - ); - } catch { - // May already exist - } - - try { - runSql( - `INSERT INTO subjects (id, tenant_id, school_id, name, code, color, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', 'E2E-Schedule-Français', 'E2ESCHEDFRA', '#ef4444', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING` - ); - } catch { - // May already exist - } + // Create test subjects + runSql( + `INSERT INTO subjects (id, tenant_id, school_id, name, code, color, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', 'E2E-Schedule-Maths', 'E2SMATH', '#3b82f6', 'active', NOW(), NOW())` + ); + runSql( + `INSERT INTO subjects (id, tenant_id, school_id, name, code, color, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', 'E2E-Schedule-Français', 'E2SFRA', '#ef4444', 'active', NOW(), NOW())` + ); cleanupScheduleData(); clearCache(); @@ -375,6 +370,78 @@ test.describe('Schedule Management - Navigation & Grid & Creation (Story 4.1)', await expect(page.locator('.slot-card').getByText('A101')).toBeVisible(); }); + test('subject field appears before teacher field in creation form', async ({ page }) => { + await loginAsAdmin(page); + await page.goto(`${ALPHA_URL}/admin/schedule`); + await waitForScheduleReady(page); + + const timeCell = page.locator('.time-cell').first(); + await timeCell.click(); + + const dialog = page.getByRole('dialog'); + await expect(dialog).toBeVisible({ timeout: 10000 }); + + // Subject should appear before teacher in DOM order + const formGroups = dialog.locator('.form-group'); + const labels = await formGroups.locator('label').allTextContents(); + const subjectIndex = labels.findIndex((l) => l.includes('Matière')); + const teacherIndex = labels.findIndex((l) => l.includes('Enseignant')); + expect(subjectIndex).toBeLessThan(teacherIndex); + }); + + test('selecting a subject filters the teacher dropdown', async ({ page }) => { + const { academicYearId } = resolveDeterministicIds(); + + // Create a second teacher + execSync( + `docker compose -f "${composeFile}" exec -T php php bin/console app:dev:create-test-user --tenant=ecole-alpha --email=e2e-sched-teacher2@example.com --password=Teacher2Pass123 --role=ROLE_PROF 2>&1`, + { encoding: 'utf-8' } + ); + + // Assign teacher1 to subject1, teacher2 to subject2 for class 6A + runSql(`DELETE FROM teacher_assignments WHERE tenant_id = '${TENANT_ID}'`); + runSql( + `INSERT INTO teacher_assignments (id, tenant_id, teacher_id, school_class_id, subject_id, academic_year_id, status, start_date, created_at, updated_at) ` + + `SELECT gen_random_uuid(), '${TENANT_ID}', u.id, c.id, s.id, '${academicYearId}', 'active', NOW(), NOW(), NOW() ` + + `FROM users u, school_classes c, (SELECT id FROM subjects WHERE code = 'E2SMATH' AND tenant_id = '${TENANT_ID}') s ` + + `WHERE u.email = '${TEACHER_EMAIL}' AND u.tenant_id = '${TENANT_ID}' ` + + `AND c.name = 'E2E-Schedule-6A' AND c.tenant_id = '${TENANT_ID}' ` + + `ON CONFLICT DO NOTHING` + ); + runSql( + `INSERT INTO teacher_assignments (id, tenant_id, teacher_id, school_class_id, subject_id, academic_year_id, status, start_date, created_at, updated_at) ` + + `SELECT gen_random_uuid(), '${TENANT_ID}', u.id, c.id, s.id, '${academicYearId}', 'active', NOW(), NOW(), NOW() ` + + `FROM users u, school_classes c, (SELECT id FROM subjects WHERE code = 'E2SFRA' AND tenant_id = '${TENANT_ID}') s ` + + `WHERE u.email = 'e2e-sched-teacher2@example.com' AND u.tenant_id = '${TENANT_ID}' ` + + `AND c.name = 'E2E-Schedule-6A' AND c.tenant_id = '${TENANT_ID}' ` + + `ON CONFLICT DO NOTHING` + ); + clearCache(); + + await loginAsAdmin(page); + await page.goto(`${ALPHA_URL}/admin/schedule`); + await waitForScheduleReady(page); + + const timeCell = page.locator('.time-cell').first(); + await timeCell.click(); + + const dialog = page.getByRole('dialog'); + await expect(dialog).toBeVisible({ timeout: 10000 }); + + // Select class 6A — both subjects should be available + await dialog.locator('#slot-class').selectOption({ label: 'E2E-Schedule-6A' }); + const subjectOptions = dialog.locator('#slot-subject option:not([value=""])'); + await expect(subjectOptions).toHaveCount(2, { timeout: 15000 }); + + // Before selecting a subject, both teachers should be available + const teacherOptions = dialog.locator('#slot-teacher option:not([value=""])'); + await expect(teacherOptions).toHaveCount(2, { timeout: 10000 }); + + // Select first subject (Français) — should filter to only teacher2 + await dialog.locator('#slot-subject').selectOption({ index: 1 }); + await expect(teacherOptions).toHaveCount(1, { timeout: 10000 }); + }); + test('filters subjects and teachers by class assignment', async ({ page }) => { const { academicYearId } = resolveDeterministicIds(); @@ -434,9 +501,10 @@ test.describe('Schedule Recurring - Week Navigation & Scope (Story 4.2)', () => const { schoolId, academicYearId } = resolveDeterministicIds(); + // Only insert if not already created by first describe block try { runSql( - `INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, level, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', '${academicYearId}', 'E2E-Schedule-6A', '6ème', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING` + `INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, level, status, created_at, updated_at) SELECT gen_random_uuid(), '${TENANT_ID}', '${schoolId}', '${academicYearId}', 'E2E-Schedule-6A', '6ème', 'active', NOW(), NOW() WHERE NOT EXISTS (SELECT 1 FROM school_classes WHERE name = 'E2E-Schedule-6A' AND tenant_id = '${TENANT_ID}')` ); } catch { // May already exist @@ -444,7 +512,7 @@ test.describe('Schedule Recurring - Week Navigation & Scope (Story 4.2)', () => try { runSql( - `INSERT INTO subjects (id, tenant_id, school_id, name, code, color, status, created_at, updated_at) VALUES (gen_random_uuid(), '${TENANT_ID}', '${schoolId}', 'E2E-Schedule-Maths', 'E2ESCHEDMATH', '#3b82f6', 'active', NOW(), NOW()) ON CONFLICT DO NOTHING` + `INSERT INTO subjects (id, tenant_id, school_id, name, code, color, status, created_at, updated_at) SELECT gen_random_uuid(), '${TENANT_ID}', '${schoolId}', 'E2E-Schedule-Maths', 'E2SMATH', '#3b82f6', 'active', NOW(), NOW() WHERE NOT EXISTS (SELECT 1 FROM subjects WHERE code = 'E2SMATH' AND tenant_id = '${TENANT_ID}')` ); } catch { // May already exist diff --git a/frontend/src/routes/admin/schedule/+page.svelte b/frontend/src/routes/admin/schedule/+page.svelte index 43310ee..f121c73 100644 --- a/frontend/src/routes/admin/schedule/+page.svelte +++ b/frontend/src/routes/admin/schedule/+page.svelte @@ -142,7 +142,6 @@ if (!classId) return subjects; const filtered = assignments.filter((a) => a.classId === classId); const subjectIds = new Set(filtered.map((a) => a.subjectId)); - if (subjectIds.size === 0) return subjects; return subjects.filter((s) => subjectIds.has(s.id)); }); @@ -154,7 +153,6 @@ ? assignments.filter((a) => a.classId === classId && a.subjectId === formSubjectId) : assignments.filter((a) => a.classId === classId); const teacherIds = new Set(filtered.map((a) => a.teacherId)); - if (teacherIds.size === 0) return teachers; return teachers.filter((t) => teacherIds.has(t.id)); }); @@ -915,16 +913,6 @@ -
- - -
-
+
+ + +
+