diff --git a/backend/server.js b/backend/server.js index c791aba..e284501 100644 --- a/backend/server.js +++ b/backend/server.js @@ -198,6 +198,16 @@ function authGuard(req, res, next) { } } +// Biomédicos não gerenciam procedimentos/especialidades (são conceitos odontológicos). +// Camada de backend além do menu/permissão do front. Fail-open só em erro de DB. +async function blockBiomedico(req, res, next) { + try { + const { rows } = await pool.query('SELECT role FROM usuarios WHERE id = $1', [req.authUser?.userId]); + if (rows[0]?.role === 'biomedico') return res.status(403).json({ error: 'Biomédicos não podem gerenciar procedimentos e especialidades.' }); + } catch (e) { console.error('[blockBiomedico]', e.message); } + next(); +} + // ============================================================================= // AUDITORIA GENÉRICA + PENDÊNCIAS DE AGENDA (Fase 1) // ============================================================================= @@ -2598,7 +2608,7 @@ app.get('/api/especialidades', authGuard, async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } }); -app.post('/api/especialidades', authGuard, async (req, res) => { +app.post('/api/especialidades', authGuard, blockBiomedico, async (req, res) => { try { const { rows: userRows } = await pool.query('SELECT role FROM usuarios WHERE id = $1', [req.authUser.userId]); const isSuperAdmin = userRows[0]?.role === 'superadmin'; @@ -2625,7 +2635,7 @@ app.put('/api/especialidades/reorder', async (req, res) => { } }); -app.put('/api/especialidades/:id', async (req, res) => { +app.put('/api/especialidades/:id', authGuard, blockBiomedico, async (req, res) => { try { const upd = buildUpdate('especialidades', req.body, 'id', req.params.id); await pool.query(upd.text, upd.values); @@ -2633,7 +2643,7 @@ app.put('/api/especialidades/:id', async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } }); -app.delete('/api/especialidades/:id', tenantGuard, async (req, res) => { +app.delete('/api/especialidades/:id', tenantGuard, blockBiomedico, async (req, res) => { try { const clinicaId = req.clinicaId; if (!clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' }); @@ -2714,7 +2724,7 @@ app.get('/api/procedimentos', authGuard, async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } }); -app.post('/api/procedimentos', authGuard, async (req, res) => { +app.post('/api/procedimentos', authGuard, blockBiomedico, async (req, res) => { try { const { rows: userRows } = await pool.query('SELECT role FROM usuarios WHERE id = $1', [req.authUser.userId]); const isSuperAdmin = userRows[0]?.role === 'superadmin'; @@ -2772,7 +2782,7 @@ app.put('/api/procedimentos/reorder', async (req, res) => { } }); -app.put('/api/procedimentos/:id', async (req, res) => { +app.put('/api/procedimentos/:id', authGuard, blockBiomedico, async (req, res) => { try { const { valoresPlanos, ...proc } = req.body; const updateData = { ...proc, exige_face: proc.exige_face ? 1 : 0 }; @@ -2794,7 +2804,7 @@ app.put('/api/procedimentos/:id', async (req, res) => { } }); -app.delete('/api/procedimentos/:id', tenantGuard, async (req, res) => { +app.delete('/api/procedimentos/:id', tenantGuard, blockBiomedico, async (req, res) => { try { const clinicaId = req.clinicaId; if (!clinicaId) return res.status(400).json({ error: 'clinicaId obrigatório.' }); @@ -4011,6 +4021,8 @@ async function runMigrations() { // Coluna geográfica (ponto WGS84) p/ busca por raio em salas e profissionais. `ALTER TABLE salas ADD COLUMN IF NOT EXISTS geo geography(Point,4326)`, `ALTER TABLE usuarios ADD COLUMN IF NOT EXISTS geo geography(Point,4326)`, + `ALTER TABLE usuarios ADD COLUMN IF NOT EXISTS logradouro TEXT`, + `ALTER TABLE usuarios ADD COLUMN IF NOT EXISTS numero TEXT`, `ALTER TABLE clinicas ADD COLUMN IF NOT EXISTS geo geography(Point,4326)`, `CREATE INDEX IF NOT EXISTS idx_salas_geo ON salas USING GIST (geo)`, `CREATE INDEX IF NOT EXISTS idx_usuarios_geo ON usuarios USING GIST (geo)`, @@ -4510,7 +4522,7 @@ const MARKETPLACE_ROLES = ['dentista', 'biomedico', 'protetico']; app.get('/api/profissionais/perfil', authGuard, async (req, res) => { try { const { rows } = await pool.query( - `SELECT id, nome, email, celular, role, estado, cidade, bairro, cep, especialidade_dir, raio_atuacao_km, bio_profissional, disponivel_diretorio + `SELECT id, nome, email, celular, role, estado, cidade, bairro, logradouro, numero, cep, especialidade_dir, raio_atuacao_km, bio_profissional, disponivel_diretorio FROM usuarios WHERE id = $1`, [req.authUser.userId]); if (!rows.length) return res.status(404).json({ error: 'Usuário não encontrado.' }); res.json(rows[0]); @@ -4523,12 +4535,13 @@ app.put('/api/profissionais/perfil', authGuard, async (req, res) => { try { await pool.query( `UPDATE usuarios SET estado = $1, cidade = $2, bairro = $3, cep = $4, especialidade_dir = $5, - raio_atuacao_km = $6, bio_profissional = $7, disponivel_diretorio = $8 - WHERE id = $9`, + raio_atuacao_km = $6, bio_profissional = $7, disponivel_diretorio = $8, logradouro = $9, numero = $10 + WHERE id = $11`, [b.estado?.toUpperCase() || null, b.cidade?.toUpperCase() || null, b.bairro?.toUpperCase() || null, b.cep || null, b.especialidade?.toUpperCase() || b.especialidade_dir?.toUpperCase() || null, Number.isFinite(+b.raio_atuacao_km) && +b.raio_atuacao_km > 0 ? Math.min(+b.raio_atuacao_km, 2000) : null, - b.bio_profissional || null, b.disponivel_diretorio === true, req.authUser.userId]); + b.bio_profissional || null, b.disponivel_diretorio === true, + b.logradouro?.toUpperCase() || null, b.numero || null, req.authUser.userId]); await setGeoByCep('usuarios', req.authUser.userId, b.cep); res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } diff --git a/frontend/views/Dashboard.tsx b/frontend/views/Dashboard.tsx index 1a94678..1cc1f4c 100644 --- a/frontend/views/Dashboard.tsx +++ b/frontend/views/Dashboard.tsx @@ -262,7 +262,7 @@ export const Dashboard: React.FC = () => {
ScoreOdonto
+ScoreOdonto
ScoreOdonto
-Plataforma Odontológica
+ScoreOdonto
+Plataforma Odontológica
+
Do prontuário ao marketplace: encontre profissionais e salas por proximidade, gerencie a agenda sem choques e o financeiro num só lugar.
{label}
-{desc}
+{desc}
Defina sua nova senha de acesso
Informe o e-mail cadastrado
Bem-vindo!
Redirecionando para o sistema...
-