d83e0c82b2
Mesmo padrão do seletor de protético, generalizado para os demais dropdowns: - Componente SearchSelect (modal genérico: trigger + busca + lista filtrável por rótulo/hint). - LancarGTO: Especialidade/Área, Produto/Prótese, Procedimento, Dentista e Convênio/Credenciada — todos os <select> convertidos (0 selects restantes). - Nova OS (ProteseView): Produto e Dentista convertidos. - Cotação (RFQ): input de busca na lista de laboratórios a convidar (continua multi-seleção). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
67 lines
3.5 KiB
TypeScript
67 lines
3.5 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Search, X, Check, ChevronDown } from 'lucide-react';
|
|
|
|
export interface SelectOption { value: string; label: string; hint?: string }
|
|
|
|
// Dropdown genérico que abre um modal com busca — substitui <select> em listas longas.
|
|
export const SearchSelect: React.FC<{
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
options: SelectOption[];
|
|
placeholder?: string;
|
|
title?: string;
|
|
className?: string;
|
|
disabled?: boolean;
|
|
}> = ({ value, onChange, options, placeholder = 'Selecione...', title, className, disabled }) => {
|
|
const [open, setOpen] = useState(false);
|
|
const [busca, setBusca] = useState('');
|
|
const sel = options.find(o => o.value === value);
|
|
const q = busca.trim().toLowerCase();
|
|
const filtrados = q ? options.filter(o => o.label.toLowerCase().includes(q) || (o.hint || '').toLowerCase().includes(q)) : options;
|
|
const pick = (v: string) => { onChange(v); setOpen(false); setBusca(''); };
|
|
|
|
return (
|
|
<>
|
|
<button type="button" disabled={disabled} onClick={() => setOpen(true)}
|
|
className={className || 'w-full p-4 bg-gray-50 border-2 border-gray-100 rounded-2xl font-black text-gray-700 text-sm focus:border-teal-400 outline-none transition-all uppercase disabled:opacity-50'}>
|
|
<span className="flex items-center justify-between gap-2">
|
|
<span className={`truncate ${sel ? '' : 'text-gray-400'}`}>{sel ? sel.label : placeholder}</span>
|
|
<ChevronDown size={16} className="text-gray-400 shrink-0" />
|
|
</span>
|
|
</button>
|
|
|
|
{open && (
|
|
<div className="fixed inset-0 bg-black/40 z-[60] flex items-center justify-center p-4" onClick={() => setOpen(false)}>
|
|
<div className="bg-white rounded-2xl w-full max-w-md max-h-[85vh] flex flex-col" onClick={e => e.stopPropagation()}>
|
|
<div className="flex items-center justify-between p-4 border-b border-gray-100">
|
|
<h3 className="font-black text-gray-800 uppercase text-sm">{title || placeholder}</h3>
|
|
<button onClick={() => setOpen(false)}><X size={20} className="text-gray-400" /></button>
|
|
</div>
|
|
<div className="p-3 border-b border-gray-50">
|
|
<div className="relative">
|
|
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-300" />
|
|
<input autoFocus value={busca} onChange={e => setBusca(e.target.value)} placeholder="Buscar…"
|
|
className="w-full pl-9 pr-3 py-2.5 rounded-xl border border-gray-200 text-sm outline-none focus:border-teal-400" />
|
|
</div>
|
|
</div>
|
|
<div className="flex-1 overflow-y-auto p-2">
|
|
{filtrados.length === 0 ? (
|
|
<p className="text-center text-gray-400 text-sm font-bold uppercase py-10">Nada encontrado.</p>
|
|
) : filtrados.map(o => (
|
|
<button key={o.value} onClick={() => pick(o.value)}
|
|
className={`w-full text-left flex items-center gap-2 px-3 py-2.5 rounded-xl ${o.value === value ? 'bg-teal-50 border border-teal-200' : 'hover:bg-gray-50'}`}>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="font-bold text-gray-800 text-sm truncate uppercase">{o.label}</p>
|
|
{o.hint && <p className="text-[11px] text-gray-400 truncate">{o.hint}</p>}
|
|
</div>
|
|
{o.value === value && <Check size={16} className="text-teal-600 shrink-0" />}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
};
|