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 @@ -