espace-paie-odentas/components/SearchableInput.tsx

86 lines
3 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { api } from "@/lib/fetcher";
type Org = { id: string; name: string; structure_api?: string | null };
export default function SearchableInput({
value,
onSelect,
placeholder = "Rechercher...",
}: {
value?: string | null;
onSelect: (o: Org | null) => void;
placeholder?: string;
}) {
const [q, setQ] = useState("");
const [open, setOpen] = useState(false);
// simple clientInfo query to include active org headers for internal api() calls if needed
const { data: me } = useQuery({
queryKey: ["client-info"],
queryFn: async () => {
try {
const res = await fetch("/api/me", { cache: "no-store", headers: { Accept: "application/json" }, credentials: "include" });
if (!res.ok) return null;
const user = await res.json();
return { id: user.active_org_id || null, name: user.active_org_name || "Organisation", api_name: user.active_org_api_name } as any;
} catch {
return null;
}
},
staleTime: 30_000,
});
const debounced = q; // keep simple; react-query will handle throttling when enabled
const orgsQuery = useQuery({
queryKey: ["search-organizations", debounced],
queryFn: async () => {
// fetch all organizations and filter client-side (server route returns list)
const res = await fetch(`/api/organizations`, { cache: "no-store", credentials: "include" });
if (!res.ok) return [] as Org[];
const json = await res.json();
const items: Org[] = json.items || [];
if (!debounced || debounced.trim().length === 0) return items;
const norm = (s: string) => s.toLowerCase().normalize("NFD").replace(/\p{Diacritic}/gu, "").replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
const nq = norm(debounced);
return items.filter((it) => norm(it.name || "").includes(nq));
},
enabled: true,
staleTime: 10_000,
});
useEffect(() => {
// prefill input when value is provided
if (value && !q) setQ(String(value));
}, [value]);
return (
<div className="relative">
<input
className="rounded border px-2 py-1 w-full"
placeholder={placeholder}
value={q}
onChange={(e) => { setQ(e.target.value); setOpen(true); }}
onFocus={() => setOpen(true)}
/>
{open && orgsQuery.data && orgsQuery.data.length > 0 ? (
<div className="absolute left-0 right-0 mt-1 bg-white border rounded shadow z-40 max-h-48 overflow-auto">
{orgsQuery.data.map((o) => (
<div
key={o.id}
className="px-3 py-2 hover:bg-slate-50 cursor-pointer text-sm"
onMouseDown={(ev) => { ev.preventDefault(); onSelect(o); setQ(o.name || ""); setOpen(false); }}
>
<div className="font-medium">{o.name}</div>
<div className="text-xs text-slate-500">{o.id}</div>
</div>
))}
</div>
) : null}
</div>
);
}