feat: Permettre la définition d'une semaine type récurrente pour l'emploi du temps
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled

Les administrateurs devaient recréer manuellement l'emploi du temps chaque
semaine. Cette implémentation introduit un système de récurrence hebdomadaire
avec gestion des exceptions par occurrence, permettant de modifier ou annuler
un cours spécifique sans affecter les autres semaines.

Le ScheduleResolver calcule dynamiquement l'EDT réel en combinant les créneaux
récurrents, les exceptions ponctuelles et le calendrier scolaire (vacances/fériés).
This commit is contained in:
2026-03-04 20:03:12 +01:00
parent e156755b86
commit ae640e91ac
35 changed files with 3550 additions and 81 deletions

View File

@@ -120,11 +120,18 @@ async function waitForScheduleReady(page: import('@playwright/test').Page) {
timeout: 15000
});
// Wait for either the grid or the empty state to appear
await expect(page.locator('.schedule-grid, .empty-state, .alert-error')).toBeVisible({
await expect(page.locator('.schedule-grid, .empty-state')).toBeVisible({
timeout: 15000
});
}
async function chooseScopeAndEdit(page: import('@playwright/test').Page) {
// Scope modal appears - choose "this occurrence"
const scopeModal = page.getByRole('dialog');
await expect(scopeModal).toBeVisible({ timeout: 10000 });
await scopeModal.getByText('Cette occurrence uniquement').click();
}
async function fillSlotForm(
dialog: import('@playwright/test').Locator,
options: {
@@ -228,7 +235,7 @@ test.describe('Schedule Management - Modification & Conflicts & Calendar (Story
// AC3: Slot Modification & Deletion
// ==========================================================================
test.describe('AC3: Slot Modification & Deletion', () => {
test('clicking a slot opens edit modal', async ({ page }) => {
test('clicking a slot opens scope modal then edit modal', async ({ page }) => {
await loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin/schedule`);
@@ -246,11 +253,13 @@ test.describe('Schedule Management - Modification & Conflicts & Calendar (Story
await dialog.getByRole('button', { name: /créer/i }).click();
await expect(dialog).not.toBeVisible({ timeout: 10000 });
// Click on the created slot
// Click on the created slot — scope modal should appear
const slotCard = page.locator('.slot-card').first();
await expect(slotCard).toBeVisible({ timeout: 10000 });
await slotCard.click();
await chooseScopeAndEdit(page);
// Edit modal should appear
const editDialog = page.getByRole('dialog');
await expect(editDialog).toBeVisible({ timeout: 10000 });
@@ -277,17 +286,24 @@ test.describe('Schedule Management - Modification & Conflicts & Calendar (Story
await dialog.getByRole('button', { name: /créer/i }).click();
await expect(dialog).not.toBeVisible({ timeout: 10000 });
// Click on the slot to edit
// Click on the slot — scope modal then edit modal
const slotCard = page.locator('.slot-card').first();
await expect(slotCard).toBeVisible({ timeout: 10000 });
await slotCard.click();
await chooseScopeAndEdit(page);
dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible({ timeout: 10000 });
// Click delete button
// Click delete button — opens scope modal again for delete scope
await dialog.getByRole('button', { name: /supprimer/i }).click();
// Scope modal appears for delete action
const scopeModal = page.locator('.modal-scope');
await expect(scopeModal).toBeVisible({ timeout: 10000 });
await scopeModal.getByText('Cette occurrence uniquement').click();
// Confirmation modal should appear
const deleteModal = page.getByRole('alertdialog');
await expect(deleteModal).toBeVisible({ timeout: 10000 });
@@ -323,9 +339,11 @@ test.describe('Schedule Management - Modification & Conflicts & Calendar (Story
await expect(slotCard).toBeVisible({ timeout: 10000 });
await expect(slotCard.getByText('A101')).toBeVisible();
// Click to open edit modal
// Click to open scope modal then edit modal
await slotCard.click();
await chooseScopeAndEdit(page);
dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible({ timeout: 10000 });
await expect(
@@ -520,5 +538,61 @@ test.describe('Schedule Management - Modification & Conflicts & Calendar (Story
const dialog = page.getByRole('dialog');
await expect(dialog).not.toBeVisible({ timeout: 3000 });
});
test('recurring slots do not appear on vacation days in next week (AC4)', async ({
page
}) => {
// Compute next week Thursday (use local format to avoid UTC shift)
const thisThursday = new Date(getWeekdayInCurrentWeek(4) + 'T00:00:00');
const nextThursday = new Date(thisThursday);
nextThursday.setDate(thisThursday.getDate() + 7);
const y = nextThursday.getFullYear();
const m = String(nextThursday.getMonth() + 1).padStart(2, '0');
const d = String(nextThursday.getDate()).padStart(2, '0');
const nextThursdayStr = `${y}-${m}-${d}`;
// Clean calendar entries and seed a vacation on next Thursday only
cleanupCalendarEntries();
seedBlockedDate(nextThursdayStr, 'Vacances AC4', 'vacation');
clearCache();
await loginAsAdmin(page);
await page.goto(`${ALPHA_URL}/admin/schedule`);
await waitForScheduleReady(page);
// Create a Thursday slot (if none exists) so we have a recurring slot on Thursday
const dayColumns = page.locator('.day-column');
// Check if Thursday column (index 3) already has a slot
const thursdaySlots = dayColumns.nth(3).locator('.slot-card');
const existingCount = await thursdaySlots.count();
if (existingCount === 0) {
// Create a slot on Thursday
const thursdayCell = dayColumns.nth(3).locator('.time-cell').first();
await thursdayCell.click();
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible({ timeout: 10000 });
await fillSlotForm(dialog, { dayValue: '4', startTime: '09:00', endTime: '10:00' });
await dialog.getByRole('button', { name: /créer/i }).click();
await expect(dialog).not.toBeVisible({ timeout: 10000 });
await waitForScheduleReady(page);
}
// Verify slot is visible on current week's Thursday
await expect(dayColumns.nth(3).locator('.slot-card').first()).toBeVisible({
timeout: 10000
});
// Navigate to next week
await page.getByRole('button', { name: 'Semaine suivante' }).click();
await waitForScheduleReady(page);
// Next week Thursday should be blocked
await expect(dayColumns.nth(3)).toHaveClass(/day-blocked/, { timeout: 10000 });
// No slot card should appear on the blocked Thursday
await expect(dayColumns.nth(3).locator('.slot-card')).toHaveCount(0);
});
});
});