86 lines
3 KiB
TypeScript
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>
|
|
);
|
|
}
|