From d83e0c82b2979ad03cbbfaa41357c64fc27fc553 Mon Sep 17 00:00:00 2001 From: VPS 4 Builder Date: Thu, 25 Jun 2026 07:29:49 +0200 Subject: [PATCH] =?UTF-8?q?feat(ui):=20dropdowns=20viram=20modal=20com=20b?= =?UTF-8?q?usca=20(SearchSelect)=20na=20Avalia=C3=A7=C3=A3o/GTO=20e=20Nova?= =?UTF-8?q?=20OS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 ( + <> + + + {open && ( +
setOpen(false)}> +
e.stopPropagation()}> +
+

{title || placeholder}

+ +
+
+
+ + 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" /> +
+
+
+ {filtrados.length === 0 ? ( +

Nada encontrado.

+ ) : filtrados.map(o => ( + + ))} +
+
+
+ )} + + ); +}; diff --git a/frontend/views/LancarGTO.tsx b/frontend/views/LancarGTO.tsx index 026cfa9..b92cd3e 100644 --- a/frontend/views/LancarGTO.tsx +++ b/frontend/views/LancarGTO.tsx @@ -14,6 +14,7 @@ import { } from 'lucide-react'; import { PageHeader } from '../components/PageHeader.tsx'; import { ProteticoPicker } from '../components/ProteticoPicker.tsx'; +import { SearchSelect } from '../components/SearchSelect.tsx'; import { HybridBackend } from '../services/backend.ts'; import { Paciente, Dentista, Procedimento, Especialidade } from '../types.ts'; import { useToast } from '../contexts/ToastContext.tsx'; @@ -457,10 +458,10 @@ export const LancarGTO: React.FC = () => { 02. Especialidade
- + options={especialidades.map(es => ({ value: es.id, label: es.nome }))} + placeholder="ESCOLHA A ÁREA..." title="Especialidade" + /> {selectedEsp && isProtese && (
@@ -484,18 +483,17 @@ export const LancarGTO: React.FC = () => {
) : ( <> - + /> {selectedProduto && ( <> @@ -543,21 +541,20 @@ export const LancarGTO: React.FC = () => {

Cadastre em Procedimentos para usá-lo aqui

) : ( - + /> )} {selectedProc && ( @@ -588,28 +585,23 @@ export const LancarGTO: React.FC = () => {
03. Dentista Executor
- + />
- + />
); diff --git a/frontend/views/ProteseView.tsx b/frontend/views/ProteseView.tsx index 0806a44..b403fde 100644 --- a/frontend/views/ProteseView.tsx +++ b/frontend/views/ProteseView.tsx @@ -6,6 +6,7 @@ import { useConfirm } from '../contexts/ConfirmContext.tsx'; import { PageHeader } from '../components/PageHeader.tsx'; import { rotulosPaciente } from '../utils/pacienteLabel.ts'; import { ProteticoPicker } from '../components/ProteticoPicker.tsx'; +import { SearchSelect } from '../components/SearchSelect.tsx'; const fmtBRL = (v: number) => 'R$ ' + Number(v || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 }); const dataBR = (d: string) => (d || '').slice(0, 10).split('-').reverse().join('/'); @@ -159,10 +160,10 @@ const NovaOSModal: React.FC<{ onClose: () => void; onSaved: () => void }> = ({ o
{form.protetico_id && produtos.length > 0 ? ( - +
set('produto_id', v)} + options={produtos.map(p => ({ value: p.id, label: p.nome, hint: fmtBRL(p.preco) }))} + placeholder="Selecione..." title="Produto do laboratório" + className="w-full px-3 py-2 rounded-xl border border-gray-200 text-sm" />
) : ( set('tipo', e.target.value)} placeholder={form.protetico_id ? 'Sem catálogo — descreva o trabalho' : 'Selecione o laboratório primeiro'} disabled={!form.protetico_id} className="w-full mt-1 px-3 py-2 rounded-xl border border-gray-200 text-sm disabled:bg-gray-50" /> @@ -174,10 +175,10 @@ const NovaOSModal: React.FC<{ onClose: () => void; onSaved: () => void }> = ({ o {/* Dentista */}
- +
set('dentista_id', v)} + options={dentistas.map(d => ({ value: d.id, label: d.nome }))} + placeholder="—" title="Dentista" + className="w-full px-3 py-2 rounded-xl border border-gray-200 text-sm" />
{/* Material + cor + dentes */}
@@ -1008,6 +1009,7 @@ const CotacoesClinicaModal: React.FC<{ onClose: () => void; onEscolhida: () => v const [dentes, setDentes] = useState(''); const [prazo, setPrazo] = useState(''); const [sel, setSel] = useState([]); + const [buscaLab, setBuscaLab] = useState(''); const [busy, setBusy] = useState(false); const carregar = useCallback(() => HybridBackend.getRfqs().then(r => setRfqs(Array.isArray(r) ? r : [])).catch(() => setRfqs([])), []); @@ -1050,8 +1052,12 @@ const CotacoesClinicaModal: React.FC<{ onClose: () => void; onEscolhida: () => v

Laboratórios a convidar

+
+ + setBuscaLab(e.target.value)} placeholder="Buscar laboratório…" className="w-full pl-8 pr-3 py-2 bg-gray-50 border border-gray-100 rounded-xl text-sm outline-none focus:border-violet-400" /> +
- {labs.map(l => ( + {labs.filter(l => !buscaLab.trim() || (l.nome || '').toLowerCase().includes(buscaLab.toLowerCase()) || (l.email || '').toLowerCase().includes(buscaLab.toLowerCase())).map(l => (