feat: Améliorations du breadcrumb
- Labels CCN collés : CCNEAC, CCNPA, CCNSVP (au lieu de CCN EAC, etc.) - Labels dynamiques : récupération automatique des noms de salariés, factures et tickets - Badge Staff : icône Shield sur les routes /staff/* (visible sur liens et dernier segment) - Hook useDynamicLabel pour charger les données en temps réel - Amélioration de l'accessibilité avec aria-label
This commit is contained in:
parent
0f6687a88c
commit
35d5283434
1 changed files with 173 additions and 32 deletions
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { ChevronRight, Home } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { ChevronRight, Home, Shield } from "lucide-react";
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
|
||||
// Mapping des routes vers des labels lisibles
|
||||
const ROUTE_LABELS: Record<string, string> = {
|
||||
|
|
@ -19,9 +19,9 @@ const ROUTE_LABELS: Record<string, string> = {
|
|||
"/vos-documents": "Vos documents",
|
||||
"/vos-acces": "Gestion des accès",
|
||||
"/minima-ccn": "Minima CCN",
|
||||
"/minima-ccn/ccneac": "CCN EAC",
|
||||
"/minima-ccn/ccnpa": "CCN PA",
|
||||
"/minima-ccn/ccnsvp": "CCN SVP",
|
||||
"/minima-ccn/ccneac": "CCNEAC",
|
||||
"/minima-ccn/ccnpa": "CCNPA",
|
||||
"/minima-ccn/ccnsvp": "CCNSVP",
|
||||
"/simulateur": "Simulateur de paie",
|
||||
"/support": "Support",
|
||||
"/debug": "Debug",
|
||||
|
|
@ -37,6 +37,77 @@ const ROUTE_LABELS: Record<string, string> = {
|
|||
"/staff/analytics": "Analytics",
|
||||
};
|
||||
|
||||
// Hook pour récupérer les labels dynamiques
|
||||
function useDynamicLabel(segment: string, currentPath: string): string | null {
|
||||
const [label, setLabel] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Réinitialiser le label si le segment change
|
||||
setLabel(null);
|
||||
|
||||
// Détecter le pattern et récupérer les données
|
||||
const fetchLabel = async () => {
|
||||
try {
|
||||
// Pattern : /salaries/[matricule]
|
||||
if (currentPath.match(/^\/salaries\/[^/]+$/)) {
|
||||
const matricule = segment;
|
||||
const res = await fetch(`/api/salaries?matricule=${matricule}`, {
|
||||
credentials: 'include',
|
||||
cache: 'no-store'
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data.length > 0) {
|
||||
const salarie = data[0];
|
||||
setLabel(`${salarie.prenom} ${salarie.nom}`.trim() || matricule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern : /staff/facturation/[id]
|
||||
else if (currentPath.match(/^\/staff\/facturation\/[^/]+$/)) {
|
||||
const id = segment;
|
||||
const res = await fetch(`/api/staff/facturation/${id}`, {
|
||||
credentials: 'include',
|
||||
cache: 'no-store'
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setLabel(data.period_label || `Facture ${id.substring(0, 8)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern : /support/[id] (ticket)
|
||||
else if (currentPath.match(/^\/support\/[^/]+$/)) {
|
||||
const id = segment;
|
||||
const res = await fetch(`/api/tickets/${id}`, {
|
||||
credentials: 'include',
|
||||
cache: 'no-store'
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setLabel(data.subject || `Ticket #${id.substring(0, 8)}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// En cas d'erreur, ne rien faire (on garde le label par défaut)
|
||||
console.debug('Breadcrumb: impossible de récupérer le label dynamique', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Lancer la récupération seulement si on détecte un pattern connu
|
||||
if (
|
||||
currentPath.match(/^\/salaries\/[^/]+$/) ||
|
||||
currentPath.match(/^\/staff\/facturation\/[^/]+$/) ||
|
||||
currentPath.match(/^\/support\/[^/]+$/)
|
||||
) {
|
||||
fetchLabel();
|
||||
}
|
||||
}, [segment, currentPath]);
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
export default function Breadcrumb() {
|
||||
const pathname = usePathname();
|
||||
|
||||
|
|
@ -45,13 +116,23 @@ export default function Breadcrumb() {
|
|||
if (pathname === "/") return [];
|
||||
|
||||
const segments = pathname.split("/").filter(Boolean);
|
||||
const crumbs: Array<{ label: string; href: string; isLast: boolean }> = [];
|
||||
const crumbs: Array<{
|
||||
label: string;
|
||||
href: string;
|
||||
isLast: boolean;
|
||||
isStaff: boolean;
|
||||
isDynamic: boolean;
|
||||
segment: string;
|
||||
}> = [];
|
||||
|
||||
// Toujours commencer par l'accueil
|
||||
crumbs.push({
|
||||
label: "Accueil",
|
||||
href: "/",
|
||||
isLast: false,
|
||||
isStaff: false,
|
||||
isDynamic: false,
|
||||
segment: "",
|
||||
});
|
||||
|
||||
// Construire le breadcrumb segment par segment
|
||||
|
|
@ -59,13 +140,24 @@ export default function Breadcrumb() {
|
|||
segments.forEach((segment, index) => {
|
||||
currentPath += `/${segment}`;
|
||||
const isLast = index === segments.length - 1;
|
||||
const isStaff = currentPath.startsWith("/staff");
|
||||
|
||||
// Détection des patterns spéciaux (IDs, matricules, etc.)
|
||||
let label = ROUTE_LABELS[currentPath];
|
||||
let isDynamic = false;
|
||||
|
||||
if (!label) {
|
||||
// Vérifier si c'est un pattern dynamique connu
|
||||
if (
|
||||
currentPath.match(/^\/salaries\/[^/]+$/) ||
|
||||
currentPath.match(/^\/staff\/facturation\/[^/]+$/) ||
|
||||
currentPath.match(/^\/support\/[^/]+$/)
|
||||
) {
|
||||
isDynamic = true;
|
||||
label = "Chargement..."; // Temporaire, sera remplacé par le hook
|
||||
}
|
||||
// Si c'est un UUID ou un nombre (probablement un ID)
|
||||
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment)) {
|
||||
else if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment)) {
|
||||
label = "Détails";
|
||||
} else if (/^\d+$/.test(segment)) {
|
||||
label = `#${segment}`;
|
||||
|
|
@ -81,6 +173,9 @@ export default function Breadcrumb() {
|
|||
label,
|
||||
href: currentPath,
|
||||
isLast,
|
||||
isStaff,
|
||||
isDynamic,
|
||||
segment,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -96,32 +191,78 @@ export default function Breadcrumb() {
|
|||
className="flex items-center gap-2 text-sm mb-4 px-1"
|
||||
>
|
||||
{breadcrumbs.map((crumb, index) => (
|
||||
<div key={crumb.href} className="flex items-center gap-2">
|
||||
{index > 0 && (
|
||||
<ChevronRight className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" aria-hidden />
|
||||
)}
|
||||
{crumb.isLast ? (
|
||||
<span className="text-slate-700 font-medium truncate max-w-[200px]">
|
||||
{crumb.label}
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
href={crumb.href}
|
||||
className="text-slate-500 hover:text-indigo-600 transition truncate max-w-[150px] flex items-center gap-1.5"
|
||||
title={crumb.label}
|
||||
>
|
||||
{index === 0 ? (
|
||||
<>
|
||||
<Home className="w-4 h-4 flex-shrink-0" aria-label="Accueil" />
|
||||
<span className="hidden sm:inline">Accueil</span>
|
||||
</>
|
||||
) : (
|
||||
crumb.label
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<BreadcrumbItem
|
||||
key={crumb.href}
|
||||
crumb={crumb}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
// Composant pour un item de breadcrumb (permet d'utiliser le hook dynamique)
|
||||
function BreadcrumbItem({
|
||||
crumb,
|
||||
index
|
||||
}: {
|
||||
crumb: {
|
||||
label: string;
|
||||
href: string;
|
||||
isLast: boolean;
|
||||
isStaff: boolean;
|
||||
isDynamic: boolean;
|
||||
segment: string;
|
||||
};
|
||||
index: number;
|
||||
}) {
|
||||
// Récupérer le label dynamique si nécessaire
|
||||
const dynamicLabel = useDynamicLabel(
|
||||
crumb.isDynamic ? crumb.segment : "",
|
||||
crumb.isDynamic ? crumb.href : ""
|
||||
);
|
||||
|
||||
// Utiliser le label dynamique si disponible, sinon le label statique
|
||||
const displayLabel = dynamicLabel || crumb.label;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{index > 0 && (
|
||||
<ChevronRight className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" aria-hidden />
|
||||
)}
|
||||
{crumb.isLast ? (
|
||||
<span className="text-slate-700 font-medium truncate max-w-[200px] flex items-center gap-1.5">
|
||||
{displayLabel}
|
||||
{crumb.isStaff && (
|
||||
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-semibold bg-indigo-100 text-indigo-700 border border-indigo-200">
|
||||
<Shield className="w-3 h-3" />
|
||||
Staff
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
href={crumb.href}
|
||||
className="text-slate-500 hover:text-indigo-600 transition truncate max-w-[150px] flex items-center gap-1.5"
|
||||
title={displayLabel}
|
||||
>
|
||||
{index === 0 ? (
|
||||
<>
|
||||
<Home className="w-4 h-4 flex-shrink-0" aria-label="Accueil" />
|
||||
<span className="hidden sm:inline">Accueil</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{displayLabel}
|
||||
{crumb.isStaff && (
|
||||
<span className="inline-flex items-center gap-0.5 px-1 py-0.5 rounded text-xs font-semibold bg-indigo-100 text-indigo-700 border border-indigo-200">
|
||||
<Shield className="w-2.5 h-2.5" />
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue