diff --git a/backend/server.js b/backend/server.js index 2d6aa39..f340eca 100644 --- a/backend/server.js +++ b/backend/server.js @@ -4146,6 +4146,29 @@ app.get('/api/protese/lab/indicadores', authGuard, async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } }); +// Nota ScoreOdonto por protético (mapa id→nota), p/ ranking no dropdown e no marketplace. +async function notasScorePorProtetico(ids) { + const notas = {}; + if (!ids || !ids.length) return notas; + const { rows: ag } = await pool.query( + `SELECT protetico_id, + COUNT(*) FILTER (WHERE status='entregue')::int AS entregues, + COUNT(*) FILTER (WHERE status='entregue' AND tipo_os='garantia')::int AS garantia, + COUNT(*) FILTER (WHERE status='entregue' AND prazo_entrega IS NOT NULL AND updated_at::date > prazo_entrega)::int AS atrasados + FROM protese_os WHERE protetico_id = ANY($1) AND deleted_at IS NULL GROUP BY protetico_id`, [ids]); + const { rows: ocs } = await pool.query( + `SELECT o.protetico_id, COUNT(DISTINCT oc.os_id)::int AS n FROM protese_os_ocorrencia oc JOIN protese_os o ON o.id = oc.os_id WHERE oc.status='resolvida' AND o.protetico_id = ANY($1) GROUP BY o.protetico_id`, [ids]); + const ocMap = Object.fromEntries(ocs.map(o => [o.protetico_id, o.n])); + for (const g of ag) { + if (g.entregues >= SCORE_MIN_OS) { + const pct = n => g.entregues > 0 ? n / g.entregues * 100 : 0; + const nota = 10 - (pct(g.atrasados) / 100 * 3 + pct(g.garantia) / 100 * 4 + pct(ocMap[g.protetico_id] || 0) / 100 * 3); + notas[g.protetico_id] = Math.round(Math.max(0, Math.min(10, nota)) * 10) / 10; + } + } + return notas; +} + // MODO 1 — gerar/recuperar o link seguro de acompanhamento da OS (clínica ou lab). app.post('/api/protese/os/:id/link', authGuard, async (req, res) => { try { @@ -7186,7 +7209,10 @@ app.get('/api/profissionais/marketplace', authGuard, async (req, res) => { const { rows } = await pool.query( `SELECT id, nome, email, celular, role, estado, cidade, bairro, cep, especialidade_dir AS especialidade, raio_atuacao_km, bio_profissional, ${distSelect} FROM usuarios ${where} ${order} LIMIT 200`, params); - res.json(rows); + // Nota ScoreOdonto nos protéticos (score público no marketplace). Mantém a ordem por + // proximidade; a nota é exibida no card para a clínica comparar. + const notas = await notasScorePorProtetico(rows.filter(r => r.role === 'protetico').map(r => r.id)); + res.json(rows.map(r => ({ ...r, nota: r.role === 'protetico' ? (notas[r.id] ?? null) : undefined }))); } catch (err) { res.status(500).json({ error: err.message }); } }); diff --git a/doc/PROVEDOR-PROTESE-DECISAO.md b/doc/PROVEDOR-PROTESE-DECISAO.md index 7c190ea..a0e3f9e 100644 --- a/doc/PROVEDOR-PROTESE-DECISAO.md +++ b/doc/PROVEDOR-PROTESE-DECISAO.md @@ -106,6 +106,16 @@ Score/ranking/marketplace. Não codar indicador antes de existir dado. > Acoplamentos respeitados: PF/PJ veio com a equipe (Fase 4); Score só após a captura (Fases 2-4). -### Fora de escopo (futuro) -Marketplace **transacional** (propostas/contratação dentro da plataforma) e o Modo 1 (protético sem -conta via link seguro `/p/TOKEN`) seguem como evolução — a base de dados/score já os suporta. +### Pós-roadmap (entregue depois das Fases 1-5) +- **✅ Modo 1** (protético sem conta via link `/p/TOKEN`) — completo (1a leitura, 1b ação, 1c gestão). + Ver `MODO1-LINK-SEGURO-PROTESE.md`. +- **✅ Higiene** — menu legado do protético removido (Sidebar/App): o protético opera só no workspace + laboratório; fallback mínimo fora dele. +- **✅ Score público no marketplace** — `GET /api/profissionais/marketplace` traz a `nota` ScoreOdonto + dos protéticos (helper `notasScorePorProtetico`, mesma fórmula da Fase 5), exibida como ★ no card. + Ordem mantida por proximidade; a nota é critério de comparação visível. + +### Ainda em aberto (futuro) +Marketplace **transacional** (anúncio de serviços + solicitação/contratação/propostas dentro da +plataforma); **agenda de produção/coleta**; **multi-unidade** do laboratório; envio automático do +link (e-mail/WhatsApp API) e herança de cadastro novo com e-mail diferente. diff --git a/frontend/views/plugins/ProfissionaisPlugin.tsx b/frontend/views/plugins/ProfissionaisPlugin.tsx index c9618e9..806963a 100644 --- a/frontend/views/plugins/ProfissionaisPlugin.tsx +++ b/frontend/views/plugins/ProfissionaisPlugin.tsx @@ -44,6 +44,7 @@ interface Profissional { raio_atuacao_km?: number | null; bio_profissional?: string | null; dist_km?: number | string | null; + nota?: number | null; // Nota ScoreOdonto (só protéticos com volume mínimo) } interface Proposta { @@ -699,6 +700,7 @@ export const ProfissionaisPlugin: React.FC = () => {
{p.nome} {meta.label} + {p.nota != null && ★ {p.nota}}
{p.especialidade &&

{p.especialidade}

}