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 = () => {
{app.pacientenome || 'Paciente'}
- {app.dentistaNome || 'Dr. Dentista'} • {new Date(app.start_time).toLocaleDateString('pt-BR')} {new Date(app.start_time).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })} + {app.dentistaNome || 'Profissional'} • {new Date(app.start_time).toLocaleDateString('pt-BR')} {new Date(app.start_time).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}
diff --git a/frontend/views/LandingPage.tsx b/frontend/views/LandingPage.tsx index b1edad3..667aa65 100644 --- a/frontend/views/LandingPage.tsx +++ b/frontend/views/LandingPage.tsx @@ -12,7 +12,11 @@ import { X, Zap, MessageCircle, - User + User, + UserSearch, + DoorOpen, + MapPin, + ScanLine } from 'lucide-react'; import { ToothIcon } from '../components/ToothIcon.tsx'; @@ -251,6 +255,26 @@ export const LandingPage: React.FC<{ title="BI Financeiro" description="Gráficos de produtividade, fluxo de caixa e gestão de taxas de operadoras." /> + } + title="Marketplace de Profissionais" + description="Encontre e contrate dentistas, biomédicos e protéticos por especialidade e proximidade." + /> + } + title="Locação de Salas" + description="Alugue ou disponibilize salas e consultórios com reserva anti-conflito de agenda." + /> + } + title="Busca por Proximidade" + description="Profissionais e salas mais perto de você primeiro, com raio de atuação." + /> + } + title="RX / Radiografias" + description="Imagens e laudos integrados ao prontuário, sincronizados com o sistema." + /> diff --git a/frontend/views/LoginView.tsx b/frontend/views/LoginView.tsx index ff0b6ac..3d4e869 100644 --- a/frontend/views/LoginView.tsx +++ b/frontend/views/LoginView.tsx @@ -53,7 +53,7 @@ const EyeToggle: React.FC<{ show: boolean; onToggle: () => void }> = ({ show, on const ErrorBanner: React.FC<{ children: React.ReactNode }> = ({ children }) => (
{children}
); -const InputCls = 'w-full pl-10 pr-4 py-3.5 bg-gray-50 border border-gray-200 rounded-xl text-sm font-medium text-gray-900 placeholder:text-gray-400 outline-none focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 transition-all'; +const InputCls = 'w-full pl-10 pr-4 py-3.5 bg-gray-50 border border-gray-200 rounded-xl text-sm font-medium text-gray-900 placeholder:text-gray-400 outline-none focus:border-teal-500 focus:bg-white focus:ring-2 focus:ring-teal-100 transition-all'; // ── Component ───────────────────────────────────────────────────────────────── export const LoginView: React.FC = ({ onLoginSuccess }) => { @@ -139,13 +139,13 @@ export const LoginView: React.FC = ({ onLoginSuccess }) => { /* ─── Splash ─── */ if (initializing) { return ( -
-
+
+
-

ScoreOdonto

+

ScoreOdonto

- {[0, 1, 2].map(i =>
)} + {[0, 1, 2].map(i =>
)}
@@ -165,9 +165,9 @@ export const LoginView: React.FC = ({ onLoginSuccess }) => { {/* ── LEFT — showcase ── */}
-
-
+ bg-gradient-to-br from-[#04201d] via-[#065f54] to-[#0d9488] text-white"> +
+
{/* Logo */}
@@ -175,21 +175,21 @@ export const LoginView: React.FC = ({ onLoginSuccess }) => {
-

ScoreOdonto

-

Plataforma Odontológica

+

ScoreOdonto

+

Plataforma Odontológica

{/* Headline + features */}
- + Novo: marketplace + locação de salas

- Gestão, conexão e
crescimento da sua prática + Gestão, conexão e
crescimento da sua prática

-

+

Do prontuário ao marketplace: encontre profissionais e salas por proximidade, gerencie a agenda sem choques e o financeiro num só lugar.

@@ -197,24 +197,24 @@ export const LoginView: React.FC = ({ onLoginSuccess }) => {
{FEATURES.map(({ icon: Icon, label, desc }) => (
- +

{label}

-

{desc}

+

{desc}

))}
{ROLE_CHIPS.map(({ icon: Icon, label }) => ( - - {label} + + {label} ))}
{/* Footer */} -
+
Dados criptografados · LGPD · {APP_VERSION}
@@ -223,22 +223,22 @@ export const LoginView: React.FC = ({ onLoginSuccess }) => {
{/* Mobile logo */}
-
+
- ScoreOdonto + ScoreOdonto
{authMode === 'reset' ? (
-
+
-
+

Nova Senha

Defina sua nova senha de acesso

@@ -254,7 +254,7 @@ export const LoginView: React.FC = ({ onLoginSuccess }) => { {error && {error}} {success &&
{success}
} - @@ -262,13 +262,13 @@ export const LoginView: React.FC = ({ onLoginSuccess }) => {
) : authMode === 'forgot' ? (
-
+
-
+

Recuperar Senha

Informe o e-mail cadastrado

@@ -279,7 +279,7 @@ export const LoginView: React.FC = ({ onLoginSuccess }) => { {error && {error}} {success &&
{success}
} - @@ -287,11 +287,11 @@ export const LoginView: React.FC = ({ onLoginSuccess }) => {
) : (
-
+
{(['login', 'cadastro'] as const).map(t => ( ))} @@ -305,7 +305,7 @@ export const LoginView: React.FC = ({ onLoginSuccess }) => {

Bem-vindo!

Redirecionando para o sistema...

- +
) : (
@@ -320,12 +320,12 @@ export const LoginView: React.FC = ({ onLoginSuccess }) => { {error && {error}}
diff --git a/frontend/views/MeusTratamentos.tsx b/frontend/views/MeusTratamentos.tsx index 343585f..020fc61 100644 --- a/frontend/views/MeusTratamentos.tsx +++ b/frontend/views/MeusTratamentos.tsx @@ -148,7 +148,7 @@ export const MeusTratamentos: React.FC = () => {
+
+ + setP('logradouro', e.target.value.toUpperCase())} placeholder="EX: AVENIDA AFONSO PENA" /> +
+
+ + setP('numero', e.target.value)} placeholder="EX: 1234" /> +
setP('especialidade_dir', e.target.value.toUpperCase())} placeholder="EX: IMPLANTODONTIA" />