Files
ita-ito-horaires/app/admin/layout.tsx
T
vliaudatandClaude Opus 5 c430205bf2 feat: add the closure periods page
Create a period with a start, an end and a label, see the twelve months
ahead at a glance, delete one. The label is required and trimmed: it
goes straight onto the shop window.

Overlapping periods are refused, and the message names the one they
collide with. Two rows covering the same day would both be "in effect"
with no way to say which, and silently merging them would lose whichever
label the owner meant. Periods that merely touch — one ending the 10th,
the next starting the 11th — are fine.

The year view exists because a list of date ranges is precise and hard
to picture, while "have I left a gap in August?" is the question people
actually ask. It renders on the server; it only changes when the data
does.

A vacation period stays a source of truth and is never expanded into
exception rows, so a one-off exception placed inside one still wins and
nothing is overwritten. The page says so rather than leaving it to be
discovered.

The first version of the calendar built the months but never applied the
periods to them, so no closure would ever have shown. Caught before
commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
2026-09-20 20:10:58 +02:00

80 lines
2.7 KiB
TypeScript

import Image from 'next/image';
import Link from 'next/link';
import { auth, signOut } from '@/lib/auth';
/** Extended as each page lands, so nothing in the bar leads nowhere. */
const NAV = [
{ href: '/admin', label: 'Tableau de bord' },
{ href: '/admin/horaires', label: 'Horaires' },
{ href: '/admin/exceptions', label: 'Exceptions' },
{ href: '/admin/vacances', label: 'Vacances' },
];
/**
* The admin shell.
*
* The middleware has already established that there is a session by the time
* this renders; the call here is only to know who it is. Navigation links are
* added as their pages land, so nothing in the bar leads nowhere.
*/
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
const session = await auth();
const email = session?.user?.email ?? '';
const isAdmin = session?.user?.role === 'admin';
return (
<div className="min-h-dvh">
<header className="border-b border-[var(--line)] bg-[var(--surface)]">
<div className="mx-auto flex max-w-5xl flex-wrap items-center gap-x-6 gap-y-3 px-4 py-3">
<Link href="/admin" className="flex items-center" aria-label="Tableau de bord">
<Image
src="/brand/logo.png"
alt="ITA ITO"
width={406}
height={194}
className="h-auto w-24"
/>
</Link>
<nav aria-label="Sections" className="flex flex-wrap items-center gap-x-5 gap-y-1">
{NAV.map((item) => (
<Link
key={item.href}
href={item.href}
className="text-sm text-[var(--ink-muted)] underline-offset-4 transition-colors hover:text-[var(--ink)] hover:underline"
>
{item.label}
</Link>
))}
</nav>
<div className="ml-auto flex items-center gap-3 text-sm">
{!isAdmin ? (
<span className="rounded-[var(--radius-sm)] bg-[var(--surface-muted)] px-2 py-1 text-xs text-[var(--ink-muted)]">
Lecture seule
</span>
) : null}
<span className="hidden text-[var(--ink-muted)] sm:inline">{email}</span>
<form
action={async () => {
'use server';
await signOut({ redirectTo: '/login' });
}}
>
<button
type="submit"
className="rounded-[var(--radius-sm)] border border-[var(--line-strong)] px-3 py-1.5 text-sm transition-colors hover:bg-[var(--surface-muted)]"
>
Se déconnecter
</button>
</form>
</div>
</div>
</header>
{children}
</div>
);
}