testes/src_2/components/shared/AutoFillInput.jsx

163 lines
6.3 KiB
JavaScript

import React, { useState, useEffect, useRef } from 'react';
import { Search, Loader2, Plus, UserPlus, Check } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
/**
* AutoFillInput - Um input inteligente para consultas e autopreenchimento.
*
* @param {string} label - Label do campo.
* @param {string} placeholder - Marcador de posição.
* @param {Array} data - Dados locais (opcional).
* @param {string} apiRoute - Rota da API para consulta (se não houver 'data').
* @param {string} filterField - Campo do objeto usado para o filtro (ex: 'nome').
* @param {string} displayField - Campo que será exibido no input após seleção.
* @param {function} onSelect - Callback disparado ao selecionar um item. Retorna o objeto completo.
* @param {function} onAddNew - Callback para o botão "Adicionar Novo".
*/
export const AutoFillInput = ({
label,
placeholder = "Comece a digitar para pesquisar...",
data = [],
apiRoute,
filterField = "name",
displayField = "name",
onSelect,
onAddNew,
className,
icon: Icon = Search
}) => {
const [query, setQuery] = useState('');
const [suggestions, setSuggestions] = useState([]);
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(-1);
const containerRef = useRef(null);
// Fecha a lista ao clicar fora do componente
useEffect(() => {
const handleClickOutside = (event) => {
if (containerRef.current && !containerRef.current.contains(event.target)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
// Lógica de Filtro / API (Mockada para o exemplo)
useEffect(() => {
const timer = setTimeout(() => {
if (query.trim().length > 0) {
setIsLoading(true);
// Simulação de consulta (Pode ser substituído por axios.get(apiRoute))
const results = data.filter(item =>
String(item[filterField] || "").toLowerCase().includes(query.toLowerCase())
).slice(0, 10);
setSuggestions(results);
setIsOpen(true);
setIsLoading(false);
} else {
setSuggestions([]);
setIsOpen(false);
}
}, 300); // 300ms de debounce
return () => clearTimeout(timer);
}, [query, data, filterField]);
const handleSelect = (item) => {
setQuery(item[displayField]);
setIsOpen(false);
if (onSelect) onSelect(item);
};
const handleKeyDown = (e) => {
if (e.key === 'ArrowDown') {
setSelectedIndex(prev => (prev < suggestions.length - 1 ? prev + 1 : prev));
} else if (e.key === 'ArrowUp') {
setSelectedIndex(prev => (prev > -1 ? prev - 1 : prev));
} else if (e.key === 'Enter' && selectedIndex >= 0) {
handleSelect(suggestions[selectedIndex]);
} else if (e.key === 'Escape') {
setIsOpen(false);
}
};
return (
<div className={cn("relative w-full space-y-1.5", className)} ref={containerRef}>
{label && (
<label className="text-xs font-bold uppercase tracking-widest text-slate-500 ml-1">
{label}
</label>
)}
<div className="relative group">
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 group-focus-within:text-emerald-500 transition-colors">
{isLoading ? <Loader2 size={16} className="animate-spin" /> : <Icon size={16} />}
</div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={() => query && setIsOpen(true)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
className={cn(
"w-full bg-white dark:bg-[#1a1a1a] border border-slate-200 dark:border-white/5 rounded-lg pl-10 pr-4 py-2.5 text-sm font-medium",
"text-slate-900 dark:text-slate-100", // Fix visibility
"focus:outline-none focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500/50 transition-all",
"placeholder:text-slate-500"
)}
/>
</div>
{/* Lista de Sugestões (Painel) */}
{isOpen && (
<div className="absolute z-50 w-full mt-1 bg-white dark:bg-[#111] border border-slate-200 dark:border-white/10 rounded-xl shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-200">
<div className="max-h-[300px] overflow-y-auto custom-scrollbar p-1">
{suggestions.length > 0 ? (
suggestions.map((item, index) => (
<button
key={index}
onClick={() => handleSelect(item)}
className={cn(
"w-full flex items-center justify-between px-3 py-2.5 rounded-lg text-sm text-left transition-all",
selectedIndex === index || query === item[displayField]
? "bg-emerald-500/10 text-emerald-500"
: "text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-white/5"
)}
>
<div className="flex flex-col">
<span className="font-bold">{item[displayField]}</span>
{item.subtext && <span className="text-[10px] opacity-60">{item.subtext}</span>}
</div>
{query === item[displayField] && <Check size={14} className="text-emerald-500" />}
</button>
))
) : (
<div className="px-4 py-6 text-center text-xs text-slate-500 italic">
Nenhum resultado encontrado para "{query}"
</div>
)}
</div>
{/* Rodapé de Ação (Adicionar Novo) */}
<div className="border-t border-slate-200 dark:border-white/5 p-1 bg-slate-50/50 dark:bg-white/[0.02]">
<button
onClick={() => onAddNew && onAddNew(query)}
className="w-full flex items-center gap-2 px-3 py-2.5 rounded-lg text-xs font-black uppercase tracking-tighter text-emerald-500 hover:bg-emerald-500/10 transition-colors"
>
<Plus size={14} strokeWidth={3} />
Criar novo registro
</button>
</div>
</div>
)}
</div>
);
};