feat(protese): seletor de protético em modal (busca, favorito/padrão, vincular por e-mail)

O dropdown só listava protéticos internos (vinculados) ou do diretório público — um protético com
conta fora do diretório (ex.: claudemir) não aparecia para a clínica. Reformulado:

- Componente ProteticoPicker (modal): busca por nome/e-mail; estrela de FAVORITO; marcar PADRÃO
  (pré-selecionado na próxima OS/GTO, só um por clínica); e "Adicionar por e-mail", que VINCULA um
  protético existente como interno — resolve o caso de não estar no diretório, sem expô-lo publicamente.
- Backend: tabela protese_protetico_pref (favorito/padrão por clínica); GET /protese/laboratorios
  passa a retornar favorito/padrao e ordena padrão→favorito→interno→nota→nome; POST .../:id/pref e
  POST /protese/laboratorios/vincular.
- Picker aplicado no LancarGTO (avaliação/GTO) e na NovaOSModal; ProteticoInternoInline (protético
  SEM conta) mantido ao lado.

Validado em DEV na CONSULTT CLINIC: claudemir ausente → vinculado por e-mail → aparece interno e,
como favorito+padrão, no topo da lista.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Builder
2026-06-25 07:18:32 +02:00
parent 8353a9d897
commit e6d8b532e3
5 changed files with 205 additions and 30 deletions
+2 -15
View File
@@ -13,6 +13,7 @@ import {
X
} from 'lucide-react';
import { PageHeader } from '../components/PageHeader.tsx';
import { ProteticoPicker } from '../components/ProteticoPicker.tsx';
import { HybridBackend } from '../services/backend.ts';
import { Paciente, Dentista, Procedimento, Especialidade } from '../types.ts';
import { useToast } from '../contexts/ToastContext.tsx';
@@ -123,7 +124,6 @@ export const LancarGTO: React.FC = () => {
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>('');
@@ -162,12 +162,6 @@ export const LancarGTO: React.FC = () => {
[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; }
@@ -482,14 +476,7 @@ export const LancarGTO: React.FC = () => {
{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)' : ''}{l.nota != null ? ` · ★ ${l.nota}` : ''}</option>)}
</select>
<ProteticoPicker value={selectedLab} onChange={(id) => { setSelectedLab(id); setSelectedProdutoId(''); setProtValor(''); }} />
{selectedLab && (labProdutos.length === 0 ? (
<div className="bg-amber-50 border-2 border-amber-100 rounded-2xl p-4 text-center">