feat: login teal + home com novas funções + endereço completo no perfil + ajustes biomédico

- login: paleta verde/teal (headline mantida)
- home (LandingPage): cards de Marketplace de Profissionais, Locação de Salas,
  Busca por Proximidade e RX/Radiografias
- perfil profissional: campos Rua/Logradouro e Número (+ ViaCEP preenche a rua);
  backend usuarios.logradouro/numero + GET/PUT perfil
- biomédico: remove tratamento 'dentista' (Dashboard 'Profissional'; tratamentos sem 'odontológico')
- backend: blockBiomedico nas rotas de procedimentos/especialidades (POST/PUT/DELETE)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Builder
2026-06-14 05:09:32 +02:00
parent 76f8fa64db
commit a383fc7a29
6 changed files with 92 additions and 45 deletions
+23 -10
View File
@@ -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 }); }
+1 -1
View File
@@ -262,7 +262,7 @@ export const Dashboard: React.FC = () => {
<div className="flex-1 min-w-0">
<div className="text-sm font-black text-white uppercase tracking-tight truncate">{app.pacientenome || 'Paciente'}</div>
<div className="text-[10px] font-bold text-gray-400 uppercase tracking-widest mt-0.5 truncate">
{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' })}
</div>
</div>
<ChevronRight size={16} className="text-gray-600 group-hover:text-white group-hover:translate-x-1 transition-all" />
+25 -1
View File
@@ -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."
/>
<FeatureCard
icon={<UserSearch size={28} />}
title="Marketplace de Profissionais"
description="Encontre e contrate dentistas, biomédicos e protéticos por especialidade e proximidade."
/>
<FeatureCard
icon={<DoorOpen size={28} />}
title="Locação de Salas"
description="Alugue ou disponibilize salas e consultórios com reserva anti-conflito de agenda."
/>
<FeatureCard
icon={<MapPin size={28} />}
title="Busca por Proximidade"
description="Profissionais e salas mais perto de você primeiro, com raio de atuação."
/>
<FeatureCard
icon={<ScanLine size={28} />}
title="RX / Radiografias"
description="Imagens e laudos integrados ao prontuário, sincronizados com o sistema."
/>
</div>
</div>
</section>
+31 -31
View File
@@ -53,7 +53,7 @@ const EyeToggle: React.FC<{ show: boolean; onToggle: () => void }> = ({ show, on
const ErrorBanner: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div className="p-3 bg-red-50 border border-red-100 rounded-xl text-red-600 text-xs font-bold text-center uppercase tracking-wide">{children}</div>
);
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<LoginViewProps> = ({ onLoginSuccess }) => {
@@ -139,13 +139,13 @@ export const LoginView: React.FC<LoginViewProps> = ({ onLoginSuccess }) => {
/* ─── Splash ─── */
if (initializing) {
return (
<div className="h-dvh bg-[#0B1220] flex flex-col items-center justify-center gap-5">
<div className="w-16 h-16 bg-gradient-to-tr from-blue-500 to-indigo-500 rounded-2xl flex items-center justify-center shadow-xl shadow-blue-900/40">
<div className="h-dvh bg-[#04201d] flex flex-col items-center justify-center gap-5">
<div className="w-16 h-16 bg-gradient-to-tr from-teal-500 to-emerald-500 rounded-2xl flex items-center justify-center shadow-xl shadow-teal-900/40">
<Stethoscope className="text-white" size={30} />
</div>
<p className="text-2xl font-black text-white tracking-tighter uppercase italic">Score<span className="text-blue-400">Odonto</span></p>
<p className="text-2xl font-black text-white tracking-tighter uppercase italic">Score<span className="text-teal-400">Odonto</span></p>
<div className="flex gap-1.5">
{[0, 1, 2].map(i => <div key={i} className="w-2 h-2 rounded-full bg-blue-400" style={{ animation: `b 1.2s ease-in-out ${i * 0.2}s infinite` }} />)}
{[0, 1, 2].map(i => <div key={i} className="w-2 h-2 rounded-full bg-teal-400" style={{ animation: `b 1.2s ease-in-out ${i * 0.2}s infinite` }} />)}
</div>
<style>{`@keyframes b{0%,80%,100%{transform:translateY(0);opacity:.5}40%{transform:translateY(-8px);opacity:1}}`}</style>
</div>
@@ -165,9 +165,9 @@ export const LoginView: React.FC<LoginViewProps> = ({ onLoginSuccess }) => {
{/* ── LEFT — showcase ── */}
<div className="hidden lg:flex flex-col justify-between flex-1 h-full relative overflow-hidden p-12 xl:p-16
bg-gradient-to-br from-[#0B1220] via-[#0e1a33] to-[#1e3a8a] text-white">
<div className="absolute -top-24 -right-24 w-96 h-96 bg-blue-500/20 rounded-full blur-3xl pointer-events-none" style={{ animation: 'float 8s ease-in-out infinite' }} />
<div className="absolute -bottom-24 -left-24 w-80 h-80 bg-indigo-500/20 rounded-full blur-3xl pointer-events-none" />
bg-gradient-to-br from-[#04201d] via-[#065f54] to-[#0d9488] text-white">
<div className="absolute -top-24 -right-24 w-96 h-96 bg-teal-500/20 rounded-full blur-3xl pointer-events-none" style={{ animation: 'float 8s ease-in-out infinite' }} />
<div className="absolute -bottom-24 -left-24 w-80 h-80 bg-emerald-500/20 rounded-full blur-3xl pointer-events-none" />
{/* Logo */}
<div className="flex items-center gap-3 relative z-10">
@@ -175,21 +175,21 @@ export const LoginView: React.FC<LoginViewProps> = ({ onLoginSuccess }) => {
<Stethoscope className="text-white" size={22} />
</div>
<div>
<p className="font-black text-xl tracking-tighter uppercase italic leading-none">Score<span className="text-blue-300">Odonto</span></p>
<p className="text-blue-300/70 text-[10px] font-black uppercase tracking-widest">Plataforma Odontológica</p>
<p className="font-black text-xl tracking-tighter uppercase italic leading-none">Score<span className="text-teal-300">Odonto</span></p>
<p className="text-teal-300/70 text-[10px] font-black uppercase tracking-widest">Plataforma Odontológica</p>
</div>
</div>
{/* Headline + features */}
<div className="relative z-10 space-y-7">
<div>
<span className="inline-flex items-center gap-1.5 text-[10px] font-black uppercase tracking-widest bg-blue-500/20 text-blue-200 border border-blue-400/30 px-3 py-1 rounded-full mb-4">
<span className="inline-flex items-center gap-1.5 text-[10px] font-black uppercase tracking-widest bg-teal-500/20 text-teal-200 border border-teal-400/30 px-3 py-1 rounded-full mb-4">
<Sparkles size={11} /> Novo: marketplace + locação de salas
</span>
<h2 className="text-4xl xl:text-5xl font-black tracking-tighter leading-[1.05]">
Gestão, conexão e<br /><span className="text-blue-300 italic">crescimento</span> da sua prática
Gestão, conexão e<br /><span className="text-teal-300 italic">crescimento</span> da sua prática
</h2>
<p className="text-blue-100/70 text-base font-medium leading-relaxed max-w-md mt-4">
<p className="text-teal-100/70 text-base font-medium leading-relaxed max-w-md mt-4">
Do prontuário ao marketplace: encontre profissionais e salas por proximidade, gerencie a agenda sem choques e o financeiro num lugar.
</p>
</div>
@@ -197,24 +197,24 @@ export const LoginView: React.FC<LoginViewProps> = ({ onLoginSuccess }) => {
<div className="grid grid-cols-2 gap-3">
{FEATURES.map(({ icon: Icon, label, desc }) => (
<div key={label} className="bg-white/5 backdrop-blur-sm rounded-2xl p-4 border border-white/10 hover:bg-white/10 transition-colors">
<Icon size={20} className="text-blue-300 mb-2" />
<Icon size={20} className="text-teal-300 mb-2" />
<p className="font-black text-xs uppercase tracking-tight leading-none mb-1">{label}</p>
<p className="text-blue-200/60 text-[10px] font-medium leading-snug">{desc}</p>
<p className="text-teal-200/60 text-[10px] font-medium leading-snug">{desc}</p>
</div>
))}
</div>
<div className="flex flex-wrap gap-2">
{ROLE_CHIPS.map(({ icon: Icon, label }) => (
<span key={label} className="inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wide bg-white/5 border border-white/10 text-blue-100/80 px-2.5 py-1 rounded-lg">
<Icon size={12} className="text-blue-300" /> {label}
<span key={label} className="inline-flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wide bg-white/5 border border-white/10 text-teal-100/80 px-2.5 py-1 rounded-lg">
<Icon size={12} className="text-teal-300" /> {label}
</span>
))}
</div>
</div>
{/* Footer */}
<div className="relative z-10 flex items-center gap-2 text-blue-300/50 text-[10px] font-black uppercase tracking-widest">
<div className="relative z-10 flex items-center gap-2 text-teal-300/50 text-[10px] font-black uppercase tracking-widest">
<ShieldCheck size={12} /> Dados criptografados · LGPD · {APP_VERSION}
</div>
</div>
@@ -223,22 +223,22 @@ export const LoginView: React.FC<LoginViewProps> = ({ onLoginSuccess }) => {
<div className="flex-1 flex flex-col items-center justify-center p-6 lg:p-12 overflow-y-auto">
{/* Mobile logo */}
<div className="lg:hidden flex items-center gap-2 mb-8">
<div className="w-9 h-9 bg-gradient-to-tr from-blue-600 to-indigo-600 rounded-xl flex items-center justify-center shadow-md shadow-blue-200">
<div className="w-9 h-9 bg-gradient-to-tr from-teal-600 to-emerald-600 rounded-xl flex items-center justify-center shadow-md shadow-teal-200">
<Stethoscope size={18} className="text-white" />
</div>
<span className="text-xl font-black text-gray-900 tracking-tighter uppercase italic">Score<span className="text-blue-600">Odonto</span></span>
<span className="text-xl font-black text-gray-900 tracking-tighter uppercase italic">Score<span className="text-teal-600">Odonto</span></span>
</div>
<div className="w-full max-w-md">
{authMode === 'reset' ? (
<div className="bg-white rounded-[2rem] shadow-[0_40px_80px_-20px_rgba(0,0,0,0.12)] border border-gray-100 overflow-hidden">
<div className="h-1.5 bg-gradient-to-r from-blue-500 to-indigo-500" />
<div className="h-1.5 bg-gradient-to-r from-teal-500 to-emerald-500" />
<div className="p-8">
<button onClick={() => setAuthMode('login')} className="flex items-center gap-1.5 text-xs font-black text-gray-400 hover:text-gray-600 uppercase tracking-widest mb-6 transition-colors">
<ChevronLeft size={14} /> Voltar ao login
</button>
<div className="text-center mb-6">
<div className="w-12 h-12 bg-blue-50 rounded-2xl flex items-center justify-center mx-auto mb-3"><KeyRound size={22} className="text-blue-600" /></div>
<div className="w-12 h-12 bg-teal-50 rounded-2xl flex items-center justify-center mx-auto mb-3"><KeyRound size={22} className="text-teal-600" /></div>
<h2 className="text-xl font-black text-gray-900 uppercase tracking-tight">Nova Senha</h2>
<p className="text-xs text-gray-500 font-medium mt-1">Defina sua nova senha de acesso</p>
</div>
@@ -254,7 +254,7 @@ export const LoginView: React.FC<LoginViewProps> = ({ onLoginSuccess }) => {
</Field>
{error && <ErrorBanner>{error}</ErrorBanner>}
{success && <div className="p-3 bg-green-50 border border-green-100 rounded-xl text-green-700 text-xs font-bold text-center uppercase">{success}</div>}
<button type="submit" disabled={loading} className="w-full bg-blue-600 hover:bg-blue-700 disabled:opacity-60 text-white py-4 rounded-xl text-sm font-black uppercase tracking-widest shadow-lg shadow-blue-200 transition-all flex items-center justify-center gap-2">
<button type="submit" disabled={loading} className="w-full bg-teal-600 hover:bg-teal-700 disabled:opacity-60 text-white py-4 rounded-xl text-sm font-black uppercase tracking-widest shadow-lg shadow-teal-200 transition-all flex items-center justify-center gap-2">
{loading ? <><Loader2 size={18} className="sp" /> Salvando...</> : <>Redefinir Senha <ArrowRight size={18} /></>}
</button>
</form>
@@ -262,13 +262,13 @@ export const LoginView: React.FC<LoginViewProps> = ({ onLoginSuccess }) => {
</div>
) : authMode === 'forgot' ? (
<div className="bg-white rounded-[2rem] shadow-[0_40px_80px_-20px_rgba(0,0,0,0.12)] border border-gray-100 overflow-hidden">
<div className="h-1.5 bg-gradient-to-r from-blue-500 to-indigo-500" />
<div className="h-1.5 bg-gradient-to-r from-teal-500 to-emerald-500" />
<div className="p-8">
<button onClick={() => { setAuthMode('login'); setError(''); setSuccess(''); }} className="flex items-center gap-1.5 text-xs font-black text-gray-400 hover:text-gray-600 uppercase tracking-widest mb-6 transition-colors">
<ChevronLeft size={14} /> Voltar ao login
</button>
<div className="text-center mb-6">
<div className="w-12 h-12 bg-blue-50 rounded-2xl flex items-center justify-center mx-auto mb-3"><RotateCcw size={22} className="text-blue-600" /></div>
<div className="w-12 h-12 bg-teal-50 rounded-2xl flex items-center justify-center mx-auto mb-3"><RotateCcw size={22} className="text-teal-600" /></div>
<h2 className="text-xl font-black text-gray-900 uppercase tracking-tight">Recuperar Senha</h2>
<p className="text-xs text-gray-500 font-medium mt-1">Informe o e-mail cadastrado</p>
</div>
@@ -279,7 +279,7 @@ export const LoginView: React.FC<LoginViewProps> = ({ onLoginSuccess }) => {
</Field>
{error && <ErrorBanner>{error}</ErrorBanner>}
{success && <div className="p-3 bg-green-50 border border-green-100 rounded-xl text-green-700 text-xs font-bold text-center uppercase">{success}</div>}
<button type="submit" disabled={loading} className="w-full bg-blue-600 hover:bg-blue-700 disabled:opacity-60 text-white py-4 rounded-xl text-sm font-black uppercase tracking-widest shadow-lg shadow-blue-200 transition-all flex items-center justify-center gap-2">
<button type="submit" disabled={loading} className="w-full bg-teal-600 hover:bg-teal-700 disabled:opacity-60 text-white py-4 rounded-xl text-sm font-black uppercase tracking-widest shadow-lg shadow-teal-200 transition-all flex items-center justify-center gap-2">
{loading ? <><Loader2 size={18} className="sp" /> Enviando...</> : <>Enviar Instruções <ArrowRight size={18} /></>}
</button>
</form>
@@ -287,11 +287,11 @@ export const LoginView: React.FC<LoginViewProps> = ({ onLoginSuccess }) => {
</div>
) : (
<div className="bg-white rounded-[2rem] shadow-[0_40px_80px_-20px_rgba(0,0,0,0.12)] border border-gray-100 overflow-hidden">
<div className="h-1.5 bg-gradient-to-r from-blue-500 to-indigo-500" />
<div className="h-1.5 bg-gradient-to-r from-teal-500 to-emerald-500" />
<div className="flex border-b border-gray-100">
{(['login', 'cadastro'] as const).map(t => (
<button key={t} type="button" onClick={() => switchTab(t)}
className={`flex-1 py-4 text-xs font-black uppercase tracking-widest transition-all ${tab === t ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-400 hover:text-gray-600'}`}>
className={`flex-1 py-4 text-xs font-black uppercase tracking-widest transition-all ${tab === t ? 'text-teal-600 border-b-2 border-teal-600' : 'text-gray-400 hover:text-gray-600'}`}>
{t === 'login' ? 'Entrar' : 'Criar Conta'}
</button>
))}
@@ -305,7 +305,7 @@ export const LoginView: React.FC<LoginViewProps> = ({ onLoginSuccess }) => {
<div className="w-16 h-16 bg-green-50 rounded-[1.5rem] flex items-center justify-center"><CheckCircle2 size={32} className="text-green-500" /></div>
<p className="text-gray-900 font-black text-lg uppercase tracking-tight">Bem-vindo!</p>
<p className="text-gray-500 text-sm">Redirecionando para o sistema...</p>
<Loader2 size={20} className="text-blue-600 sp" />
<Loader2 size={20} className="text-teal-600 sp" />
</div>
) : (
<form onSubmit={handleLogin} className="space-y-5">
@@ -320,12 +320,12 @@ export const LoginView: React.FC<LoginViewProps> = ({ onLoginSuccess }) => {
</Field>
{error && <ErrorBanner>{error}</ErrorBanner>}
<button type="submit" disabled={loading}
className="w-full bg-blue-600 hover:bg-blue-700 disabled:opacity-60 disabled:cursor-not-allowed text-white py-4 rounded-2xl text-sm font-black uppercase tracking-widest shadow-lg shadow-blue-200 hover:shadow-xl hover:shadow-blue-300 hover:-translate-y-0.5 active:translate-y-0 transition-all flex items-center justify-center gap-2">
className="w-full bg-teal-600 hover:bg-teal-700 disabled:opacity-60 disabled:cursor-not-allowed text-white py-4 rounded-2xl text-sm font-black uppercase tracking-widest shadow-lg shadow-teal-200 hover:shadow-xl hover:shadow-teal-300 hover:-translate-y-0.5 active:translate-y-0 transition-all flex items-center justify-center gap-2">
{loading ? <><Loader2 size={18} className="sp" /> Verificando...</> : <>Entrar <ArrowRight size={18} /></>}
</button>
<div className="text-center">
<button type="button" onClick={() => { setAuthMode('forgot'); setError(''); setSuccess(''); }}
className="text-xs text-gray-400 hover:text-blue-600 font-bold uppercase tracking-widest transition-colors">
className="text-xs text-gray-400 hover:text-teal-600 font-bold uppercase tracking-widest transition-colors">
Esqueci minha senha
</button>
</div>
+1 -1
View File
@@ -148,7 +148,7 @@ export const MeusTratamentos: React.FC = () => {
<div className="flex flex-col h-full space-y-4">
<PageHeader
title="Meus Tratamentos"
description="LISTAGEM E GESTÃO DE GUIAS DE TRATAMENTO ODONTOLÓGICO (GTO)."
description="LISTAGEM E GESTÃO DE GUIAS DE TRATAMENTO (GTO)."
>
<button
onClick={exportToCSV}
+11 -1
View File
@@ -33,6 +33,8 @@ interface Profissional {
estado: string;
cidade: string;
bairro: string;
logradouro?: string;
numero?: string;
cep?: string;
especialidade: string;
raio_atuacao_km?: number | null;
@@ -445,11 +447,19 @@ export const ProfissionaisPlugin: React.FC = () => {
setP('cep', v);
if (v.replace(/\D/g, '').length === 8) {
const r = await buscarCep(v);
if (r) { setPerfil((prev: any) => ({ ...prev, bairro: r.bairro, cidade: r.cidade, estado: r.estado })); toast.success('LOCALIZAÇÃO PREENCHIDA PELO CEP!'); }
if (r) { setPerfil((prev: any) => ({ ...prev, logradouro: r.logradouro, bairro: r.bairro, cidade: r.cidade, estado: r.estado })); toast.success('ENDEREÇO PREENCHIDO PELO CEP!'); }
else toast.error('CEP NÃO ENCONTRADO.');
}
}} placeholder="79000-000" />
</div>
<div className="md:col-span-2">
<label className={labelCls}>Rua / Logradouro</label>
<input className={`${inputCls} uppercase`} value={perfil.logradouro || ''} onChange={e => setP('logradouro', e.target.value.toUpperCase())} placeholder="EX: AVENIDA AFONSO PENA" />
</div>
<div>
<label className={labelCls}>Número</label>
<input className={inputCls} value={perfil.numero || ''} onChange={e => setP('numero', e.target.value)} placeholder="EX: 1234" />
</div>
<div>
<label className={labelCls}>Especialidade principal</label>
<input className={`${inputCls} uppercase`} value={perfil.especialidade_dir || ''} onChange={e => setP('especialidade_dir', e.target.value.toUpperCase())} placeholder="EX: IMPLANTODONTIA" />