Verifying the rendered pages rather than the build turned up real bugs: - The WP-00 health page still sat at src/app/page.tsx and silently won the route over the new Aperçu screen, so the home page was a database status readout. Moved to /api/sante, where a probe belongs, and wired into the compose healthcheck. - The unassigned row showed a +14 h delta against a contract of zero, reading as an overshoot when it is simply the volume left to staff. It now shows what there is to fill. - Two sidebar entries lit at once: an anchor link matched its own page, and /equipe matched an employee record. Highlighting now resolves to the most specific match, and a test asserts exactly one entry lights per screen. - Section tabs with no built screen pointed at the home page, which reads as a broken tab. They now lead to their first entry's placeholder. Also gives truncated compliance alerts a title attribute, so a narrow cell no longer says there is a problem without saying which. Playwright can reuse a preinstalled browser through PLAYWRIGHT_CHROMIUM_PATH when its revision differs from the bundled one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
104 lines
2.9 KiB
TypeScript
104 lines
2.9 KiB
TypeScript
import { posteShort, posteTokens, type PosteCode } from '@/lib/design/postes';
|
||
import { cx } from '@/lib/cx';
|
||
|
||
/**
|
||
* États d'un créneau, repris du design system.
|
||
*
|
||
* `alert` ajoute un liseré danger de 1,5 px : la couleur du poste occupe déjà
|
||
* le fond, l'alerte doit donc passer par la forme, pas par une seconde teinte
|
||
* qui entrerait en concurrence avec la palette catégorielle.
|
||
*/
|
||
export type ShiftState =
|
||
| 'published'
|
||
| 'validated'
|
||
| 'draft'
|
||
| 'unpublished'
|
||
| 'alert'
|
||
| 'unassigned';
|
||
|
||
export interface ShiftChipProps {
|
||
poste: PosteCode;
|
||
/** « 09:00–17:00 ». */
|
||
time: string;
|
||
state?: ShiftState;
|
||
/** Écart affiché en pastille, ex. « +2 h 55 ». */
|
||
delta?: string;
|
||
/** Motif de l'alerte, ex. « Repos < 11 h ». */
|
||
alert?: string;
|
||
/** Durée de pause, en minutes. */
|
||
breakMinutes?: number;
|
||
className?: string;
|
||
}
|
||
|
||
export function ShiftChip({
|
||
poste,
|
||
time,
|
||
state = 'published',
|
||
delta,
|
||
alert,
|
||
breakMinutes,
|
||
className,
|
||
}: ShiftChipProps) {
|
||
const tokens = posteTokens(poste);
|
||
const ghost = state === 'unassigned';
|
||
|
||
return (
|
||
<div
|
||
className={cx(
|
||
'flex min-w-0 flex-col gap-px rounded-2 px-1.5 py-1 text-micro',
|
||
state === 'draft' && 'border-dashed',
|
||
className,
|
||
)}
|
||
style={{
|
||
background: ghost ? 'transparent' : tokens.bg,
|
||
color: ghost ? 'var(--color-ink-2)' : tokens.fg,
|
||
border:
|
||
state === 'alert'
|
||
? '1.5px solid var(--color-danger)'
|
||
: ghost
|
||
? `1px dashed ${tokens.edge}`
|
||
: state === 'draft'
|
||
? `1px dashed ${tokens.edge}`
|
||
: state === 'unpublished'
|
||
? `1px dotted ${tokens.edge}`
|
||
: '1px solid transparent',
|
||
}}
|
||
>
|
||
<span className="flex items-center gap-1">
|
||
{/* Le code du poste est le second canal : la couleur ne suffit pas. */}
|
||
<span className="font-semibold tracking-wide">{posteShort(poste)}</span>
|
||
{state === 'validated' ? (
|
||
<span className="text-ok" title="Validé" aria-label="Validé">
|
||
✓
|
||
</span>
|
||
) : null}
|
||
{delta ? (
|
||
<span className="tnum ml-auto rounded-full bg-warn-soft px-1 font-semibold text-warn-soft-ink">
|
||
{delta}
|
||
</span>
|
||
) : null}
|
||
</span>
|
||
|
||
<span className="tnum truncate">
|
||
{time}
|
||
{breakMinutes ? (
|
||
<span className="opacity-70"> ({breakMinutes} mn)</span>
|
||
) : null}
|
||
</span>
|
||
|
||
{alert ? (
|
||
// Le motif est tronqué dans une cellule étroite : le `title` garde le
|
||
// texte complet accessible, sinon l'alerte dit qu'il y a un problème
|
||
// sans jamais dire lequel.
|
||
<span
|
||
className="mt-px flex items-center gap-1 font-medium text-danger-soft-ink"
|
||
title={alert}
|
||
>
|
||
<span aria-hidden>▲</span>
|
||
<span className="truncate">{alert}</span>
|
||
</span>
|
||
) : null}
|
||
</div>
|
||
);
|
||
}
|