feat(protese): Fase 1 — finalizar GTO abre OS de prótese na Bancada
Itens de GTO ligados a um produto do catálogo do protético geram, na finalização da GTO, uma OS de prótese (status 'solicitado') na Bancada do laboratório — idempotente por gto_item_id e isolado em try/catch para nunca derrubar o finalize. Migrações aditivas (gto_items.produto_id/protetico_id, protese_os.gto_item_id, re-tag de especialidades para area='protese'). No Lançar GTO, a especialidade de prótese troca o seletor de procedimentos pelo fluxo laboratório → produto → valor → observação. Busca de pacientes passa a ser server-side com debounce (antes filtrava só a 1ª página). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4445,6 +4445,39 @@ app.put('/api/gto-builder/:id/finalizar', authGuard, async (req, res) => {
|
|||||||
await registrarAudit(pool, { clinicaId: b.clinica_id, entidade: 'gto', entidadeId: b.id, acao: 'finalizou_gto', actorId: req.authUser?.userId, actorNome: await actorNome(req.authUser?.userId), detalhes: { total, financeiro_id: financeiroId } });
|
await registrarAudit(pool, { clinicaId: b.clinica_id, entidade: 'gto', entidadeId: b.id, acao: 'finalizou_gto', actorId: req.authUser?.userId, actorNome: await actorNome(req.authUser?.userId), detalhes: { total, financeiro_id: financeiroId } });
|
||||||
schedulePush('financeiro');
|
schedulePush('financeiro');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prótese (Fase 1): cada item de GTO ligado a um produto do protético abre uma OS
|
||||||
|
// de prótese (cai na Bancada do laboratório, com etapas/fotos/garantia já existentes).
|
||||||
|
// Idempotente por gto_item_id; isolado em try/catch para nunca derrubar o finalize.
|
||||||
|
try {
|
||||||
|
const { rows: protItems } = await pool.query(
|
||||||
|
'SELECT * FROM gto_items WHERE builderid = $1 AND produto_id IS NOT NULL AND protetico_id IS NOT NULL',
|
||||||
|
[req.params.id]);
|
||||||
|
const uid = req.authUser?.userId;
|
||||||
|
for (const it of protItems) {
|
||||||
|
const { rows: jaTem } = await pool.query('SELECT id FROM protese_os WHERE gto_item_id = $1 LIMIT 1', [it.id]);
|
||||||
|
if (jaTem.length) continue;
|
||||||
|
const { rows: pr } = await pool.query(
|
||||||
|
'SELECT id, nome, preco, garantia_meses FROM protese_produtos WHERE id=$1 AND protetico_id=$2 AND deleted_at IS NULL',
|
||||||
|
[it.produto_id, it.protetico_id]);
|
||||||
|
if (!pr.length) continue;
|
||||||
|
const { rows: prot } = await pool.query("SELECT nome FROM usuarios WHERE id=$1 AND role='protetico'", [it.protetico_id]);
|
||||||
|
const osId = `pos_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO protese_os (id, clinica_id, protetico_id, protetico_nome, paciente_id, paciente_nome, dentista_id, procedimento_id, tipo, produto_id, tipo_os, garantia_meses, dentes, observacoes, valor, custo, status, created_by, gto_item_id)
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,'nova',$11,$12,$13,$14,$15,'solicitado',$16,$17)`,
|
||||||
|
[osId, b.clinica_id, it.protetico_id, prot[0]?.nome || null, b.pacienteid, b.paciente_nome,
|
||||||
|
b.dentistaid, it.procedimentoid || null, pr[0].nome, pr[0].id, Number(pr[0].garantia_meses) || 12,
|
||||||
|
it.dente || null, it.observacao || null, Number(it.valortotal) || 0, parseFloat(pr[0].preco || 0), uid, it.id]);
|
||||||
|
await pool.query(
|
||||||
|
`INSERT INTO protese_os_evento (id, os_id, clinica_id, status, observacao, actor_id, actor_nome) VALUES ($1,$2,$3,'solicitado',$4,$5,$6)`,
|
||||||
|
[`pev_${Date.now()}_${Math.random().toString(36).slice(2, 5)}`, osId, b.clinica_id, 'OS criada a partir da GTO', uid, await actorNome(uid)]);
|
||||||
|
await registrarAudit(pool, { clinicaId: b.clinica_id, entidade: 'protese_os', entidadeId: osId, acao: 'criou', actorId: uid, actorNome: await actorNome(uid), detalhes: { origem: 'gto', gto_item_id: it.id, tipo: pr[0].nome } });
|
||||||
|
await notificarUsuario(it.protetico_id, 'Nova ordem de prótese', `Você recebeu uma nova OS (${pr[0].nome}) gerada por GTO. Veja na Bancada.`);
|
||||||
|
wsBroadcast(b.clinica_id, 'protese', { entidadeId: osId, acao: 'criou' });
|
||||||
|
}
|
||||||
|
} catch (e) { console.error('[GTO→protese_os]', e.message); }
|
||||||
|
|
||||||
res.json({ success: true, financeiro_id: financeiroId });
|
res.json({ success: true, financeiro_id: financeiroId });
|
||||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
});
|
});
|
||||||
@@ -5630,6 +5663,13 @@ async function runMigrations() {
|
|||||||
`ALTER TABLE protese_os ADD COLUMN IF NOT EXISTS garantia_meses INTEGER DEFAULT 12`,
|
`ALTER TABLE protese_os ADD COLUMN IF NOT EXISTS garantia_meses INTEGER DEFAULT 12`,
|
||||||
`ALTER TABLE protese_os ADD COLUMN IF NOT EXISTS garantia_ate DATE`,
|
`ALTER TABLE protese_os ADD COLUMN IF NOT EXISTS garantia_ate DATE`,
|
||||||
`ALTER TABLE protese_os ADD COLUMN IF NOT EXISTS financeiro_id TEXT`, // DESPESA gerada na entrega
|
`ALTER TABLE protese_os ADD COLUMN IF NOT EXISTS financeiro_id TEXT`, // DESPESA gerada na entrega
|
||||||
|
`ALTER TABLE protese_os ADD COLUMN IF NOT EXISTS gto_item_id TEXT`, // item de GTO que originou a OS (Fase 1 prótese↔GTO)
|
||||||
|
// Prótese no Lançar GTO: o item de GTO referencia um produto do catálogo do protético.
|
||||||
|
`ALTER TABLE gto_items ADD COLUMN IF NOT EXISTS produto_id TEXT`,
|
||||||
|
`ALTER TABLE gto_items ADD COLUMN IF NOT EXISTS protetico_id TEXT`,
|
||||||
|
// Especialidade de prótese vinha marcada como 'odontologia' (legado) → re-tag para 'protese'
|
||||||
|
// permite o front detectar a especialidade e puxar o catálogo do protético. Idempotente.
|
||||||
|
`UPDATE especialidades SET area='protese' WHERE area IS DISTINCT FROM 'protese' AND (nome ILIKE '%protese%' OR nome ILIKE '%prótese%')`,
|
||||||
`CREATE TABLE IF NOT EXISTS protese_produtos (
|
`CREATE TABLE IF NOT EXISTS protese_produtos (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
protetico_id TEXT NOT NULL, -- dono (usuário protético)
|
protetico_id TEXT NOT NULL, -- dono (usuário protético)
|
||||||
|
|||||||
+172
-14
@@ -32,6 +32,9 @@ interface GTOItem {
|
|||||||
valorUnitario: number;
|
valorUnitario: number;
|
||||||
valorTotal: number;
|
valorTotal: number;
|
||||||
observacao?: string;
|
observacao?: string;
|
||||||
|
// Prótese (Fase 1): item ligado ao catálogo do protético → gera OS ao finalizar.
|
||||||
|
produtoId?: string;
|
||||||
|
proteticoId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GTOStore {
|
interface GTOStore {
|
||||||
@@ -119,6 +122,14 @@ export const LancarGTO: React.FC = () => {
|
|||||||
const [patientSearchOpen, setPatientSearchOpen] = useState(false);
|
const [patientSearchOpen, setPatientSearchOpen] = useState(false);
|
||||||
const [patientSearchTerm, setPatientSearchTerm] = useState('');
|
const [patientSearchTerm, setPatientSearchTerm] = useState('');
|
||||||
|
|
||||||
|
// --- PRÓTESE (Fase 1): catálogo do protético entra como "procedimentos" do GTO ---
|
||||||
|
const [laboratorios, setLaboratorios] = useState<any[]>([]);
|
||||||
|
const [selectedLab, setSelectedLab] = useState<string>('');
|
||||||
|
const [labProdutos, setLabProdutos] = useState<any[]>([]);
|
||||||
|
const [selectedProdutoId, setSelectedProdutoId] = useState<string>('');
|
||||||
|
const [protValor, setProtValor] = useState<string>('');
|
||||||
|
const [protObs, setProtObs] = useState<string>('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
const p = await HybridBackend.getPacientes();
|
const p = await HybridBackend.getPacientes();
|
||||||
@@ -140,14 +151,55 @@ export const LancarGTO: React.FC = () => {
|
|||||||
return procedimentos.filter(p => p.especialidadeId === selectedEsp);
|
return procedimentos.filter(p => p.especialidadeId === selectedEsp);
|
||||||
}, [selectedEsp, procedimentos]);
|
}, [selectedEsp, procedimentos]);
|
||||||
|
|
||||||
const filteredPatients = useMemo(() => {
|
// Especialidade de prótese → o "procedimento" vem do catálogo do protético (não da
|
||||||
if (!patientSearchTerm) return pacientes.slice(0, 30);
|
// tabela procedimentos). Detecta pela área da especialidade (re-tagueada no backend).
|
||||||
const term = normalize(patientSearchTerm);
|
const isProtese = useMemo(
|
||||||
return pacientes.filter(p =>
|
() => (especialidades.find(e => e.id === selectedEsp)?.area || '') === 'protese',
|
||||||
normalize(p.nome).includes(term) ||
|
[especialidades, selectedEsp]
|
||||||
(p.cpf && p.cpf.includes(patientSearchTerm.replace(/\D/g, '')))
|
);
|
||||||
).slice(0, 30);
|
const selectedProduto = useMemo(
|
||||||
}, [pacientes, patientSearchTerm]); // eslint-disable-line react-hooks/exhaustive-deps
|
() => labProdutos.find(p => p.id === selectedProdutoId) || null,
|
||||||
|
[labProdutos, selectedProdutoId]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Carrega os laboratórios acessíveis assim que a especialidade de prótese é escolhida.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isProtese || laboratorios.length) return;
|
||||||
|
HybridBackend.getProteseLaboratorios().then(l => setLaboratorios(Array.isArray(l) ? l : [])).catch(() => {});
|
||||||
|
}, [isProtese, laboratorios.length]);
|
||||||
|
|
||||||
|
// Catálogo do laboratório selecionado.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedLab) { setLabProdutos([]); return; }
|
||||||
|
let active = true;
|
||||||
|
HybridBackend.getLaboratorioProdutos(selectedLab)
|
||||||
|
.then(p => { if (active) setLabProdutos(Array.isArray(p) ? p : []); })
|
||||||
|
.catch(() => { if (active) setLabProdutos([]); });
|
||||||
|
return () => { active = false; };
|
||||||
|
}, [selectedLab]);
|
||||||
|
|
||||||
|
// Busca de pacientes é SERVER-SIDE: a clínica pode ter milhares e a rota sem termo
|
||||||
|
// devolve só as 50 primeiras. Filtrar essas 50 no cliente fazia a busca "não achar"
|
||||||
|
// quem estava fora da 1ª página. Sem termo, mostra a lista inicial (recentes).
|
||||||
|
const [patientResults, setPatientResults] = useState<Paciente[]>([]);
|
||||||
|
const [searchingPatients, setSearchingPatients] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const term = patientSearchTerm.trim();
|
||||||
|
if (!term) { setPatientResults(pacientes.slice(0, 30)); setSearchingPatients(false); return; }
|
||||||
|
let active = true;
|
||||||
|
setSearchingPatients(true);
|
||||||
|
const t = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const r: any = await HybridBackend.getPacientes(term);
|
||||||
|
const list: Paciente[] = Array.isArray(r) ? r : (r?.data ?? []);
|
||||||
|
if (active) setPatientResults(list);
|
||||||
|
} finally {
|
||||||
|
if (active) setSearchingPatients(false);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
return () => { active = false; clearTimeout(t); };
|
||||||
|
}, [patientSearchTerm, pacientes]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { pendingSearch } = store;
|
const { pendingSearch } = store;
|
||||||
@@ -250,6 +302,31 @@ export const LancarGTO: React.FC = () => {
|
|||||||
setArcoObs('');
|
setArcoObs('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Adiciona um item de prótese (catálogo do protético) ao rascunho do GTO.
|
||||||
|
const handleAddProtese = () => {
|
||||||
|
if (!selectedLab) { toast.error("SELECIONE O LABORATÓRIO."); return; }
|
||||||
|
if (!selectedProduto) { toast.error("SELECIONE O PRODUTO DE PRÓTESE."); return; }
|
||||||
|
if (store.items.length >= 20) { toast.error("LIMITE DE 20 ITENS ATINGIDO."); return; }
|
||||||
|
const valor = parseFloat(protValor) || Number(selectedProduto.preco) || 0;
|
||||||
|
store.addItem({
|
||||||
|
id: Math.random().toString(36).substring(7),
|
||||||
|
procedimentoId: '',
|
||||||
|
codigoTUSS: '',
|
||||||
|
codigoInterno: '',
|
||||||
|
descricao: selectedProduto.nome,
|
||||||
|
quantidade: 1,
|
||||||
|
valorUnitario: valor,
|
||||||
|
valorTotal: valor,
|
||||||
|
observacao: protObs || undefined,
|
||||||
|
produtoId: selectedProduto.id,
|
||||||
|
proteticoId: selectedLab,
|
||||||
|
});
|
||||||
|
toast.success("PRÓTESE ADICIONADA AO RASCUNHO.");
|
||||||
|
setSelectedProdutoId('');
|
||||||
|
setProtValor('');
|
||||||
|
setProtObs('');
|
||||||
|
};
|
||||||
|
|
||||||
const handleFinalize = async () => {
|
const handleFinalize = async () => {
|
||||||
if (!store.paciente || !store.dentista || store.items.length === 0) {
|
if (!store.paciente || !store.dentista || store.items.length === 0) {
|
||||||
toast.error("PACIENTE, DENTISTA E PELO MENOS 1 ITEM SÃO OBRIGATÓRIOS.");
|
toast.error("PACIENTE, DENTISTA E PELO MENOS 1 ITEM SÃO OBRIGATÓRIOS.");
|
||||||
@@ -276,9 +353,12 @@ export const LancarGTO: React.FC = () => {
|
|||||||
credenciadaId: store.credenciada,
|
credenciadaId: store.credenciada,
|
||||||
});
|
});
|
||||||
for (const it of store.items) {
|
for (const it of store.items) {
|
||||||
const { arco, id: _localId, ...rest } = it as any;
|
const { arco, id: _localId, produtoId, proteticoId, ...rest } = it as any;
|
||||||
void arco; void _localId;
|
void arco; void _localId;
|
||||||
await HybridBackend.addGtoItem(id, rest);
|
const payload: any = { ...rest };
|
||||||
|
// Prótese: envia produto/protético (snake_case) → o backend abre a OS na finalização.
|
||||||
|
if (produtoId) { payload.produto_id = produtoId; payload.protetico_id = proteticoId; }
|
||||||
|
await HybridBackend.addGtoItem(id, payload);
|
||||||
}
|
}
|
||||||
const fin = await HybridBackend.finalizarGto(id);
|
const fin = await HybridBackend.finalizarGto(id);
|
||||||
toast.success(fin?.financeiro_id
|
toast.success(fin?.financeiro_id
|
||||||
@@ -390,6 +470,9 @@ export const LancarGTO: React.FC = () => {
|
|||||||
setSelectedProc(null);
|
setSelectedProc(null);
|
||||||
setSelectedDentes([]);
|
setSelectedDentes([]);
|
||||||
setSelectedArco(null);
|
setSelectedArco(null);
|
||||||
|
setSelectedProdutoId('');
|
||||||
|
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"
|
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"
|
||||||
>
|
>
|
||||||
@@ -397,8 +480,82 @@ export const LancarGTO: React.FC = () => {
|
|||||||
{especialidades.map(es => <option key={es.id} value={es.id}>{es.nome}</option>)}
|
{especialidades.map(es => <option key={es.id} value={es.id}>{es.nome}</option>)}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
{selectedEsp && (
|
{selectedEsp && isProtese && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<select
|
||||||
|
value={selectedLab}
|
||||||
|
onChange={(e) => { setSelectedLab(e.target.value); setSelectedProdutoId(''); setProtValor(''); }}
|
||||||
|
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 O LABORATÓRIO / PROTÉTICO...</option>
|
||||||
|
{laboratorios.map(l => <option key={l.id} value={l.id}>{l.nome}{l.interno ? ' (INTERNO)' : ''}</option>)}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{selectedLab && (labProdutos.length === 0 ? (
|
||||||
|
<div className="bg-amber-50 border-2 border-amber-100 rounded-2xl p-4 text-center">
|
||||||
|
<p className="text-[11px] font-black text-amber-700 uppercase tracking-wide">Este laboratório não tem produtos ativos</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<select
|
||||||
|
value={selectedProdutoId}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSelectedProdutoId(e.target.value);
|
||||||
|
const pr = labProdutos.find(p => p.id === e.target.value);
|
||||||
|
setProtValor(pr ? String(pr.preco ?? '') : '');
|
||||||
|
}}
|
||||||
|
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 && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1.5">Valor (R$)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={protValor}
|
||||||
|
onChange={e => setProtValor(e.target.value)}
|
||||||
|
className="w-full p-4 bg-gray-50 border-2 border-gray-100 rounded-2xl font-black text-gray-800 text-sm focus:border-teal-400 outline-none transition-all"
|
||||||
|
placeholder="0,00"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
value={protObs}
|
||||||
|
onChange={e => setProtObs(e.target.value)}
|
||||||
|
rows={2}
|
||||||
|
placeholder="OBSERVAÇÃO PARA O PROTÉTICO (cor, material, dentes...)"
|
||||||
|
className="w-full p-4 bg-gray-50 border-2 border-gray-100 rounded-2xl text-[11px] focus:border-amber-400 outline-none transition-all resize-none"
|
||||||
|
/>
|
||||||
|
<div className="bg-teal-50 p-3 rounded-xl border border-teal-100">
|
||||||
|
<div className="flex items-center gap-1.5 text-[10px] font-bold text-teal-800">
|
||||||
|
<ChevronRight size={10} /> AO FINALIZAR A GTO, UMA OS DE PRÓTESE SERÁ ABERTA NA BANCADA DO LABORATÓRIO.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleAddProtese}
|
||||||
|
className="w-full bg-[#2d6a4f] text-white py-4 rounded-2xl font-black text-sm uppercase shadow-lg shadow-green-100 hover:bg-[#1b4332] active:scale-95 transition-all flex items-center justify-center gap-2 group"
|
||||||
|
>
|
||||||
|
ADICIONAR PRÓTESE <ChevronRight size={16} className="group-hover:translate-x-1 transition-transform" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedEsp && !isProtese && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
{filteredProcedimentos.length === 0 ? (
|
||||||
|
<div className="bg-amber-50 border-2 border-amber-100 rounded-2xl p-4 text-center">
|
||||||
|
<p className="text-[11px] font-black text-amber-700 uppercase tracking-wide">Nenhum procedimento cadastrado nesta especialidade</p>
|
||||||
|
<p className="text-[10px] font-bold text-amber-500 uppercase mt-1">Cadastre em Procedimentos para usá-lo aqui</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<select
|
<select
|
||||||
value={selectedProc?.id || ''}
|
value={selectedProc?.id || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -414,6 +571,7 @@ export const LancarGTO: React.FC = () => {
|
|||||||
<option value="">BUSCAR PROCEDIMENTO</option>
|
<option value="">BUSCAR PROCEDIMENTO</option>
|
||||||
{filteredProcedimentos.map(p => <option key={p.id} value={p.id}>{p.nome}</option>)}
|
{filteredProcedimentos.map(p => <option key={p.id} value={p.id}>{p.nome}</option>)}
|
||||||
</select>
|
</select>
|
||||||
|
)}
|
||||||
|
|
||||||
{selectedProc && (
|
{selectedProc && (
|
||||||
<div className="bg-teal-50 p-3 rounded-xl border border-teal-100">
|
<div className="bg-teal-50 p-3 rounded-xl border border-teal-100">
|
||||||
@@ -854,12 +1012,12 @@ export const LancarGTO: React.FC = () => {
|
|||||||
className="w-full p-4 bg-gray-50 border-2 border-gray-100 rounded-2xl font-black text-gray-800 text-sm focus:border-teal-500 outline-none transition-all uppercase"
|
className="w-full p-4 bg-gray-50 border-2 border-gray-100 rounded-2xl font-black text-gray-800 text-sm focus:border-teal-500 outline-none transition-all uppercase"
|
||||||
/>
|
/>
|
||||||
<div className="max-h-72 overflow-y-auto space-y-1">
|
<div className="max-h-72 overflow-y-auto space-y-1">
|
||||||
{filteredPatients.length === 0 ? (
|
{patientResults.length === 0 ? (
|
||||||
<div className="py-10 text-center text-gray-300 font-bold uppercase text-xs">
|
<div className="py-10 text-center text-gray-300 font-bold uppercase text-xs">
|
||||||
{patientSearchTerm ? 'NENHUM PACIENTE ENCONTRADO' : 'CARREGANDO...'}
|
{searchingPatients ? 'BUSCANDO...' : (patientSearchTerm ? 'NENHUM PACIENTE ENCONTRADO' : 'CARREGANDO...')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
filteredPatients.map(p => (
|
patientResults.map(p => (
|
||||||
<button
|
<button
|
||||||
key={p.id}
|
key={p.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user