Opening later and closing earlier were fixed to today, which was an assumption rather than a requirement: closing next Thursday afternoon is the same decision as closing this one, and the shop knows about it in advance more often than not. The date is a single control and still defaults to today, so the two-click path for the most frequent case is unchanged. The usual hours differ from one weekday to the next, and "opens later" is late relative to them, so the whole sixty-day window is resolved on the server and handed over at once. Sixty small entries cost less than a round trip per change of date. The date control's accessible name is explicit: "du" alone collided with the "Du" field of the range form below it, which the end-to-end run caught as an ambiguous locator — and an ambiguous locator is an ambiguous label for anyone using a screen reader. The end-to-end suite ran before this was pushed, unlike last time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
111 lines
3.8 KiB
TypeScript
111 lines
3.8 KiB
TypeScript
import { auth } from '@/lib/auth';
|
|
import { prisma } from '@/lib/db';
|
|
import { addCivilDays, toCivilDate } from '@/lib/schedule/civil';
|
|
import { loadScheduleContext, loadSettings } from '@/lib/schedule/context';
|
|
import { formatDateFr, formatHoursText } from '@/lib/schedule/format';
|
|
import { resolveCivilDay } from '@/lib/schedule/resolver';
|
|
import type { ScheduleContext } from '@/lib/schedule/types';
|
|
|
|
import ExceptionsManager, { type SelectableDay, type UpcomingDay } from './ExceptionsManager';
|
|
|
|
export const metadata = { title: 'Changements ponctuels — ITA ITO' };
|
|
|
|
/** How far ahead the list looks. Far enough for a season, short enough to read. */
|
|
const HORIZON_DAYS = 60;
|
|
|
|
export default async function ExceptionsPage() {
|
|
const [session, settings] = await Promise.all([auth(), loadSettings()]);
|
|
const now = new Date();
|
|
const context = await loadScheduleContext(now, settings);
|
|
const today = toCivilDate(now, settings.timezone);
|
|
|
|
const stored = await prisma.scheduleException.findMany({
|
|
where: { date: { gte: new Date(`${today}T00:00:00.000Z`) } },
|
|
select: { date: true },
|
|
});
|
|
const deletable = new Set(stored.map((row) => row.date.toISOString().slice(0, 10)));
|
|
|
|
const upcoming = collectUpcoming(today, context, deletable);
|
|
const days = collectSelectableDays(today, context);
|
|
|
|
return (
|
|
<main className="mx-auto max-w-3xl px-4 py-10">
|
|
<h1 className="text-2xl">Changements ponctuels</h1>
|
|
<p className="mt-2 max-w-prose text-[var(--ink-muted)]">
|
|
Une fermeture ou un horaire différent pour une date précise. Ces changements priment sur les
|
|
horaires fixes.
|
|
</p>
|
|
|
|
<ExceptionsManager
|
|
canEdit={session?.user?.role === 'admin'}
|
|
today={today}
|
|
days={days}
|
|
upcoming={upcoming}
|
|
/>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* Built by resolving each day rather than by listing the exception table,
|
|
* because vacations and public holidays are never materialised as rows — so
|
|
* the table alone would quietly omit most of what is actually in effect.
|
|
*/
|
|
function collectUpcoming(
|
|
today: string,
|
|
context: ScheduleContext,
|
|
deletable: Set<string>,
|
|
): UpcomingDay[] {
|
|
const days: UpcomingDay[] = [];
|
|
|
|
for (let offset = 0; offset <= HORIZON_DAYS; offset += 1) {
|
|
const date = addCivilDays(today, offset);
|
|
const day = resolveCivilDay(date, context);
|
|
if (!day.isException) {
|
|
continue;
|
|
}
|
|
days.push({
|
|
date,
|
|
label: formatDateFr(date),
|
|
isToday: offset === 0,
|
|
isClosed: !day.isOpen,
|
|
hoursText: day.slots.length > 0 ? formatHoursText(day.slots) : 'Fermé',
|
|
kind: day.exceptionKind ?? 'MANUAL',
|
|
note: day.noteFr,
|
|
canDelete: deletable.has(date),
|
|
});
|
|
}
|
|
|
|
return days;
|
|
}
|