feat(ui): dropdowns viram modal com busca (SearchSelect) na Avaliação/GTO e Nova OS
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>
This commit is contained in:
@@ -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 = () => {
|
||||
<Stethoscope size={20} strokeWidth={3} /> 02. Especialidade
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<select
|
||||
<SearchSelect
|
||||
value={selectedEsp}
|
||||
onChange={(e) => {
|
||||
setSelectedEsp(e.target.value);
|
||||
onChange={(v) => {
|
||||
setSelectedEsp(v);
|
||||
setSelectedProc(null);
|
||||
setSelectedDentes([]);
|
||||
setSelectedArco(null);
|
||||
@@ -468,11 +469,9 @@ export const LancarGTO: React.FC = () => {
|
||||
setProtValor('');
|
||||
setProtObs('');
|
||||
}}
|
||||
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"
|
||||
>
|
||||
<option value="">ESCOLHA A ÁREA...</option>
|
||||
{especialidades.map(es => <option key={es.id} value={es.id}>{es.nome}</option>)}
|
||||
</select>
|
||||
options={especialidades.map(es => ({ value: es.id, label: es.nome }))}
|
||||
placeholder="ESCOLHA A ÁREA..." title="Especialidade"
|
||||
/>
|
||||
|
||||
{selectedEsp && isProtese && (
|
||||
<div className="space-y-3">
|
||||
@@ -484,18 +483,17 @@ export const LancarGTO: React.FC = () => {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<select
|
||||
<SearchSelect
|
||||
value={selectedProdutoId}
|
||||
onChange={(e) => {
|
||||
setSelectedProdutoId(e.target.value);
|
||||
const pr = labProdutos.find(p => p.id === e.target.value);
|
||||
onChange={(v) => {
|
||||
setSelectedProdutoId(v);
|
||||
const pr = labProdutos.find(p => p.id === v);
|
||||
setProtValor(pr ? String(pr.preco ?? '') : '');
|
||||
}}
|
||||
options={labProdutos.map(p => ({ value: p.id, label: p.nome, hint: `R$ ${Number(p.preco || 0).toFixed(2)}` }))}
|
||||
placeholder="ESCOLHA A PRÓTESE..." title="Produto do laboratório"
|
||||
className="w-full p-4 bg-white border-2 border-teal-100 rounded-2xl font-black text-gray-800 text-sm shadow-inner uppercase"
|
||||
>
|
||||
<option value="">ESCOLHA A PRÓTESE...</option>
|
||||
{labProdutos.map(p => <option key={p.id} value={p.id}>{p.nome} — R$ {Number(p.preco || 0).toFixed(2)}</option>)}
|
||||
</select>
|
||||
/>
|
||||
|
||||
{selectedProduto && (
|
||||
<>
|
||||
@@ -543,21 +541,20 @@ export const LancarGTO: React.FC = () => {
|
||||
<p className="text-[10px] font-bold text-amber-500 uppercase mt-1">Cadastre em Procedimentos para usá-lo aqui</p>
|
||||
</div>
|
||||
) : (
|
||||
<select
|
||||
<SearchSelect
|
||||
value={selectedProc?.id || ''}
|
||||
onChange={(e) => {
|
||||
const pr = procedimentos.find(p => p.id === e.target.value);
|
||||
onChange={(v) => {
|
||||
const pr = procedimentos.find(p => p.id === v);
|
||||
setSelectedProc(pr || null);
|
||||
setSelectedDentes([]);
|
||||
setSelectedArco(null);
|
||||
setArcoFace('');
|
||||
setArcoObs('');
|
||||
}}
|
||||
options={filteredProcedimentos.map(p => ({ value: p.id, label: p.nome }))}
|
||||
placeholder="BUSCAR PROCEDIMENTO" title="Procedimento"
|
||||
className="w-full p-4 bg-white border-2 border-teal-100 rounded-2xl font-black text-gray-800 text-sm shadow-inner uppercase"
|
||||
>
|
||||
<option value="">BUSCAR PROCEDIMENTO</option>
|
||||
{filteredProcedimentos.map(p => <option key={p.id} value={p.id}>{p.nome}</option>)}
|
||||
</select>
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedProc && (
|
||||
@@ -588,28 +585,23 @@ export const LancarGTO: React.FC = () => {
|
||||
<div className="flex items-center gap-3 text-[#2d6a4f] font-black text-base uppercase tracking-wider">
|
||||
<Check size={20} strokeWidth={3} className="bg-green-100 p-1 rounded-full text-green-700" /> 03. Dentista Executor
|
||||
</div>
|
||||
<select
|
||||
<SearchSelect
|
||||
value={store.dentista?.id || ''}
|
||||
onChange={(e) => {
|
||||
const d = dentistas.find(d => d.id === e.target.value);
|
||||
store.setDentista(d || null);
|
||||
}}
|
||||
onChange={(v) => { const d = dentistas.find(d => d.id === v); store.setDentista(d || null); }}
|
||||
options={dentistas.map(d => ({ value: d.id, label: d.nome }))}
|
||||
placeholder="SELECIONE O PROFISSIONAL" title="Dentista executor"
|
||||
className="w-full p-4 bg-gray-50 border-2 border-gray-100 rounded-2xl font-black text-gray-800 text-sm focus:border-green-500 outline-none transition-all uppercase"
|
||||
>
|
||||
<option value="">SELECIONE O PROFISSIONAL</option>
|
||||
{dentistas.map(d => <option key={d.id} value={d.id}>{d.nome}</option>)}
|
||||
</select>
|
||||
/>
|
||||
|
||||
<div className="pt-1">
|
||||
<label className="text-[11px] font-black text-gray-400 uppercase tracking-widest mb-1.5 block">Convênio / Credenciada</label>
|
||||
<select
|
||||
<SearchSelect
|
||||
value={store.credenciada || ''}
|
||||
onChange={(e) => store.setCredenciada(e.target.value)}
|
||||
onChange={(v) => store.setCredenciada(v)}
|
||||
options={convenios.map(c => ({ value: c.id, label: c.nome }))}
|
||||
placeholder="SELECIONE O CONVÊNIO" title="Convênio / Credenciada"
|
||||
className="w-full p-4 bg-gray-50 border-2 border-gray-100 rounded-2xl font-black text-gray-800 text-sm focus:border-green-500 outline-none transition-all uppercase"
|
||||
>
|
||||
<option value="">SELECIONE O CONVÊNIO</option>
|
||||
{convenios.map(c => <option key={c.id} value={c.id}>{c.nome}</option>)}
|
||||
</select>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
<div>
|
||||
<label className="text-[10px] font-black text-gray-500 uppercase">Produto / Trabalho *</label>
|
||||
{form.protetico_id && produtos.length > 0 ? (
|
||||
<select value={form.produto_id} onChange={e => set('produto_id', e.target.value)} className="w-full mt-1 px-3 py-2 rounded-xl border border-gray-200 text-sm">
|
||||
<option value="">Selecione...</option>
|
||||
{produtos.map(p => <option key={p.id} value={p.id}>{p.nome} — {fmtBRL(p.preco)}</option>)}
|
||||
</select>
|
||||
<div className="mt-1"><SearchSelect value={form.produto_id} onChange={(v) => 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" /></div>
|
||||
) : (
|
||||
<input value={form.tipo} onChange={e => 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 */}
|
||||
<div>
|
||||
<label className="text-[10px] font-black text-gray-500 uppercase">Dentista</label>
|
||||
<select value={form.dentista_id} onChange={e => set('dentista_id', e.target.value)} className="w-full mt-1 px-3 py-2 rounded-xl border border-gray-200 text-sm">
|
||||
<option value="">—</option>
|
||||
{dentistas.map(d => <option key={d.id} value={d.id}>{d.nome}</option>)}
|
||||
</select>
|
||||
<div className="mt-1"><SearchSelect value={form.dentista_id} onChange={(v) => 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" /></div>
|
||||
</div>
|
||||
{/* Material + cor + dentes */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
@@ -1008,6 +1009,7 @@ const CotacoesClinicaModal: React.FC<{ onClose: () => void; onEscolhida: () => v
|
||||
const [dentes, setDentes] = useState('');
|
||||
const [prazo, setPrazo] = useState('');
|
||||
const [sel, setSel] = useState<string[]>([]);
|
||||
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
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase mb-2">Laboratórios a convidar</p>
|
||||
<div className="relative mb-2">
|
||||
<Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-gray-300" />
|
||||
<input value={buscaLab} onChange={e => 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" />
|
||||
</div>
|
||||
<div className="space-y-1.5 max-h-56 overflow-y-auto">
|
||||
{labs.map(l => (
|
||||
{labs.filter(l => !buscaLab.trim() || (l.nome || '').toLowerCase().includes(buscaLab.toLowerCase()) || (l.email || '').toLowerCase().includes(buscaLab.toLowerCase())).map(l => (
|
||||
<button key={l.id} onClick={() => toggleLab(l.id)} className={`w-full flex items-center gap-2 px-3 py-2 rounded-xl border text-left ${sel.includes(l.id) ? 'border-violet-400 bg-violet-50' : 'border-gray-100'}`}>
|
||||
<span className={`w-4 h-4 rounded border flex items-center justify-center ${sel.includes(l.id) ? 'bg-violet-600 border-violet-600' : 'border-gray-300'}`}>{sel.includes(l.id) && <Check size={11} className="text-white" />}</span>
|
||||
<span className="text-sm font-bold text-gray-700 flex-1 truncate">{l.nome}</span>
|
||||
|
||||
Reference in New Issue
Block a user