- La boutique est déjà fermée aujourd’hui : pour une ouverture exceptionnelle,
- utilisez le formulaire ci-dessous.
+ La boutique est déjà fermée {when} : pour une ouverture exceptionnelle, utilisez le
+ formulaire ci-dessous.
)}
diff --git a/app/admin/exceptions/page.tsx b/app/admin/exceptions/page.tsx
index 376836d..189ffc7 100644
--- a/app/admin/exceptions/page.tsx
+++ b/app/admin/exceptions/page.tsx
@@ -6,7 +6,7 @@ import { formatDateFr, formatHoursText } from '@/lib/schedule/format';
import { resolveCivilDay } from '@/lib/schedule/resolver';
import type { ScheduleContext } from '@/lib/schedule/types';
-import ExceptionsManager, { type UpcomingDay } from './ExceptionsManager';
+import ExceptionsManager, { type SelectableDay, type UpcomingDay } from './ExceptionsManager';
export const metadata = { title: 'Changements ponctuels — ITA ITO' };
@@ -26,11 +26,7 @@ export default async function ExceptionsPage() {
const deletable = new Set(stored.map((row) => row.date.toISOString().slice(0, 10)));
const upcoming = collectUpcoming(today, context, deletable);
-
- // What today would normally be, ignoring any exception already recorded:
- // that is what "opening late" is late relative to.
- const usual = resolveCivilDay(today, { ...context, exceptions: [] });
- const effective = resolveCivilDay(today, context);
+ const days = collectSelectableDays(today, context);
return (
@@ -43,17 +39,41 @@ export default async function ExceptionsPage() {
0 ? formatHoursText(usual.slots) : 'Fermé'}
- effectiveText={effective.slots.length > 0 ? formatHoursText(effective.slots) : 'Fermé'}
- todayIsException={effective.isException}
+ days={days}
upcoming={upcoming}
/>
);
}
+/**
+ * Each day the quick actions can target, with the hours it would normally
+ * have.
+ *
+ * "Opens later" is late relative to the day's usual hours, and those differ
+ * from one weekday to the next — so the whole window is resolved here rather
+ * than asking the server again every time the date changes. Sixty small
+ * entries cost less than a round trip per keystroke.
+ */
+function collectSelectableDays(today: string, context: ScheduleContext): SelectableDay[] {
+ const withoutExceptions: ScheduleContext = { ...context, exceptions: [] };
+
+ return Array.from({ length: HORIZON_DAYS + 1 }, (_, offset) => {
+ const date = addCivilDays(today, offset);
+ const usual = resolveCivilDay(date, withoutExceptions);
+ const effective = resolveCivilDay(date, context);
+
+ return {
+ date,
+ label: formatDateFr(date),
+ usualSlots: usual.slots,
+ usualText: usual.slots.length > 0 ? formatHoursText(usual.slots) : 'Fermé',
+ effectiveText: effective.slots.length > 0 ? formatHoursText(effective.slots) : 'Fermé',
+ isException: effective.isException,
+ };
+ });
+}
+
/**
* Every day ahead that departs from the reference week, whatever the cause.
*
diff --git a/e2e/schedule.spec.ts b/e2e/schedule.spec.ts
index df46658..c66b1eb 100644
--- a/e2e/schedule.spec.ts
+++ b/e2e/schedule.spec.ts
@@ -18,7 +18,7 @@ test('changing today’s hours reaches the panel', async ({ page, request }) =>
await page.goto('/admin/exceptions');
await page.getByRole('button', { name: /Fermé aujourd’hui/ }).click();
- await expect(page.getByText(/La boutique est fermée aujourd’hui/)).toBeVisible();
+ await expect(page.getByRole('status')).toContainText(/sera fermée aujourd’hui/);
// Stored as a manual exception for today, in the shop's timezone.
// Read as text: node-postgres parses a DATE column into a local-midnight
@@ -37,6 +37,26 @@ test('changing today’s hours reaches the panel', async ({ page, request }) =>
await expect(page.getByText(/aujourd’hui/).first()).toBeVisible();
});
+test('a quick change can target a day other than today', async ({ page }) => {
+ const target = today(3);
+
+ await signIn(page, 'admin');
+ await page.goto('/admin/exceptions');
+
+ // The date defaults to today; closing next Thursday must not cost more
+ // clicks than closing this afternoon.
+ await page.getByLabel('Jour à modifier').selectOption(target);
+ await page.getByRole('button', { name: /^Fermé le / }).click();
+
+ await expect(page.getByRole('status')).toContainText(/sera fermée/);
+
+ const [exception] = await query<{ date: string; isClosed: boolean }>(
+ 'SELECT date::text AS date, "isClosed" FROM schedule_exceptions',
+ );
+ expect(exception?.date).toBe(target);
+ expect(exception?.isClosed).toBe(true);
+});
+
test('the reference week can be edited and shows what will change', async ({ page }) => {
await signIn(page, 'admin');
await page.goto('/admin/horaires');