From 6a88995146d23d4cd89e29177d3db7eb457d3164 Mon Sep 17 00:00:00 2001 From: VPS 4 Builder Date: Wed, 24 Jun 2026 07:56:33 +0200 Subject: [PATCH] =?UTF-8?q?feat(protese):=20marketplace=20transacional=20f?= =?UTF-8?q?atia=202=20=E2=80=94=20solicitar=20pr=C3=B3tese=20cria=20a=20OS?= =?UTF-8?q?=20direto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Solicitar prótese" no card do protético abre o SolicitarProteseModal (lab pré-selecionado): produto do catálogo (ou livre) + paciente + dentista + dentes + prazo + obs → cria a OS via createProteseOS (que já aceita lab do diretório, sem vínculo prévio). Confirmação com CTA p/ a Bancada. Modal auto-contido reusando getLaboratorioProdutos/getDentistas/getPacientes/createProteseOS. Validado em DEV: OS criada para lab do diretório (Coroa de Zircônia, status solicitado). Co-Authored-By: Claude Opus 4.8 --- doc/MARKETPLACE-TRANSACIONAL-PROTESE.md | 11 +- .../views/plugins/ProfissionaisPlugin.tsx | 110 +++++++++++++++++- 2 files changed, 116 insertions(+), 5 deletions(-) diff --git a/doc/MARKETPLACE-TRANSACIONAL-PROTESE.md b/doc/MARKETPLACE-TRANSACIONAL-PROTESE.md index 05a6cad..fa6e4c8 100644 --- a/doc/MARKETPLACE-TRANSACIONAL-PROTESE.md +++ b/doc/MARKETPLACE-TRANSACIONAL-PROTESE.md @@ -24,10 +24,13 @@ via `getLaboratorioProdutos(id)` (reuso — zero endpoint novo). A clínica pass serviços + preço + garantia + ★nota** antes de contratar. Botão de contato vira **"Solicitar prótese"** (proposta com contexto de prótese). -### Fatia 2 — Solicitação → OS direto -"Solicitar prótese" abre um fluxo enxuto (produto do catálogo + paciente + dentes + prazo + obs) que -**cria a OS** já com o lab selecionado (reusa `POST /api/protese/os`). Requer extrair o seletor de -paciente/dentista da Nova OS para um componente reutilizável fora da tela de Prótese. +### ✅ Fatia 2 — Solicitação → OS direto — IMPLEMENTADA +"Solicitar prótese" (card do protético) abre o `SolicitarProteseModal` — fluxo enxuto com o lab +**pré-selecionado**: produto do catálogo (ou descrição livre) + paciente (busca) + dentista + dentes ++ prazo + observações → **cria a OS** via `createProteseOS` (que já aceita lab do diretório, sem +vínculo prévio). Tela de confirmação com CTA para a Bancada. Optei por um modal próprio e auto-contido +(reusa getLaboratorioProdutos/getDentistas/getPacientes/createProteseOS) em vez de extrair a Nova OS — +menor risco. Validado em DEV: OS criada para lab do diretório (Coroa de Zircônia, status solicitado). ### Fatia 3 — Cotação (RFQ) para trabalho sob medida Quando não há produto de catálogo: a clínica descreve o trabalho e **N labs cotam** (preço + prazo); diff --git a/frontend/views/plugins/ProfissionaisPlugin.tsx b/frontend/views/plugins/ProfissionaisPlugin.tsx index 983a474..d40f698 100644 --- a/frontend/views/plugins/ProfissionaisPlugin.tsx +++ b/frontend/views/plugins/ProfissionaisPlugin.tsx @@ -7,6 +7,112 @@ import { const fmtBRLp = (v: number) => 'R$ ' + Number(v || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 }); +// Fatia 2: solicitar prótese direto do marketplace → cria a OS (lab pré-selecionado). +const SolicitarProteseModal: React.FC<{ prof: any; onClose: () => void }> = ({ prof, onClose }) => { + const ws = HybridBackend.getActiveWorkspace(); + const [produtos, setProdutos] = useState([]); + const [dentistas, setDentistas] = useState([]); + const [busca, setBusca] = useState(''); + const [pacientes, setPacientes] = useState([]); + const [paciente, setPaciente] = useState(null); + const [form, setForm] = useState({ produto_id: '', tipo: '', dentista_id: '', dentes: '', prazo_entrega: '', observacoes: '' }); + const [saving, setSaving] = useState(false); + const [okMsg, setOkMsg] = useState(false); + + useEffect(() => { + HybridBackend.getLaboratorioProdutos(prof.id).then(r => setProdutos(Array.isArray(r) ? r : [])).catch(() => setProdutos([])); + HybridBackend.getDentistas(ws?.id).then(r => setDentistas(Array.isArray(r) ? r : [])).catch(() => setDentistas([])); + }, [prof.id, ws?.id]); + useEffect(() => { + if (paciente) return; + const t = setTimeout(() => { HybridBackend.getPacientes(busca).then(r => setPacientes(r.data || [])).catch(() => setPacientes([])); }, 250); + return () => clearTimeout(t); + }, [busca, paciente]); + const set = (k: string, v: any) => setForm((f: any) => ({ ...f, [k]: v })); + + const salvar = async () => { + if (!form.produto_id && !form.tipo.trim()) { alert('Escolha um produto do catálogo ou descreva o trabalho.'); return; } + const dent = dentistas.find(d => d.id === form.dentista_id); + setSaving(true); + try { + await HybridBackend.createProteseOS({ + ...form, protetico_id: prof.id, valor: 0, + paciente_id: paciente?.id || null, paciente_nome: paciente?.nome || null, dentista_nome: dent?.nome || null, + }); + setOkMsg(true); + } catch (e: any) { alert((e.message || 'Erro').toUpperCase()); } finally { setSaving(false); } + }; + + return ( +
+
e.stopPropagation()}> +
+

Solicitar prótese

+ +
+ {okMsg ? ( +
+ +

OS enviada ao laboratório

+

{prof.nome} recebeu a solicitação. Acompanhe em Prótese / Bancada.

+ +
+ ) : ( +
+
Laboratório: {prof.nome}
+
+ + {produtos.length > 0 ? ( + + ) : ( + set('tipo', e.target.value)} placeholder="Descreva o trabalho (sem catálogo)" className="w-full mt-1 px-3 py-2 rounded-xl border border-gray-200 text-sm" /> + )} +
+
+ + {paciente ? ( +
{paciente.nome}
+ ) : ( +
+ + setBusca(e.target.value)} placeholder="Buscar paciente..." className="w-full pl-9 pr-3 py-2 rounded-xl border border-gray-200 text-sm" /> + {busca && pacientes.length > 0 && ( +
+ {pacientes.slice(0, 8).map(p => )} +
+ )} +
+ )} +
+
+
+ + +
+
+ + set('prazo_entrega', e.target.value)} className="w-full mt-1 px-3 py-2 rounded-xl border border-gray-200 text-sm" /> +
+
+
+ + set('dentes', e.target.value)} placeholder="Ex.: 11, 21" className="w-full mt-1 px-3 py-2 rounded-xl border border-gray-200 text-sm" /> +