633 lines
29 KiB
TypeScript
633 lines
29 KiB
TypeScript
"use client";
|
||
import { useEffect, useState, Fragment } from "react";
|
||
import { Command } from "cmdk";
|
||
import { useQuery } from "@tanstack/react-query";
|
||
|
||
|
||
// Petit hook de debounce pour éviter de spammer /api/search à chaque frappe
|
||
function useDebouncedValue<T>(value: T, delay = 300) {
|
||
const [debounced, setDebounced] = useState(value);
|
||
useEffect(() => {
|
||
const id = setTimeout(() => setDebounced(value), delay);
|
||
return () => clearTimeout(id);
|
||
}, [value, delay]);
|
||
return debounced;
|
||
}
|
||
|
||
function normalize(str: string) {
|
||
return (str || "")
|
||
.toLowerCase()
|
||
.normalize("NFD")
|
||
.replace(/\p{Diacritic}/gu, "");
|
||
}
|
||
|
||
const MONTH_NAME_MAP: Record<string, number> = {
|
||
"janvier":1, "fevrier":2, "février":2, "mars":3, "avril":4, "mai":5, "juin":6,
|
||
"juillet":7, "aout":8, "août":8, "septembre":9, "octobre":10, "novembre":11, "decembre":12, "décembre":12
|
||
};
|
||
|
||
function pad2(n: number) { return String(n).padStart(2, "0"); }
|
||
|
||
// Parse "1 mars 2024" ou "1/03/2024" -> { y, m, d }
|
||
function parseFRDateParts(token: string, fallbackYear?: number): { y: number, m: number, d: number } | null {
|
||
const t = normalize(token);
|
||
// Format texte: 1 mars 2024 (année optionnelle)
|
||
const rxText = /\b(\d{1,2})\s+(janvier|fevrier|février|mars|avril|mai|juin|juillet|aout|août|septembre|octobre|novembre|decembre|décembre)\s*(20\d{2})?\b/;
|
||
const m1 = t.match(rxText);
|
||
if (m1) {
|
||
const d = parseInt(m1[1], 10);
|
||
const m = MONTH_NAME_MAP[m1[2]];
|
||
const y = m1[3] ? parseInt(m1[3], 10) : (fallbackYear ?? new Date().getFullYear());
|
||
return { y, m, d };
|
||
}
|
||
// Format numérique: 01/03/2024 ou 1-3-2024
|
||
const rxNum = /\b(\d{1,2})[\/-](\d{1,2})[\/-](20\d{2})\b/;
|
||
const m2 = t.match(rxNum);
|
||
if (m2) {
|
||
const d = parseInt(m2[1], 10);
|
||
const m = parseInt(m2[2], 10);
|
||
const y = parseInt(m2[3], 10);
|
||
return { y, m, d };
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function toISO({ y, m, d }: { y:number, m:number, d:number }) {
|
||
return `${y}-${pad2(m)}-${pad2(d)}`;
|
||
}
|
||
|
||
// Récupère le matricule d'un résultat de type "salarie"
|
||
function getSalarieMatricule(result: any): string | null {
|
||
try {
|
||
const meta = (result && typeof result === 'object') ? (result.meta || {}) : {};
|
||
const candidates = [
|
||
meta?.matricule,
|
||
meta?.code_salarie,
|
||
meta?.codeSalarie,
|
||
meta?.code,
|
||
meta?.['Code salarié'],
|
||
meta?.['code salarié'],
|
||
];
|
||
for (const c of candidates) {
|
||
if (typeof c === 'string' && c.trim()) return c.trim();
|
||
}
|
||
const url = String(result?.url || '');
|
||
if (url) {
|
||
// Essaye d'extraire depuis le chemin /salaries/<matricule>
|
||
const m = url.match(/\/(?:app\/)?salaries\/([^/?#]+)/i) || url.match(/\bsalaries\/([^/?#]+)/i);
|
||
if (m && m[1]) return decodeURIComponent(m[1]).trim();
|
||
}
|
||
} catch {}
|
||
return null;
|
||
}
|
||
|
||
// Détecte des intentions simples en FR: créer contrat (CDDU par défaut) / créer salarié
|
||
function detectActions(input: string) {
|
||
const q = normalize(input.trim());
|
||
if (!q) return [] as { id: string; title: string; subtitle?: string; url: string }[];
|
||
|
||
const wantsCreate = /(creer|créer|nouveau|ajouter|demarrer|démarrer|ouvrir)/.test(q);
|
||
const isContract = /(contrat|cddu|intermittent|spectacle|regime general|rgi|rg|classique|mono|multi)/.test(q);
|
||
const isEmployee = /(salarie|salarié|employe|employé|artiste|technicien|personne)/.test(q);
|
||
|
||
const actions: { id: string; title: string; subtitle?: string; url: string }[] = [];
|
||
|
||
if (wantsCreate && isContract) {
|
||
let type: "cddu" | "rg" = "cddu"; // par défaut CDDU
|
||
if (/\b(rg|regime general|rgi|classique)\b/.test(q)) type = "rg";
|
||
if (/\b(cddu|intermittent|spectacle)\b/.test(q)) type = "cddu";
|
||
actions.push({
|
||
id: `new-contrat-${type}`,
|
||
title: `Créer un contrat ${type.toUpperCase()}`,
|
||
subtitle: type === "cddu" ? "Par défaut si non précisé" : "Régime général",
|
||
url: `/contrats/nouveau?type=${type}`,
|
||
});
|
||
}
|
||
|
||
if (wantsCreate && isEmployee) {
|
||
actions.push({
|
||
id: `new-salarie`,
|
||
title: `Créer un salarié`,
|
||
subtitle: "Fiche salarié",
|
||
url: `/salaries/nouveau`,
|
||
});
|
||
}
|
||
|
||
// Accès / Utilisateurs : ouvrir la page de gestion ou démarrer une invitation
|
||
const isAccess = /(acces|accès|utilisateur|users?|membre|compte|invitation|inviter|droits|permission)/.test(q);
|
||
if (isAccess) {
|
||
// Si l'intention est de créer/ajouter/inviter -> lien direct vers le formulaire
|
||
if (/(creer|créer|ajouter|nouvel|nouveau|inviter|invite|ajoute|add)/.test(q)) {
|
||
actions.push({
|
||
id: `access-new`,
|
||
title: `Ajouter un accès`,
|
||
subtitle: `Inviter un utilisateur`,
|
||
url: `/vos-acces/nouveau`,
|
||
});
|
||
}
|
||
// Proposer l'ouverture de la gestion des accès de toute façon
|
||
actions.push({
|
||
id: `access-manage`,
|
||
title: `Gérer les accès`,
|
||
subtitle: `Voir et gérer les utilisateurs`,
|
||
url: `/vos-acces`,
|
||
});
|
||
}
|
||
|
||
// Intention: voir les cotisations de <mois|semestre|trimestre> <année> (+ variantes) ou plage de dates
|
||
const cotisationsIntent = /(\bcotis\b|\bcotisation[s]?\b|t[é|e]l[é|e]paiement[s]?)/.test(q) || /^cotis(ation)?s?:?\s*/.test(q) || /\bvoir\b.*\bcotis(ation)?/.test(q);
|
||
if (cotisationsIntent) {
|
||
const now = new Date();
|
||
const currentYear = now.getFullYear();
|
||
const currentMonth = now.getMonth() + 1;
|
||
const prevMonth = currentMonth === 1 ? 12 : currentMonth - 1;
|
||
const prevMonthYear = currentMonth === 1 ? currentYear - 1 : currentYear;
|
||
|
||
// 1) Plages de dates: "du 1 mars 2024 au 30 avril 2024" ou "01/03/2024 au 30/04/2024"
|
||
const range = q.match(/\b(du\s+)?([^\s].*?)\s+(?:au|->|jusqu'?au|to)\s+([^\s].*?)\b/);
|
||
if (range) {
|
||
const left = parseFRDateParts(range[2], currentYear);
|
||
const right = parseFRDateParts(range[3], left?.y ?? currentYear);
|
||
if (left && right) {
|
||
const fromISO = toISO(left);
|
||
const toISODate = toISO(right);
|
||
actions.push({
|
||
id: `see-cotis-range-${fromISO}-${toISODate}`,
|
||
title: `Voir les cotisations du ${pad2(left.d)}/${pad2(left.m)}/${left.y} au ${pad2(right.d)}/${pad2(right.m)}/${right.y}`,
|
||
subtitle: "Plage de dates",
|
||
url: `/cotisations?from=${fromISO}&to=${toISODate}&year=${left.y}&period=toute_annee`,
|
||
});
|
||
return actions;
|
||
}
|
||
}
|
||
|
||
// 2) Année spécifique
|
||
const yearMatch = q.match(/\b(20\d{2})\b/);
|
||
const year = yearMatch ? parseInt(yearMatch[1], 10) : undefined;
|
||
|
||
// 3) Mois spécifique (avec inférence d'année si absente)
|
||
let month: number | undefined;
|
||
let inferredYear: number | undefined;
|
||
for (const name in MONTH_NAME_MAP) {
|
||
if (q.includes(name)) { month = MONTH_NAME_MAP[name]; break; }
|
||
}
|
||
if (/\bce\s+mois\b|\bmois\s+en\s+cours\b/.test(q)) {
|
||
month = currentMonth;
|
||
inferredYear = currentYear;
|
||
}
|
||
if (/\b(mois\s+dernier|dernier\s+mois|mois\s+pr[ée]c[ée]dent)\b/.test(q)) {
|
||
month = prevMonth;
|
||
inferredYear = prevMonthYear;
|
||
}
|
||
|
||
// 4) Semestre (accepte 1er, 1re, 1e, S1, second, 2e, 2nd, S2)
|
||
let semester: 1|2|undefined;
|
||
if (/(premier|1er|1re|1e)\s*semestre|semestre\s*(premier|1er|1re|1e)|\bS1\b/.test(q)) semester = 1;
|
||
if (/(second|2e|2eme|2ème|2nd)\s*semestre|semestre\s*(second|2e|2eme|2ème|2nd)|\bS2\b/.test(q)) semester = 2;
|
||
|
||
// 5) Trimestre (accepte: "trimestre 1/1er/…", "1er trimestre", "tri 3", "T1..T4")
|
||
let trimestre: 1|2|3|4|undefined;
|
||
const tri = q.match(/\b(?:(?:trimestre|tri|t)\s*(?:n[oº°]\s*)?(1|2|3|4)\s*(?:er|re|e|eme|ème|nd)?|(1|2|3|4)\s*(?:er|re|e|eme|ème|nd)?\s*(?:trimestre|tri))\b/);
|
||
if (tri) {
|
||
const num = parseInt((tri[1] || tri[2]), 10);
|
||
if (num >= 1 && num <= 4) trimestre = num as 1|2|3|4;
|
||
}
|
||
|
||
// Construire l'action la plus précise trouvée
|
||
if (month) {
|
||
const y = year ?? inferredYear ?? currentYear;
|
||
actions.push({
|
||
id: `see-cotis-${y}-m${month}`,
|
||
title: `Voir les cotisations de ${month}/${y}`,
|
||
subtitle: "Période mensuelle",
|
||
url: `/cotisations?year=${y}&period=mois_${month}`,
|
||
});
|
||
} else if (year && semester) {
|
||
actions.push({
|
||
id: `see-cotis-${year}-s${semester}`,
|
||
title: `Voir les cotisations S${semester} ${year}`,
|
||
subtitle: "Période semestrielle",
|
||
url: `/cotisations?year=${year}&period=${semester === 1 ? "premier_semestre" : "second_semestre"}`,
|
||
});
|
||
} else if (trimestre) {
|
||
const y = year ?? currentYear;
|
||
actions.push({
|
||
id: `see-cotis-${y}-t${trimestre}`,
|
||
title: `Voir les cotisations T${trimestre} ${y}`,
|
||
subtitle: "Période trimestrielle",
|
||
url: `/cotisations?year=${y}&period=trimestre_${trimestre}`,
|
||
});
|
||
} else if (year) {
|
||
actions.push({
|
||
id: `see-cotis-${year}`,
|
||
title: `Voir les cotisations ${year}`,
|
||
subtitle: "Toute l'année",
|
||
url: `/cotisations?year=${year}&period=toute_annee`,
|
||
});
|
||
} else {
|
||
// Requête générique (ex: "cotis") -> raccourcis année courante
|
||
actions.push({
|
||
id: `see-cotis-${currentYear}`,
|
||
title: `Voir les cotisations ${currentYear}`,
|
||
subtitle: "Toute l'année",
|
||
url: `/cotisations?year=${currentYear}&period=toute_annee`,
|
||
});
|
||
actions.push({
|
||
id: `see-cotis-${currentYear}-s1`,
|
||
title: `Voir les cotisations S1 ${currentYear}`,
|
||
subtitle: "Période semestrielle",
|
||
url: `/cotisations?year=${currentYear}&period=premier_semestre`,
|
||
});
|
||
actions.push({
|
||
id: `see-cotis-${currentYear}-s2`,
|
||
title: `Voir les cotisations S2 ${currentYear}`,
|
||
subtitle: "Période semestrielle",
|
||
url: `/cotisations?year=${currentYear}&period=second_semestre`,
|
||
});
|
||
}
|
||
}
|
||
|
||
return actions;
|
||
}
|
||
|
||
export default function GlobalSearchOverlay() {
|
||
const isMac = typeof navigator !== "undefined" && navigator.platform?.toUpperCase().includes("MAC");
|
||
const [open, setOpen] = useState(false);
|
||
const [query, setQuery] = useState("");
|
||
const debouncedQuery = useDebouncedValue(query, 300);
|
||
const actions = detectActions(query);
|
||
const [activeType, setActiveType] = useState<string | null>(null);
|
||
const [helpOpen, setHelpOpen] = useState(false);
|
||
const [currentValue, setCurrentValue] = useState<string>("");
|
||
const [copiedToast, setCopiedToast] = useState(false);
|
||
|
||
// Fetch des résultats via /api/search
|
||
const { data: results = [], isLoading } = useQuery({
|
||
queryKey: ["global-search", debouncedQuery],
|
||
queryFn: async () => {
|
||
const q = debouncedQuery?.trim();
|
||
if (!q || q.length < 2) return [];
|
||
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, {
|
||
credentials: "include",
|
||
headers: { Accept: "application/json" },
|
||
});
|
||
if (!res.ok) throw new Error("search_api_error");
|
||
return res.json();
|
||
},
|
||
enabled: open && (debouncedQuery?.length ?? 0) >= 2,
|
||
staleTime: 10_000,
|
||
});
|
||
|
||
useEffect(() => {
|
||
if (Array.isArray(results) && results.length > 0) {
|
||
const order = ["salarie","contrat","paie","document","cotisation"];
|
||
const firstType = order.find((t) => (results as any[]).some((r) => r.entity_type === t)) || null;
|
||
setActiveType(firstType);
|
||
const first = (results as any[]).find((r) => r.entity_type === firstType) || (results as any[])[0];
|
||
if (first) setCurrentValue(`${first.entity_type}:${first.entity_id}`);
|
||
} else {
|
||
setActiveType(null);
|
||
}
|
||
}, [results]);
|
||
|
||
// Raccourcis clavier globaux (Cmd/Ctrl + K) + fermeture par ESC
|
||
useEffect(() => {
|
||
const onKeyDown = (e: KeyboardEvent) => {
|
||
const isMac = typeof navigator !== "undefined" && navigator.platform.toUpperCase().includes("MAC");
|
||
const meta = isMac ? e.metaKey : e.ctrlKey;
|
||
if (meta && (e.key === "k" || e.key === "K")) {
|
||
e.preventDefault();
|
||
setOpen((o) => !o);
|
||
}
|
||
// Fermer avec ESC
|
||
if (e.key === "Escape") {
|
||
setOpen(false);
|
||
setHelpOpen(false);
|
||
}
|
||
// Ouvrir/fermer l'aide avec '?' ou Ctrl/⌘ + '/'
|
||
if (open && (e.key === "?" || (e.key === "/" && (isMac ? e.metaKey : e.ctrlKey)))) {
|
||
e.preventDefault();
|
||
setHelpOpen((h) => !h);
|
||
}
|
||
// Ouvrir la sélection dans un nouvel onglet avec Shift + Entrée
|
||
if (open && e.key === "Enter" && e.shiftKey) {
|
||
e.preventDefault();
|
||
try {
|
||
const [t, id] = (currentValue || "").split(":");
|
||
if (t === "action") {
|
||
const a = (actions || []).find((x) => x.id === id);
|
||
if (a?.url) window.open(a.url, "_blank", "noopener,noreferrer");
|
||
return;
|
||
}
|
||
const list = Array.isArray(results) ? (results as any[]) : [];
|
||
const found = list.find((r: any) => r.entity_type === t && String(r.entity_id) === id);
|
||
if (found?.url) window.open(found.url, "_blank", "noopener,noreferrer");
|
||
} catch {}
|
||
}
|
||
// Copier le lien de la sélection avec ⌘/Ctrl + C
|
||
if (open && (isMac ? e.metaKey : e.ctrlKey) && (e.key === "c" || e.key === "C")) {
|
||
const [t, id] = (currentValue || "").split(":");
|
||
if (t === "action") {
|
||
const a = (actions || []).find((x) => x.id === id);
|
||
if (a?.url && navigator?.clipboard?.writeText) {
|
||
e.preventDefault();
|
||
navigator.clipboard.writeText(a.url).then(() => {
|
||
setCopiedToast(true);
|
||
setTimeout(() => setCopiedToast(false), 1500);
|
||
}).catch(() => {});
|
||
}
|
||
} else {
|
||
const list = Array.isArray(results) ? (results as any[]) : [];
|
||
const found = list.find((r: any) => r.entity_type === t && String(r.entity_id) === id);
|
||
if (found?.url && navigator?.clipboard?.writeText) {
|
||
e.preventDefault();
|
||
navigator.clipboard.writeText(found.url).then(() => {
|
||
setCopiedToast(true);
|
||
setTimeout(() => setCopiedToast(false), 1500);
|
||
}).catch(() => {});
|
||
}
|
||
}
|
||
}
|
||
};
|
||
const onOpen = () => setOpen(true);
|
||
|
||
window.addEventListener("keydown", onKeyDown);
|
||
window.addEventListener("open-global-search" as any, onOpen as any);
|
||
return () => {
|
||
window.removeEventListener("keydown", onKeyDown);
|
||
window.removeEventListener("open-global-search" as any, onOpen as any);
|
||
};
|
||
}, [open, currentValue, results, actions]);
|
||
|
||
// Formatage FR des dates pour les métadonnées de contrats
|
||
const fmtDate = (s?: string) => {
|
||
if (!s) return "";
|
||
const d = new Date(s);
|
||
if (isNaN(d.getTime())) return s;
|
||
return d.toLocaleDateString("fr-FR");
|
||
};
|
||
|
||
|
||
if (!open) return null;
|
||
|
||
return (
|
||
<div
|
||
role="dialog"
|
||
aria-modal="true"
|
||
className="cmdk-overlay"
|
||
onClick={() => setOpen(false)}
|
||
>
|
||
<div
|
||
className="cmdk-container"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<div className="">
|
||
<div className="cmdk-header">
|
||
<h3 className="cmdk-title">Palette de Commandes · Raccourci {isMac ? "⌘K" : "Ctrl+K"}</h3>
|
||
<p className="cmdk-subtitle">Utilisez cet outil pour naviguer dans l'Espace Paie Odentas et effectuer des actions.</p>
|
||
</div>
|
||
<Command
|
||
label="Rechercher dans l’Espace Paie"
|
||
// Désactive le filtrage interne de cmdk (on filtre côté API/UI)
|
||
filter={() => 1}
|
||
className="cmdk-root"
|
||
>
|
||
<div className="cmdk-input-wrapper">
|
||
<Command.Input
|
||
autoFocus
|
||
value={query}
|
||
onValueChange={setQuery}
|
||
placeholder="Rechercher un contrat, un salarié, une cotisation… ou effectuer une action en langage naturel"
|
||
className="cmdk-input"
|
||
/>
|
||
<kbd className="cmdk-esc">ESC</kbd>
|
||
</div>
|
||
|
||
<Command.List className="cmdk-list">
|
||
{actions.length > 0 && (
|
||
<Command.Group heading="ACTIONS">
|
||
{actions.map((a) => {
|
||
const val = `action:${a.id}`;
|
||
return (
|
||
<Command.Item
|
||
key={`action-${a.id}`}
|
||
className="cmdk-item"
|
||
onSelect={() => {
|
||
window.location.href = a.url;
|
||
setOpen(false);
|
||
}}
|
||
onMouseEnter={() => { setActiveType("action"); setCurrentValue(val); }}
|
||
onFocus={() => { setActiveType("action"); setCurrentValue(val); }}
|
||
value={val}
|
||
>
|
||
<div className="cmdk-item-main">
|
||
<div className="cmdk-item-left">
|
||
<span className="cmdk-item-icon" aria-hidden="true">⚡</span>
|
||
<div className="min-w-0">
|
||
<div className="text-sm truncate">{a.title}</div>
|
||
{a.subtitle && (
|
||
<div className="cmdk-item-subtitle truncate">{a.subtitle}</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Command.Item>
|
||
);
|
||
})}
|
||
</Command.Group>
|
||
)}
|
||
{helpOpen && (
|
||
<div className="cmdk-help-panel" role="note">
|
||
<div className="cmdk-help-section">
|
||
<div className="cmdk-help-title">Raccourcis clavier</div>
|
||
<ul className="cmdk-help-grid">
|
||
<li><span className="cmdk-kbd">⌘/Ctrl</span> + <span className="cmdk-kbd">K</span> — Ouvrir/Fermer</li>
|
||
<li><span className="cmdk-kbd">ESC</span> — Fermer</li>
|
||
<li><span className="cmdk-kbd">↑</span> / <span className="cmdk-kbd">↓</span> — Naviguer</li>
|
||
<li><span className="cmdk-kbd">Entrée</span> — Ouvrir l'élément</li>
|
||
<li><span className="cmdk-kbd">⌘/Ctrl</span> + <span className="cmdk-kbd">C</span> — Copier le lien</li>
|
||
<li><span className="cmdk-kbd">?</span> — Afficher/Masquer l'aide</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{(!debouncedQuery || debouncedQuery.length < 2) && actions.length === 0 && (
|
||
<Command.Empty className="cmdk-empty">
|
||
Tapez au moins 2 caractères pour commencer.
|
||
</Command.Empty>
|
||
)}
|
||
|
||
{debouncedQuery && debouncedQuery.length >= 2 && isLoading && (
|
||
<Command.Empty className="cmdk-empty">
|
||
Recherche en cours…
|
||
</Command.Empty>
|
||
)}
|
||
|
||
{debouncedQuery && debouncedQuery.length >= 2 && !isLoading && (results as any[]).length === 0 && (
|
||
<Command.Empty className="cmdk-empty">
|
||
Aucun résultat.
|
||
</Command.Empty>
|
||
)}
|
||
|
||
{debouncedQuery && debouncedQuery.length >= 2 && !isLoading && (results as any[]).length > 0 && (
|
||
<>
|
||
{["salarie","contrat","paie","document","cotisation"].map((type) => {
|
||
const items = (results as any[]).filter((r) => r.entity_type === type);
|
||
// Si contrats: trier par date de début (meta.debut) décroissante et limiter à 5
|
||
if (type === "contrat") {
|
||
const parseDate = (s: any) => {
|
||
if (!s) return 0;
|
||
const d = new Date(s);
|
||
return isNaN(d.getTime()) ? 0 : d.getTime();
|
||
};
|
||
items.sort((a: any, b: any) => parseDate(b?.meta?.debut) - parseDate(a?.meta?.debut));
|
||
// Limite à 5 résultats contrats
|
||
while (items.length > 5) items.pop();
|
||
}
|
||
if (!items.length) return null;
|
||
return (
|
||
<Command.Group key={type} heading={type.toUpperCase()}>
|
||
{items.map((r: any) => {
|
||
const val = `${r.entity_type}:${r.entity_id}`;
|
||
return (
|
||
<Fragment key={`${r.entity_type}-${r.entity_id}-wrap`}>
|
||
<Command.Item
|
||
key={`${r.entity_type}-${r.entity_id}`}
|
||
className="cmdk-item"
|
||
onSelect={() => {
|
||
window.location.href = r.url;
|
||
setOpen(false);
|
||
}}
|
||
onMouseEnter={() => { setActiveType(r.entity_type); setCurrentValue(val); }}
|
||
onFocus={() => { setActiveType(r.entity_type); setCurrentValue(val); }}
|
||
value={val}
|
||
>
|
||
<div className="cmdk-item-main">
|
||
<div className="cmdk-item-left">
|
||
<span className="cmdk-item-icon" aria-hidden="true">🔎</span>
|
||
<div className="min-w-0">
|
||
<div className="text-sm truncate">{r.title}</div>
|
||
{r.subtitle && (
|
||
<div className="cmdk-item-subtitle truncate">{r.subtitle}</div>
|
||
)}
|
||
{r.entity_type === "contrat" && (
|
||
<div className="cmdk-item-meta truncate">
|
||
{r?.meta?.spectacle && <span className="truncate">{r.meta.spectacle}</span>}
|
||
{(r?.meta?.debut || r?.meta?.fin) && (
|
||
<>
|
||
{r?.meta?.spectacle && <span className="cmdk-dot">•</span>}
|
||
<span>{fmtDate(r?.meta?.debut)}{r?.meta?.fin ? " → " + fmtDate(r.meta.fin) : ""}</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
{r.entity_type === "contrat" && (
|
||
<div className="cmdk-item-right">
|
||
{(() => {
|
||
const isSigned = (r?.meta?.is_signed === true) || (String(r?.meta?.contrat_signe || '').toLowerCase() === 'oui');
|
||
return (
|
||
<span className={`cmdk-badge ${isSigned ? "cmdk-badge--success" : "cmdk-badge--muted"}`}>
|
||
{isSigned ? "Signé" : "Non signé"}
|
||
</span>
|
||
);
|
||
})()}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</Command.Item>
|
||
{r.entity_type === 'salarie' && (
|
||
<Command.Item
|
||
key={`${r.entity_type}-${r.entity_id}-create-contrat`}
|
||
className="cmdk-item"
|
||
onSelect={() => {
|
||
try {
|
||
const matricule = getSalarieMatricule(r);
|
||
if (matricule && matricule.trim()) {
|
||
window.location.href = `/contrats/nouveau?salarie=${encodeURIComponent(matricule.trim())}`;
|
||
setOpen(false);
|
||
}
|
||
} catch {}
|
||
}}
|
||
onMouseEnter={() => { setActiveType('action'); setCurrentValue(`action:create-contrat:${r.entity_id}`); }}
|
||
onFocus={() => { setActiveType('action'); setCurrentValue(`action:create-contrat:${r.entity_id}`); }}
|
||
value={`action:create-contrat:${r.entity_id}`}
|
||
>
|
||
<div className="cmdk-item-main">
|
||
<div className="cmdk-item-left">
|
||
<span className="cmdk-item-icon" aria-hidden="true">⚡</span>
|
||
<div className="min-w-0">
|
||
<div className="text-sm truncate">Créer un contrat CDDU pour {r.title}</div>
|
||
<div className="cmdk-item-subtitle truncate">Formulaire nouveau contrat</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Command.Item>
|
||
)}
|
||
</Fragment>
|
||
);
|
||
})}
|
||
</Command.Group>
|
||
);
|
||
})}
|
||
</>
|
||
)}
|
||
</Command.List>
|
||
<div className="cmdk-footer">
|
||
<div className="cmdk-hint">
|
||
<div className="cmdk-hint-left">
|
||
<span className="cmdk-kbd">↵</span>
|
||
<span>
|
||
{activeType === "action" && "Exécuter l'action"}
|
||
{activeType === "salarie" && "Ouvrir le salarié"}
|
||
{activeType === "contrat" && "Ouvrir le contrat"}
|
||
{activeType === "paie" && "Ouvrir la paie"}
|
||
{activeType === "document" && "Ouvrir le document"}
|
||
{activeType === "cotisation" && "Ouvrir la cotisation"}
|
||
{!activeType && " ouvrir"}
|
||
</span>
|
||
</div>
|
||
<div className="cmdk-hint-right">
|
||
<span className="cmdk-dot">•</span>
|
||
<span className="cmdk-kbd">↑</span>
|
||
<span className="cmdk-kbd">↓</span>
|
||
<span> pour naviguer </span>
|
||
<span className="cmdk-dot">•</span>
|
||
<span className="cmdk-kbd">⇧</span><span className="cmdk-kbd">↵</span>
|
||
<span> nouvel onglet </span>
|
||
<span className="cmdk-dot">•</span>
|
||
<button type="button" className="cmdk-helpbtn" onClick={() => setHelpOpen((h) => !h)} aria-pressed={helpOpen} aria-label="Afficher l'aide">?
|
||
</button>
|
||
<span> aide</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Command>
|
||
{copiedToast && (
|
||
<div className="cmdk-toast" role="status" aria-live="polite">Lien copié ✅</div>
|
||
)}
|
||
<style jsx>{`
|
||
.cmdk-header { padding: 10px 12px 0; }
|
||
.cmdk-title { margin: 0 0 2px 0; font-size: 18px; font-weight: 600; }
|
||
.cmdk-subtitle { margin: 0; font-size: 12px; color: var(--muted-foreground, #666); }
|
||
.cmdk-helpbar { margin: 6px 12px 0; font-size: 12px; display: flex; align-items: center; gap: 6px; color: var(--muted-foreground, #666); }
|
||
.cmdk-help-panel { padding: 12px; border-top: 1px solid rgba(0,0,0,0.06); background: var(--panel, #fafafa); }
|
||
.cmdk-help-section { margin-bottom: 10px; }
|
||
.cmdk-help-title { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; opacity: .8; margin-bottom: 6px; }
|
||
.cmdk-help-grid { margin: 0; padding-left: 16px; display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 6px 16px; font-size: 13px; }
|
||
.cmdk-help-tip { font-size: 12px; color: var(--muted-foreground, #666); margin-top: 6px; }
|
||
.cmdk-kbd { display: inline-block; border: 1px solid rgba(0,0,0,0.2); border-bottom-width: 2px; padding: 0 6px; border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; line-height: 18px; }
|
||
.cmdk-dot { margin: 0 6px; opacity: .6; }
|
||
.cmdk-hint { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; border-top: 1px solid rgba(0,0,0,0.06); font-size: 12px; color: var(--muted-foreground, #666); }
|
||
.cmdk-hint-right { display: flex; align-items: center; gap: 6px; }
|
||
.cmdk-helpbtn { border: 1px solid rgba(0,0,0,0.2); border-bottom-width: 2px; border-radius: 4px; font: inherit; padding: 0 8px; height: 22px; background: transparent; cursor: pointer; }
|
||
.cmdk-tagline { display: block; padding: 6px 12px 10px; font-size: 11px; color: var(--muted-foreground, #666); opacity: .75; text-align: center; }
|
||
.cmdk-toast { position: absolute; right: 12px; bottom: 12px; padding: 6px 10px; font-size: 12px; border: 1px solid rgba(0,0,0,0.08); border-radius: 6px; background: rgba(0,0,0,0.75); color: #fff; box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
|
||
`}</style>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|