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
142 lines
4.5 KiB
TypeScript
142 lines
4.5 KiB
TypeScript
/**
|
|
* Modèle de navigation, repris de l'export Claude Design.
|
|
*
|
|
* Deux niveaux : une section dans l'en-tête, ses entrées dans la barre
|
|
* latérale. Les badges portent un décompte à traiter.
|
|
*
|
|
* Les entrées sans `href` correspondent à des écrans du périmètre qui n'ont pas
|
|
* encore d'implémentation : elles restent visibles pour que la navigation
|
|
* reflète le produit réel, et mènent à un écran d'attente explicite plutôt qu'à
|
|
* une page blanche.
|
|
*/
|
|
|
|
export interface NavItem {
|
|
id: string;
|
|
label: string;
|
|
href?: string;
|
|
badge?: string;
|
|
}
|
|
|
|
export interface NavSection {
|
|
id: string;
|
|
label: string;
|
|
items: NavItem[];
|
|
}
|
|
|
|
export const NAVIGATION: NavSection[] = [
|
|
{
|
|
id: 'apercu',
|
|
label: 'Aperçu',
|
|
items: [
|
|
{ id: 'taches', label: 'Tâches RH', href: '/' },
|
|
{ id: 'entrees', label: 'Entrées et sorties' },
|
|
{ id: 'profils', label: 'Profils incomplets', badge: '6' },
|
|
{ id: 'soldes-cp', label: 'Compteurs de congés payés' },
|
|
],
|
|
},
|
|
{
|
|
id: 'plannings',
|
|
label: 'Plannings',
|
|
items: [
|
|
{ id: 'semaine', label: 'Vue semaine', href: '/planning/semaine' },
|
|
{ id: 'jour', label: 'Vue jour', href: '/planning/jour' },
|
|
{ id: 'besoins', label: 'Besoins non couverts', badge: '5' },
|
|
{ id: 'modeles', label: "Modèles d'ouverture" },
|
|
],
|
|
},
|
|
{
|
|
id: 'equipe',
|
|
label: 'Équipe',
|
|
items: [
|
|
{ id: 'membres', label: 'Membres', href: '/equipe' },
|
|
{ id: 'fiche', label: 'Fiche salarié', href: '/equipe/camille-ferrand' },
|
|
{ id: 'contrats', label: 'Modifications de contrat' },
|
|
],
|
|
},
|
|
{
|
|
id: 'conges',
|
|
label: 'Congés',
|
|
items: [
|
|
{ id: 'calendrier', label: 'Calendrier des absences', href: '/conges' },
|
|
{ id: 'attente', label: 'Demandes en attente', href: '/conges#attente', badge: '3' },
|
|
{ id: 'politiques', label: 'Politiques de congés' },
|
|
],
|
|
},
|
|
{
|
|
id: 'rapports',
|
|
label: 'Rapports',
|
|
items: [
|
|
{ id: 'heures', label: 'Heures travaillées' },
|
|
{ id: 'paie', label: 'Préparation de paie' },
|
|
{ id: 'activite', label: "Journal d'activité" },
|
|
],
|
|
},
|
|
{
|
|
id: 'reglages',
|
|
label: 'Réglages',
|
|
items: [
|
|
{ id: 'convention', label: 'Convention collective' },
|
|
{ id: 'postes', label: 'Postes et étiquettes' },
|
|
{ id: 'sites', label: 'Établissements' },
|
|
{ id: 'roles', label: 'Rôles et permissions' },
|
|
],
|
|
},
|
|
];
|
|
|
|
/** Section contenant la route donnée, ou la première par défaut. */
|
|
export function sectionForPath(pathname: string): NavSection {
|
|
const active = activeItem(pathname);
|
|
const match = active
|
|
? NAVIGATION.find((section) =>
|
|
section.items.some((item) => item.id === active.id),
|
|
)
|
|
: undefined;
|
|
return match ?? (NAVIGATION[0] as NavSection);
|
|
}
|
|
|
|
/** Chemin vers l'écran d'attente d'une entrée non encore construite. */
|
|
export function placeholderHref(label: string): string {
|
|
return `/bientot?ecran=${encodeURIComponent(label)}`;
|
|
}
|
|
|
|
/**
|
|
* Destination d'un onglet de section.
|
|
*
|
|
* Une section dont aucun écran n'est construit ne doit pas renvoyer à
|
|
* l'accueil : le clic donnerait l'impression que l'onglet est cassé. Elle mène
|
|
* à l'écran d'attente de sa première entrée, qui dit ce qui viendra là.
|
|
*/
|
|
export function sectionHref(section: NavSection): string {
|
|
const built = section.items.find((item) => item.href);
|
|
if (built?.href) return built.href;
|
|
const first = section.items[0];
|
|
return first ? placeholderHref(first.label) : '/';
|
|
}
|
|
|
|
export function matches(href: string, pathname: string): boolean {
|
|
// Un lien d'ancre pointe à l'intérieur d'une page déjà représentée par une
|
|
// autre entrée : il ne concourt pas.
|
|
if (href.includes('#')) return false;
|
|
|
|
if (href === '/') return pathname === '/';
|
|
return pathname === href || pathname.startsWith(`${href}/`);
|
|
}
|
|
|
|
/**
|
|
* Entrée à mettre en évidence pour une route donnée.
|
|
*
|
|
* `/equipe` et `/equipe/camille-ferrand` correspondent tous deux quand on est
|
|
* sur une fiche. On retient la **plus spécifique** : deux entrées allumées à la
|
|
* fois, et le repère de position ne repère plus rien.
|
|
*/
|
|
export function activeItem(pathname: string): NavItem | undefined {
|
|
return NAVIGATION.flatMap((section) => section.items)
|
|
.filter((item) => item.href && matches(item.href, pathname))
|
|
.sort((a, b) => (b.href as string).length - (a.href as string).length)[0];
|
|
}
|
|
|
|
export function isActive(href: string, pathname: string): boolean {
|
|
const active = activeItem(pathname);
|
|
return active?.href === href;
|
|
}
|