Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d357f3a2e8 | |||
| 2e90a2ba5f | |||
| 53cf2c52e6 |
+27
-1
@@ -4146,6 +4146,29 @@ app.get('/api/protese/lab/indicadores', authGuard, async (req, res) => {
|
|||||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
} 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).
|
// 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) => {
|
app.post('/api/protese/os/:id/link', authGuard, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -7186,7 +7209,10 @@ app.get('/api/profissionais/marketplace', authGuard, async (req, res) => {
|
|||||||
const { rows } = await pool.query(
|
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}
|
`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);
|
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 }); }
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
> Acoplamentos respeitados: PF/PJ veio com a equipe (Fase 4); Score só após a captura (Fases 2-4).
|
||||||
|
|
||||||
### Fora de escopo (futuro)
|
### Pós-roadmap (entregue depois das Fases 1-5)
|
||||||
Marketplace **transacional** (propostas/contratação dentro da plataforma) e o Modo 1 (protético sem
|
- **✅ Modo 1** (protético sem conta via link `/p/TOKEN`) — completo (1a leitura, 1b ação, 1c gestão).
|
||||||
conta via link seguro `/p/TOKEN`) seguem como evolução — a base de dados/score já os suporta.
|
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.
|
||||||
|
|||||||
+3
-3
@@ -9,7 +9,7 @@
|
|||||||
### 1. Onde está o código?
|
### 1. Onde está o código?
|
||||||
- **Fonte da verdade:** Gitea na **VPS3**, repo `ruicesar/scoreodonto.com`.
|
- **Fonte da verdade:** Gitea na **VPS3**, repo `ruicesar/scoreodonto.com`.
|
||||||
- **Dev (trabalho):** VPS4 em `/home/deploy/stack/scoreodonto.com`.
|
- **Dev (trabalho):** VPS4 em `/home/deploy/stack/scoreodonto.com`.
|
||||||
- **Produção:** VPS1 roda **imagens versionadas do registry** (não código-fonte). No ar: **v1.0.24 · commit `28dc27a`**.
|
- **Produção:** VPS1 roda **imagens versionadas do registry** (não código-fonte). No ar: **v1.0.25 · commit `8040e70`**.
|
||||||
- **Documentação:** centralizada em `scoreodonto.com/doc/` (pasta única).
|
- **Documentação:** centralizada em `scoreodonto.com/doc/` (pasta única).
|
||||||
|
|
||||||
### 2. Onde está o banco?
|
### 2. Onde está o banco?
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
- ✅ **Build Once → Promote (Passo 5, validado em produção):** push builda 1× e publica por **commit** (`:<sha>`); a **tag re-tagueia** esse artefato como `:vX.Y.Z` (sem rebuild — digest idêntico) e deploya. Provado em v1.0.16 (`:v1.0.16` == `:471c2c2`, mesmo digest).
|
- ✅ **Build Once → Promote (Passo 5, validado em produção):** push builda 1× e publica por **commit** (`:<sha>`); a **tag re-tagueia** esse artefato como `:vX.Y.Z` (sem rebuild — digest idêntico) e deploya. Provado em v1.0.16 (`:v1.0.16` == `:471c2c2`, mesmo digest).
|
||||||
|
|
||||||
### 5. Como versiona?
|
### 5. Como versiona?
|
||||||
- A versão exibida vem do **backend em runtime**: `GET /api/version` (de `process.env`), mostrada em **Configurações → "Sobre o sistema"** e nas telas públicas (o frontend consome via `useAppVersion()`, não embute mais a versão). No ar: **v1.0.24 · `28dc27a` · PROD**.
|
- A versão exibida vem do **backend em runtime**: `GET /api/version` (de `process.env`), mostrada em **Configurações → "Sobre o sistema"** e nas telas públicas (o frontend consome via `useAppVersion()`, não embute mais a versão). No ar: **v1.0.25 · `8040e70` · PROD**.
|
||||||
- A versão **nasce da tag git** e é **injetada no deploy em runtime** (`APP_VERSION`=tag, `APP_ENV=PROD`); `commit`/`build` vêm carimbados na imagem.
|
- A versão **nasce da tag git** e é **injetada no deploy em runtime** (`APP_VERSION`=tag, `APP_ENV=PROD`); `commit`/`build` vêm carimbados na imagem.
|
||||||
- ✅ **`constants.ts`/`update-version.js` foram removidos** (Passo 5) — não há mais versão manual.
|
- ✅ **`constants.ts`/`update-version.js` foram removidos** (Passo 5) — não há mais versão manual.
|
||||||
|
|
||||||
@@ -39,4 +39,4 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
## Veredito de uma linha
|
## Veredito de uma linha
|
||||||
> O código vive no Gitea (VPS3); a **produção (VPS1) roda imagens versionadas do registry** (hoje **v1.0.24**), sem buildar; o **DEV está isolado no `pgdev`** (não toca o banco de produção). **CI/CD por tag ativo** no modelo **Build Once → Promote → Deploy** (push builda por commit; tag re-tagueia sem rebuild e deploya — validado em v1.0.16), com **versão visível em runtime** (`/api/version` → Configurações) nascendo da tag git.
|
> O código vive no Gitea (VPS3); a **produção (VPS1) roda imagens versionadas do registry** (hoje **v1.0.25**), sem buildar; o **DEV está isolado no `pgdev`** (não toca o banco de produção). **CI/CD por tag ativo** no modelo **Build Once → Promote → Deploy** (push builda por commit; tag re-tagueia sem rebuild e deploya — validado em v1.0.16), com **versão visível em runtime** (`/api/version` → Configurações) nascendo da tag git.
|
||||||
|
|||||||
+1
-1
@@ -329,7 +329,7 @@ const App: React.FC = () => {
|
|||||||
paciente: ['tratamentos', 'notificacoes', 'configuracoes'],
|
paciente: ['tratamentos', 'notificacoes', 'configuracoes'],
|
||||||
dentista: ['dashboard', 'agenda', 'ortodontia', 'tratamentos', 'lancar-gto', 'proteses', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'rx-radiografias', 'salas', 'marketplace-profissionais', 'candidatura-tutor', 'tutoria-painel', 'diretorio-profissionais'],
|
dentista: ['dashboard', 'agenda', 'ortodontia', 'tratamentos', 'lancar-gto', 'proteses', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'rx-radiografias', 'salas', 'marketplace-profissionais', 'candidatura-tutor', 'tutoria-painel', 'diretorio-profissionais'],
|
||||||
biomedico: ['dashboard', 'agenda', 'tratamentos', 'lancar-gto', 'especialidades', 'procedimentos', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'salas', 'marketplace-profissionais', 'diretorio-profissionais'],
|
biomedico: ['dashboard', 'agenda', 'tratamentos', 'lancar-gto', 'especialidades', 'procedimentos', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'salas', 'marketplace-profissionais', 'diretorio-profissionais'],
|
||||||
protetico: ['dashboard', 'agenda', 'tratamentos', 'lancar-gto', 'proteses', 'lab-home', 'lab-bancada', 'lab-clientes', 'lab-financeiro', 'lab-equipe', 'lab-indicadores', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'salas', 'marketplace-profissionais', 'diretorio-profissionais'],
|
protetico: ['lab-home', 'lab-bancada', 'lab-clientes', 'lab-financeiro', 'lab-equipe', 'lab-indicadores', 'notificacoes', 'clinicas', 'configuracoes', 'plugins', 'salas', 'marketplace-profissionais', 'diretorio-profissionais'],
|
||||||
funcionario: ['dashboard', 'leads', 'pacientes', 'agenda', 'financeiro', 'tratamentos', 'lancar-gto', 'proteses', 'relatorios', 'notificacoes', 'clinicas', 'configuracoes', 'orcamentos', 'contratos', 'plugins', 'rx-radiografias', 'salas', 'marketplace-profissionais', 'candidatura-tutor', 'diretorio-profissionais'],
|
funcionario: ['dashboard', 'leads', 'pacientes', 'agenda', 'financeiro', 'tratamentos', 'lancar-gto', 'proteses', 'relatorios', 'notificacoes', 'clinicas', 'configuracoes', 'orcamentos', 'contratos', 'plugins', 'rx-radiografias', 'salas', 'marketplace-profissionais', 'candidatura-tutor', 'diretorio-profissionais'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -149,7 +149,9 @@ export const Sidebar: React.FC<SidebarProps> = ({ activeTab, setActiveTab }) =>
|
|||||||
// SEM itens odontológicos (ortodontia, RX, tutoria orto).
|
// SEM itens odontológicos (ortodontia, RX, tutoria orto).
|
||||||
if (currentRole === 'biomedico') return ['dashboard', 'agenda', 'tratamentos', 'especialidades', 'procedimentos', 'notificacoes', 'clinicas', 'configuracoes', 'diretorio-profissionais'].includes(item.id);
|
if (currentRole === 'biomedico') return ['dashboard', 'agenda', 'tratamentos', 'especialidades', 'procedimentos', 'notificacoes', 'clinicas', 'configuracoes', 'diretorio-profissionais'].includes(item.id);
|
||||||
// Protético: laboratório — agenda própria + GTO; sem ortodontia/RX/pacientes clínicos.
|
// Protético: laboratório — agenda própria + GTO; sem ortodontia/RX/pacientes clínicos.
|
||||||
if (currentRole === 'protetico') return ['dashboard', 'agenda', 'proteses', 'tratamentos', 'lancar-gto', 'notificacoes', 'clinicas', 'configuracoes', 'diretorio-profissionais'].includes(item.id);
|
// Protético: opera dentro do workspace 'laboratorio' (menu governado pelo branch tipo acima).
|
||||||
|
// Este é só o fallback fora desse contexto — sem telas clínicas (agenda/GTO/tratamentos).
|
||||||
|
if (currentRole === 'protetico') return ['notificacoes', 'clinicas', 'configuracoes', 'diretorio-profissionais'].includes(item.id);
|
||||||
if (currentRole === 'funcionario') return ['dashboard', 'leads', 'pacientes', 'tratamentos', 'lancar-gto', 'proteses', 'agenda', 'financeiro', 'relatorios', 'glosas', 'notificacoes', 'clinicas', 'configuracoes', 'orcamentos', 'contratos', 'diretorio-profissionais'].includes(item.id);
|
if (currentRole === 'funcionario') return ['dashboard', 'leads', 'pacientes', 'tratamentos', 'lancar-gto', 'proteses', 'agenda', 'financeiro', 'relatorios', 'glosas', 'notificacoes', 'clinicas', 'configuracoes', 'orcamentos', 'contratos', 'diretorio-profissionais'].includes(item.id);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ interface Profissional {
|
|||||||
raio_atuacao_km?: number | null;
|
raio_atuacao_km?: number | null;
|
||||||
bio_profissional?: string | null;
|
bio_profissional?: string | null;
|
||||||
dist_km?: number | string | null;
|
dist_km?: number | string | null;
|
||||||
|
nota?: number | null; // Nota ScoreOdonto (só protéticos com volume mínimo)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Proposta {
|
interface Proposta {
|
||||||
@@ -699,6 +700,7 @@ export const ProfissionaisPlugin: React.FC = () => {
|
|||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<span className="font-black text-gray-900 text-sm uppercase truncate">{p.nome}</span>
|
<span className="font-black text-gray-900 text-sm uppercase truncate">{p.nome}</span>
|
||||||
<span className={`text-[9px] font-black px-2 py-0.5 rounded-full uppercase ${meta.cls}`}>{meta.label}</span>
|
<span className={`text-[9px] font-black px-2 py-0.5 rounded-full uppercase ${meta.cls}`}>{meta.label}</span>
|
||||||
|
{p.nota != null && <span className="text-[9px] font-black px-2 py-0.5 rounded-full bg-amber-100 text-amber-700 uppercase" title="Nota ScoreOdonto">★ {p.nota}</span>}
|
||||||
</div>
|
</div>
|
||||||
{p.especialidade && <p className="text-xs text-gray-500 font-medium mt-0.5 uppercase">{p.especialidade}</p>}
|
{p.especialidade && <p className="text-xs text-gray-500 font-medium mt-0.5 uppercase">{p.especialidade}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user