diff --git a/doc/MARKETPLACE-TRANSACIONAL-PROTESE.md b/doc/MARKETPLACE-TRANSACIONAL-PROTESE.md new file mode 100644 index 0000000..05a6cad --- /dev/null +++ b/doc/MARKETPLACE-TRANSACIONAL-PROTESE.md @@ -0,0 +1,45 @@ +# Marketplace transacional de Prótese + +> Desenho (24/jun/2026). O último grande item do backlog do Provedor de Prótese. +> Princípio: **transformar o diretório (descoberta) em jornada de compra** — descobrir → ver +> serviços/preços → solicitar → cotar → contratar → vira OS. Reaproveita ao máximo o que já existe. + +## O que já existe (não recriar) +- **Descoberta:** `/api/profissionais/marketplace` (busca por proximidade/especialidade) **+ Nota + ScoreOdonto** no card (feito). +- **Catálogo:** `protese_produtos` (nome, preço, garantia) + `GET /api/protese/laboratorios/:id/produtos` + — já acessível a quem está no **diretório** (`proteticoAcessivelPelaClinica` aceita `disponivel_diretorio`). +- **Propostas:** `propostas_profissional` (clínica→profissional: mensagem, `pendente→aceita/recusada`). +- **Contratação:** a clínica **já cria OS para qualquer protético do diretório** (Nova OS). +- **Pós-venda:** OS completa (etapas, custódia, ocorrências), financeiro, score — tudo pronto. + +**Conclusão:** o "transacional" não exige nova fundação — falta **costurar a jornada** dentro do +marketplace (hoje a clínica vê nome/nota/distância, mas não os serviços, e contrata por outra tela). + +## Fatias + +### ✅ Fatia 1 — Vitrine (catálogo no marketplace) — IMPLEMENTADA +No card do protético no marketplace, **"Ver catálogo"** expande os produtos (nome, preço, garantia) +via `getLaboratorioProdutos(id)` (reuso — zero endpoint novo). A clínica passa a **decidir vendo +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 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); +a clínica escolhe e vira OS. Estende `propostas_profissional` com itens de prótese e valores, ou nova +`protese_rfq` + `protese_rfq_cotacao`. + +### Fatia 4 — Reputação pública e conversão +Avaliação da clínica ao final da OS (alimenta a Nota, hoje só operacional), selo "lab verificado", +ordenar marketplace por nota dentro do raio. + +## Decisões de modelagem +- **Catálogo público = catálogo atual** (`protese_produtos`); a vitrine só o exibe — sem duplicar. +- **Solicitação reusa a OS** (não cria entidade paralela): toda contratação termina em `protese_os`, + preservando rastreabilidade/financeiro/score. +- **Cotação (Fatia 3)** é o único caso que precisa de entidade nova (pré-OS multi-lab). diff --git a/frontend/views/plugins/ProfissionaisPlugin.tsx b/frontend/views/plugins/ProfissionaisPlugin.tsx index 806963a..983a474 100644 --- a/frontend/views/plugins/ProfissionaisPlugin.tsx +++ b/frontend/views/plugins/ProfissionaisPlugin.tsx @@ -2,8 +2,44 @@ import React, { useState, useEffect, useCallback } from 'react'; import { UserSearch, Stethoscope, Wrench, Sparkles, MapPin, Search, Mail, Phone, RefreshCw, Save, BadgeCheck, Inbox, Send, CheckCircle2, XCircle, CircleDollarSign, UserCircle2, X, - ChevronUp, Eraser, FileText, Plus, Pencil, Trash2 + ChevronUp, Eraser, FileText, Plus, Pencil, Trash2, Tags, ChevronDown } from 'lucide-react'; + +const fmtBRLp = (v: number) => 'R$ ' + Number(v || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 }); + +// Fatia 1 do marketplace transacional: vitrine do catálogo do laboratório (reusa getLaboratorioProdutos). +const VitrineProtetico: React.FC<{ proteticoId: string }> = ({ proteticoId }) => { + const [aberto, setAberto] = useState(false); + const [produtos, setProdutos] = useState(null); + const [loading, setLoading] = useState(false); + const toggle = async () => { + if (!aberto && produtos === null) { + setLoading(true); + try { const p = await HybridBackend.getLaboratorioProdutos(proteticoId); setProdutos(Array.isArray(p) ? p : []); } + catch { setProdutos([]); } finally { setLoading(false); } + } + setAberto(a => !a); + }; + return ( +
+ + {aberto && ( +
+ {loading ?

Carregando…

+ : produtos && produtos.length ? produtos.map(pr => ( +
+ {pr.nome} + {pr.garantia_meses ? {pr.garantia_meses}m gar. : null} + {fmtBRLp(pr.preco)} +
+ )) :

Sem catálogo publicado.

} +
+ )} +
+ ); +}; import { PageHeader } from '../../components/PageHeader.tsx'; import { useToast } from '../../contexts/ToastContext.tsx'; import { useConfirm } from '../../contexts/ConfirmContext.tsx'; @@ -723,10 +759,11 @@ export const ProfissionaisPlugin: React.FC = () => { )} + {p.role === 'protetico' && } {ws?.id && ( )}