The page opens on the gesture the shop actually makes: changing today's hours from a phone, behind the counter. "Closed today", "opens later at" and "closes earlier at" are one tap plus a time, and each shows the hours it would produce before it is applied. The derivation is the delicate part and is pure and tested. Opening at 14:00 drops a 10:00-13:00 morning rather than keeping it, and trims the slot the new time falls inside rather than dropping it. Closing early is the mirror. Both are computed against what the day would normally be, ignoring any exception already recorded, since that is what "late" is late relative to. The upcoming list is built by resolving each of the next sixty days, not by reading the exception table. Vacations and public holidays are never materialised as rows, so the table alone would quietly omit most of what is actually in effect. Each entry is badged with the rule that produced it, and only stored exceptions offer a delete. Ranges are capped at 92 days and point at the holidays page beyond that: a typo in a year field should produce an error, not thirty thousand rows. Editing the French note clears the English one. A translation of text that has changed is worse than no translation. Three exports were removed from the actions module before committing: every export in a 'use server' file becomes a publicly callable endpoint, and those three had ended up unused. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
79 lines
2.7 KiB
TypeScript
79 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' },
|
|
];
|
|
|
|
/**
|
|
* 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>
|
|
);
|
|
}
|