diff --git a/frontend/App.tsx b/frontend/App.tsx index 1d4069f..48cb5a0 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -382,6 +382,24 @@ const App: React.FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // Roteamento pós-login/cadastro. + const onAuthSuccess = (result?: { noWorkspace?: boolean; userRole?: string }) => { + if (result?.noWorkspace) { + const userRole = result.userRole || 'dentista'; + if (userRole === 'superadmin') { setCurrentView('superadmin'); navigate('superadmin'); } + else if (userRole === 'admin' || userRole === 'donoclinica' || userRole === 'donoconsultorio') { setCurrentView('criar-clinica'); navigate('criar-clinica'); } + else { setCurrentView('aguardando-convite'); navigate('aguardando-convite'); } + return; + } + const storedRole = getStoredUserRole(); + if (storedRole === 'superadmin') { setCurrentView('superadmin-overview'); navigate('superadmin-overview'); return; } + if (storedRole === 'donoconsultorio') { setCurrentView('dashboard'); navigate('dashboard'); return; } + const role = HybridBackend.getCurrentRole(); + const view = getDefaultViewForRole(role); + setCurrentView(view); + navigate(view); + }; + const renderView = () => { switch (currentView) { case 'landing': return { case 'diretorio-profissionais': return isPluginActive('profissionais-marketplace') ? : ; case 'login': default: - return { - if (result?.noWorkspace) { - const userRole = result.userRole || 'dentista'; - if (userRole === 'superadmin') { - setCurrentView('superadmin'); - navigate('superadmin'); - } else if (userRole === 'admin' || userRole === 'donoclinica' || userRole === 'donoconsultorio') { - setCurrentView('criar-clinica'); - navigate('criar-clinica'); - } else { - setCurrentView('aguardando-convite'); - navigate('aguardando-convite'); - } - return; - } - const storedRole = getStoredUserRole(); - if (storedRole === 'superadmin') { - setCurrentView('superadmin-overview'); - navigate('superadmin-overview'); - return; - } - if (storedRole === 'donoconsultorio') { - setCurrentView('dashboard'); - navigate('dashboard'); - return; - } - const role = HybridBackend.getCurrentRole(); - const view = getDefaultViewForRole(role); - setCurrentView(view); - navigate(view); - }} />; + return ; } }; diff --git a/frontend/views/LoginView.tsx b/frontend/views/LoginView.tsx index 94db3b5..ff0b6ac 100644 --- a/frontend/views/LoginView.tsx +++ b/frontend/views/LoginView.tsx @@ -1,8 +1,8 @@ import React, { useState, useEffect } from 'react'; import { - Mail, Lock, User, ArrowRight, ShieldCheck, Loader2, Eye, EyeOff, - CheckCircle2, Stethoscope, Calendar, Users, BarChart3, Zap, - Building2, ChevronLeft, Briefcase, KeyRound, RotateCcw + Mail, Lock, ArrowRight, ShieldCheck, Loader2, Eye, EyeOff, CheckCircle2, + Stethoscope, Calendar, BarChart3, ChevronLeft, KeyRound, RotateCcw, + UserSearch, DoorOpen, ScanLine, Sparkles, MapPin, Building2, Users, FlaskConical } from 'lucide-react'; import { HybridBackend } from '../services/backend.ts'; import { APP_VERSION } from '../constants.ts'; @@ -14,63 +14,24 @@ interface LoginViewProps { const API_URL = (window as any).__API_URL__ || ''; -type UserRole = 'donoconsultorio' | 'donoclinica' | 'dentista' | 'protetico' | 'paciente'; - -const UF_LIST = ['AC','AL','AM','AP','BA','CE','DF','ES','GO','MA','MG','MS','MT','PA','PB','PE','PI','PR','RJ','RN','RO','RR','RS','SC','SE','SP','TO']; - -const ROLE_OPTIONS: { role: UserRole; icon: React.ElementType; title: string; desc: string; badge: string; price: string; priceColor: string }[] = [ - { - role: 'donoconsultorio', - icon: Briefcase, - title: 'Consultório', - desc: 'Prática solo ou 1 cadeira — ideal para dentistas autônomos', - badge: 'Mais acessível', - price: 'R$49,90/mês', - priceColor: 'text-emerald-600', - }, - { - role: 'donoclinica', - icon: Building2, - title: 'Clínica', - desc: 'Multi-cadeira com equipe, gestão completa e financeiro', - badge: 'Completo', - price: 'R$499,00/mês', - priceColor: 'text-blue-600', - }, - { - role: 'dentista', - icon: Stethoscope, - title: 'Dentista', - desc: 'Acesse a clínica ou consultório pelo qual foi convidado', - badge: '', - price: 'R$49,90/mês', - priceColor: 'text-indigo-600', - }, - { - role: 'protetico', - icon: Zap, - title: 'Protético', - desc: 'Laboratório de prótese — acesse pelo convite recebido', - badge: '', - price: 'R$49,90/mês', - priceColor: 'text-violet-600', - }, - { - role: 'paciente', - icon: Users, - title: 'Paciente', - desc: 'Acompanhe seus tratamentos e histórico clínico', - badge: '', - price: 'Gratuito', - priceColor: 'text-green-600', - }, +// Funcionalidades atuais da plataforma (destaque no painel da esquerda). +const FEATURES = [ + { icon: UserSearch, label: 'Marketplace de Profissionais', desc: 'Encontre dentistas, biomédicos e protéticos por proximidade' }, + { icon: DoorOpen, label: 'Locação de Salas', desc: 'Alugue ou disponibilize salas com reserva anti-conflito' }, + { icon: Calendar, label: 'Agenda Anti-overbooking', desc: 'Horários do profissional sincronizados entre unidades' }, + { icon: BarChart3, label: 'Financeiro & GTO', desc: 'Faturamento, guias e contratos num lugar só' }, + { icon: ScanLine, label: 'RX / Radiografias', desc: 'Imagens integradas ao prontuário do paciente' }, + { icon: MapPin, label: 'Busca por Proximidade', desc: 'Profissionais e salas mais perto de você primeiro' }, ]; -const FEATURES = [ - { icon: Calendar, label: 'Agenda Inteligente', desc: 'Gestão de horários com lembretes automáticos' }, - { icon: Users, label: 'CRM de Pacientes', desc: 'Histórico completo e acompanhamento de tratamentos' }, - { icon: BarChart3, label: 'Financeiro Avançado', desc: 'Faturamento, GTO e contratos integrados' }, - { icon: Zap, label: 'Ortodontia Avançada', desc: 'Controle de tratamentos e radiografias' }, +// Perfis suportados (chips informativos — o cadastro real é guiado no chat). +const ROLE_CHIPS = [ + { icon: Building2, label: 'Clínica' }, + { icon: Stethoscope, label: 'Consultório' }, + { icon: Stethoscope, label: 'Dentista' }, + { icon: Sparkles, label: 'Biomédico(a)' }, + { icon: FlaskConical, label: 'Protético' }, + { icon: Users, label: 'Paciente' }, ]; // ── Helpers ────────────────────────────────────────────────────────────────── @@ -80,55 +41,43 @@ const Field: React.FC<{ label: string; children: React.ReactNode }> = ({ label,
{children}
); - const InputIcon: React.FC<{ icon: React.ReactNode }> = ({ icon }) => ( {icon} ); - const EyeToggle: React.FC<{ show: boolean; onToggle: () => void }> = ({ show, onToggle }) => ( ); - const ErrorBanner: React.FC<{ children: React.ReactNode }> = ({ children }) => ( -
- {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'; // ── Component ───────────────────────────────────────────────────────────────── export const LoginView: React.FC = ({ onLoginSuccess }) => { - const [tab, setTab] = useState<'login' | 'cadastro'>('login'); - const [authMode, setAuthMode] = useState<'login' | 'forgot' | 'reset'>('login'); - // cadastro steps: 'role' | 'form' - const [cadStep, setCadStep] = useState<'role' | 'form'>('role'); - const [selectedRole, setSelectedRole] = useState(null); - const [isClinica, setIsClinica] = useState(false); + const [tab, setTab] = useState<'login' | 'cadastro'>('login'); + const [authMode, setAuthMode] = useState<'login' | 'forgot' | 'reset'>('login'); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [nome, setNome] = useState(''); - const [nomeEstab, setNomeEstab] = useState(''); - const [confirmPass, setConfirmPass] = useState(''); - const [showPass, setShowPass] = useState(false); - const [showCfm, setShowCfm] = useState(false); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); - const [success, setSuccess] = useState(''); - const [initializing, setInit] = useState(true); - const [leaving, setLeaving] = useState(false); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [showPass, setShowPass] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + const [initializing, setInit] = useState(true); + const [leaving, setLeaving] = useState(false); - // forgot/reset state - const [forgotEmail, setForgotEmail] = useState(''); - const [resetToken, setResetToken] = useState(''); - const [newPassword, setNewPassword] = useState(''); - const [confirmNew, setConfirmNew] = useState(''); - const [showNewPass, setShowNewPass] = useState(false); + // forgot/reset + const [forgotEmail, setForgotEmail] = useState(''); + const [resetToken, setResetToken] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmNew, setConfirmNew] = useState(''); + const [showNewPass, setShowNewPass] = useState(false); useEffect(() => { - const t = setTimeout(() => setInit(false), 2000); + const t = setTimeout(() => setInit(false), 1200); const params = new URLSearchParams(window.location.search); const token = params.get('reset_token'); if (token) { setResetToken(token); setAuthMode('reset'); } @@ -137,10 +86,8 @@ export const LoginView: React.FC = ({ onLoginSuccess }) => { const switchTab = (next: 'login' | 'cadastro') => { if (next === tab || leaving) return; - setLeaving(true); - setError(''); setSuccess(''); - setCadStep('role'); setSelectedRole(null); setIsClinica(false); - setTimeout(() => { setTab(next); setLeaving(false); }, 220); + setLeaving(true); setError(''); setSuccess(''); + setTimeout(() => { setTab(next); setLeaving(false); }, 200); }; const handleLogin = async (e: React.FormEvent) => { @@ -148,42 +95,10 @@ export const LoginView: React.FC = ({ onLoginSuccess }) => { setLoading(true); setError(''); try { const result = await HybridBackend.login(email, password); - if (result.ok) { - onLoginSuccess(result.noWorkspace ? { noWorkspace: true, userRole: result.userRole } : undefined); - } else { - setError('Credenciais inválidas. Verifique e-mail e senha.'); - } - } catch { - setError('Erro de conexão. Tente novamente.'); - } finally { setLoading(false); } - }; - - const handleCadastro = async (e: React.FormEvent) => { - e.preventDefault(); - if (!selectedRole) { setError('Selecione um perfil.'); return; } - if (password !== confirmPass) { setError('As senhas não coincidem.'); return; } - if (password.length < 6) { setError('A senha deve ter no mínimo 6 caracteres.'); return; } - setLoading(true); setError(''); - try { - const effectiveRole = selectedRole === 'donoconsultorio' && isClinica ? 'donoclinica' : selectedRole; - const res = await fetch(`${API_URL}/api/register`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ nome, email, senha: password, role: effectiveRole, workspaceNome: nomeEstab.trim() || undefined }), - }); - const data = await res.json(); - if (!res.ok || !data.success) { setError(data.message || 'Erro ao criar conta.'); return; } - localStorage.setItem('SCOREODONTO_AUTH_TOKEN', data.token); - localStorage.setItem('SCOREODONTO_USER_DATA', JSON.stringify(data.user)); - localStorage.setItem('SCOREODONTO_WORKSPACES', JSON.stringify(data.workspaces || [])); - if (data.workspaces?.length) { - localStorage.setItem('SCOREODONTO_ACTIVE_WORKSPACE', JSON.stringify(data.workspaces[0])); - } - setSuccess('Conta criada! Redirecionando...'); - setTimeout(() => onLoginSuccess(data.noWorkspace ? { noWorkspace: true, userRole: data.userRole } : undefined), 1400); - } catch { - setError('Erro de conexão. Tente novamente.'); - } finally { setLoading(false); } + if (result.ok) onLoginSuccess(result.noWorkspace ? { noWorkspace: true, userRole: result.userRole } : undefined); + else setError('Credenciais inválidas. Verifique e-mail e senha.'); + } catch { setError('Erro de conexão. Tente novamente.'); } + finally { setLoading(false); } }; const handleForgotPassword = async (e: React.FormEvent) => { @@ -216,184 +131,163 @@ export const LoginView: React.FC = ({ onLoginSuccess }) => { setSuccess('Senha redefinida! Redirecionando para o login...'); window.history.replaceState({}, '', window.location.pathname); setTimeout(() => { setAuthMode('login'); setSuccess(''); setNewPassword(''); setConfirmNew(''); }, 2200); - } else { - setError(data.error || 'Link inválido ou expirado. Solicite um novo.'); - } + } else setError(data.error || 'Link inválido ou expirado. Solicite um novo.'); } catch { setError('Erro de conexão. Tente novamente.'); } finally { setLoading(false); } }; - /* ─── Loading splash ─── */ + /* ─── Splash ─── */ if (initializing) { return ( -
-
+
+
-
-

- ScoreOdonto -

-

Inicializando sistema...

+

ScoreOdonto

+
+ {[0, 1, 2].map(i =>
)}
-
- {[0, 1, 2].map(i => ( -
- ))} -
- {APP_VERSION} - +
); } - /* ─── Main layout ─── */ + /* ─── Layout ─── */ return ( -
+
- {/* ── LEFT BRAND PANEL ── */} -
-
-
+ {/* ── LEFT — showcase ── */} +
+
+
{/* Logo */}
-
+
-

- ScoreOdonto -

-

CRM Odontológico

+

ScoreOdonto

+

Plataforma Odontológica

- {/* Headline */} -
+ {/* Headline + features */} +
-

- O Futuro da Sua
Clínica é{' '} - Digital + + Novo: marketplace + locação de salas + +

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

-

- Gestão financeira, agenda avançada e controle de tratamentos — tudo em uma plataforma elegante. +

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

{FEATURES.map(({ icon: Icon, label, desc }) => ( -
- -

{label}

-

{desc}

+
+ +

{label}

+

{desc}

))}
+ +
+ {ROLE_CHIPS.map(({ icon: Icon, label }) => ( + + {label} + + ))} +
{/* Footer */} -
+
Dados criptografados · LGPD · {APP_VERSION}
- {/* ── RIGHT FORM PANEL ── */} -
+ {/* ── RIGHT — auth ── */} +
{/* Mobile logo */}
- - ScoreOdonto - + ScoreOdonto
- {/* Reset password mode */} {authMode === 'reset' ? ( -
+
-
- -
+

Nova Senha

Defina sua nova senha de acesso

} /> - setNewPassword(e.target.value)} - className="w-full pl-10 pr-10 py-3.5 bg-gray-50 border border-gray-200 rounded-xl text-sm font-medium outline-none focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 transition-all" - placeholder="••••••" autoComplete="new-password" /> + setNewPassword(e.target.value)} className={InputCls} placeholder="••••••" autoComplete="new-password" /> setShowNewPass(p => !p)} /> } /> - setConfirmNew(e.target.value)} - className="w-full pl-10 pr-4 py-3.5 bg-gray-50 border border-gray-200 rounded-xl text-sm font-medium outline-none focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 transition-all" - placeholder="••••••" autoComplete="new-password" /> + setConfirmNew(e.target.value)} className={InputCls} placeholder="••••••" autoComplete="new-password" /> {error && {error}} {success &&
{success}
} -
) : authMode === 'forgot' ? ( -
+
-
- -
+

Recuperar Senha

Informe o e-mail cadastrado

} /> - setForgotEmail(e.target.value)} - className="w-full pl-10 pr-4 py-3.5 bg-gray-50 border border-gray-200 rounded-xl text-sm font-medium outline-none focus:border-blue-500 focus:bg-white focus:ring-2 focus:ring-blue-100 transition-all" - placeholder="seu@email.com" autoComplete="email" /> + setForgotEmail(e.target.value)} className={InputCls} placeholder="seu@email.com" autoComplete="email" /> {error && {error}} {success &&
{success}
} -
) : ( - /* ── Login / Cadastro ── */ -
+
- - {/* Tabs */}
{(['login', 'cadastro'] as const).map(t => ( ))}
-
-
+
{tab === 'cadastro' ? ( switchTab('login')} /> ) : success ? (
-
- -
-

Conta criada!

+
+

Bem-vindo!

Redirecionando para o sistema...

- +
- ) : tab === 'login' ? ( - /* ── LOGIN ── */ + ) : (
} /> - setEmail(e.target.value)} - className="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" - placeholder="seu@email.com" autoComplete="email" autoFocus /> + setEmail(e.target.value)} className={InputCls} placeholder="seu@email.com" autoComplete="email" autoFocus /> - } /> - setPassword(e.target.value)} - className="w-full pl-10 pr-10 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" - placeholder="••••••" autoComplete="current-password" /> + setPassword(e.target.value)} className={InputCls.replace('pr-4', 'pr-10')} placeholder="••••••" autoComplete="current-password" /> setShowPass(p => !p)} /> - {error && {error}} - -
- ) : cadStep === 'role' ? ( - /* ── STEP 0: Role selection ── */ -
- {/* Todos os perfis, cada um com seu preço */} - {ROLE_OPTIONS.map(({ role, icon: Icon, title, desc, badge, price, priceColor }) => ( - - ))} - -

- Já tem conta?{' '} - -

-
- ) : ( - /* ── STEP 1: Form ── */ -
- - - - } /> - setNome(e.target.value)} - className="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" - placeholder="Dr. João Silva" autoComplete="name" /> - - - {(selectedRole === 'dentista' || selectedRole === 'protetico') && ( - - } /> - setNomeEstab(e.target.value.toUpperCase())} - className="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 uppercase placeholder:normal-case" - placeholder={selectedRole === 'protetico' ? 'Ex: Laboratório Sorriso' : 'Ex: Consultório Sorriso'} /> -

- É o nome da sua unidade (não o seu). Em branco, usaremos{' '} - {selectedRole === 'protetico' ? `Laboratório de ${nome || '...'}` : `Consultório de ${nome || '...'}`}. -

-
- )} - - - } /> - setEmail(e.target.value)} - className="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" - placeholder="seu@email.com" autoComplete="email" /> - - -
- - } /> - setPassword(e.target.value)} - className="w-full pl-10 pr-10 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" - placeholder="••••••" autoComplete="new-password" /> - setShowPass(p => !p)} /> - - - } /> - setConfirmPass(e.target.value)} - className="w-full pl-10 pr-10 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" - placeholder="••••••" autoComplete="new-password" /> - setShowCfm(p => !p)} /> - -
- - {/* Password strength */} - {password.length > 0 && ( -
- {[3, 6, 9, 12].map((threshold, i) => ( -
= threshold ? (password.length >= 10 ? '#22C55E' : password.length >= 6 ? '#F59E0B' : '#EF4444') : '#E5E7EB' }} /> - ))} - - {password.length < 6 ? 'Fraca' : password.length < 10 ? 'Boa' : 'Forte'} - -
- )} - - {error && {error}} - - - -

- Ao criar conta você concorda com os{' '} - Termos de Uso{' '} - e{' '} - Política de Privacidade. -

- )}
-
- - Ambiente Seguro · {APP_VERSION} - + Ambiente Seguro · {APP_VERSION}
)}