fix(login): render standalone views edge-to-edge e esconde scrollbar do carrossel de onboarding

- App.tsx: wrappers de padding (p-4/md:p-8) e max-w-7xl/mx-auto agora só se aplicam quando não é tela standalone; login/landing/public renderizam full-bleed
- OnboardingChat.tsx: classe ob-noscroll esconde a barra de rolagem do slide intro (mantém scroll), substituindo a custom-scrollbar indefinida no fluxo de login

Inclui também demais alterações pendentes do working tree (snapshot do estado atual de dev).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Builder
2026-06-13 13:23:19 +02:00
parent ae16a9b333
commit ba062e92d0
65 changed files with 20893 additions and 1974 deletions
+237
View File
@@ -0,0 +1,237 @@
import React, { useState } from 'react';
import { Mail, LogOut, Stethoscope, Clock, CheckCircle2, ArrowRight, MapPin, Users, Wrench, ChevronDown, ChevronUp, Loader2 } from 'lucide-react';
import { HybridBackend } from '../services/backend.ts';
import { APP_VERSION } from '../constants.ts';
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 API_URL = (window as any).__API_URL__ || '';
const ROLE_CONFIG: Record<string, { label: string; icon: React.ElementType; color: string; desc: string }> = {
dentista: { label: 'Dentista', icon: Stethoscope, color: 'blue', desc: 'dentista disponível para contratação' },
protetico: { label: 'Protético', icon: Wrench, color: 'purple', desc: 'protético disponível para laboratório' },
};
export const WaitingInviteView: React.FC = () => {
const userData = localStorage.getItem('SCOREODONTO_USER_DATA');
const user = userData ? JSON.parse(userData) : null;
const role = user?.role ?? 'dentista';
const cfg = ROLE_CONFIG[role] ?? ROLE_CONFIG['dentista'];
const email = user?.email ?? '';
const RoleIcon = cfg.icon;
const [showDiretorio, setShowDiretorio] = useState(false);
const [estado, setEstado] = useState('');
const [cidade, setCidade] = useState('');
const [bairro, setBairro] = useState('');
const [especialidade, setEspecialidade] = useState('');
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const [saveError, setSaveError] = useState('');
const steps = [
{ icon: CheckCircle2, label: 'Conta criada', done: true },
{ icon: Mail, label: 'Aguardando convite', done: false },
{ icon: ArrowRight, label: 'Acesso à clínica', done: false },
];
const handleSaveDiretorio = async () => {
if (!estado || !cidade) { setSaveError('Estado e cidade são obrigatórios.'); return; }
setSaving(true); setSaveError('');
try {
const token = localStorage.getItem('SCOREODONTO_AUTH_TOKEN');
const res = await fetch(`${API_URL}/api/usuarios/localidade`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ estado, cidade, bairro, especialidade, disponivel_diretorio: true }),
});
const data = await res.json();
if (!res.ok || !data.success) { setSaveError(data.message || 'Erro ao salvar.'); }
else setSaved(true);
} catch { setSaveError('Erro de conexão.'); }
finally { setSaving(false); }
};
const colorMap: Record<string, string> = {
blue: 'from-blue-600 to-indigo-600',
purple: 'from-purple-600 to-violet-600',
};
return (
<div className="min-h-screen bg-[#F8FAFC] flex flex-col items-center justify-center px-4 py-12">
{/* Logo */}
<div className="flex items-center gap-2 mb-10">
<div className={`w-9 h-9 bg-gradient-to-tr ${colorMap[cfg.color]} rounded-xl flex items-center justify-center shadow-md`}>
<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>
</div>
{/* Card */}
<div className="w-full max-w-md bg-white rounded-3xl border border-gray-100 shadow-[0_20px_60px_rgba(0,0,0,0.07)] overflow-hidden">
<div className={`h-1 bg-gradient-to-r ${colorMap[cfg.color]}`} />
<div className="px-8 py-10 text-center">
{/* Icon */}
<div className={`w-20 h-20 bg-${cfg.color}-50 border border-${cfg.color}-100 rounded-3xl flex items-center justify-center mx-auto mb-6`}>
<RoleIcon size={36} className={`text-${cfg.color}-600`} />
</div>
<h1 className="text-2xl font-black text-gray-900 tracking-tighter mb-2">
Conta criada com sucesso!
</h1>
<p className="text-gray-500 text-sm font-medium leading-relaxed mb-8">
Seu perfil de <strong className="text-gray-700">{cfg.label}</strong> foi registrado.
{role === 'protetico'
? ' Para acessar o sistema, você precisa ser convidado por um laboratório ou clínica.'
: ' Para acessar o sistema, você precisa ser convidado pelo administrador de uma clínica ou consultório.'}
</p>
{/* Steps */}
<div className="flex items-center justify-center gap-0 mb-8">
{steps.map((step, i) => {
const Icon = step.icon;
return (
<React.Fragment key={i}>
<div className="flex flex-col items-center gap-1.5">
<div className={`w-10 h-10 rounded-2xl flex items-center justify-center ${
step.done
? 'bg-green-100 text-green-600'
: i === 1
? `bg-${cfg.color}-100 text-${cfg.color}-600 ring-2 ring-${cfg.color}-200 ring-offset-1`
: 'bg-gray-100 text-gray-400'
}`}>
<Icon size={18} />
</div>
<span className={`text-[9px] font-black uppercase tracking-widest ${
step.done ? 'text-green-600' : i === 1 ? `text-${cfg.color}-600` : 'text-gray-400'
}`}>{step.label}</span>
</div>
{i < steps.length - 1 && (
<div className={`w-12 h-px mb-5 ${step.done ? 'bg-green-200' : 'bg-gray-200'}`} />
)}
</React.Fragment>
);
})}
</div>
{/* Info box */}
<div className="bg-blue-50 border border-blue-100 rounded-2xl p-4 text-left mb-4">
<p className="text-[10px] font-black text-blue-600 uppercase tracking-widest mb-1">
O que fazer agora?
</p>
<ul className="space-y-1.5 text-sm text-blue-800 font-medium">
<li className="flex items-start gap-2">
<span className="text-blue-400 mt-0.5">1.</span>
Informe seu e-mail <strong>{email}</strong> ao administrador
</li>
<li className="flex items-start gap-2">
<span className="text-blue-400 mt-0.5">2.</span>
Aguarde o convite ser enviado
</li>
<li className="flex items-start gap-2">
<span className="text-blue-400 mt-0.5">3.</span>
Acesse o link do convite para completar o vínculo
</li>
</ul>
</div>
{/* Diretório de profissionais */}
<div className="border border-purple-100 bg-purple-50 rounded-2xl overflow-hidden mb-6">
<button
type="button"
onClick={() => setShowDiretorio(v => !v)}
className="w-full flex items-center justify-between gap-3 p-4 text-left"
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-purple-100 rounded-xl flex items-center justify-center">
<Users size={16} className="text-purple-600" />
</div>
<div>
<p className="text-xs font-black text-purple-800 uppercase tracking-wide">Apareça no Diretório</p>
<p className="text-[10px] text-purple-600 font-medium">Clínicas encontram você por localização</p>
</div>
</div>
{showDiretorio ? <ChevronUp size={16} className="text-purple-400" /> : <ChevronDown size={16} className="text-purple-400" />}
</button>
{showDiretorio && (
<div className="px-4 pb-4 space-y-3 border-t border-purple-100 pt-3">
{saved ? (
<div className="flex items-center gap-2 text-green-700 bg-green-50 rounded-xl p-3">
<CheckCircle2 size={18} />
<span className="text-xs font-black uppercase">Perfil publicado no diretório!</span>
</div>
) : (
<>
<p className="text-[11px] text-purple-700 font-medium">
Preencha sua localização para que clínicas e consultórios possam te contratar como {cfg.desc}.
</p>
<div className="grid grid-cols-2 gap-2">
<select
value={estado}
onChange={e => setEstado(e.target.value)}
className="w-full px-3 py-2 bg-white border border-purple-200 rounded-lg text-xs font-bold text-gray-700 outline-none focus:border-purple-400"
>
<option value="">Estado *</option>
{UF_LIST.map(uf => <option key={uf}>{uf}</option>)}
</select>
<input
type="text"
value={cidade}
onChange={e => setCidade(e.target.value.toUpperCase())}
placeholder="Cidade *"
className="w-full px-3 py-2 bg-white border border-purple-200 rounded-lg text-xs font-bold text-gray-700 outline-none focus:border-purple-400 uppercase"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<input
type="text"
value={bairro}
onChange={e => setBairro(e.target.value.toUpperCase())}
placeholder="Bairro"
className="w-full px-3 py-2 bg-white border border-purple-200 rounded-lg text-xs font-bold text-gray-700 outline-none focus:border-purple-400 uppercase"
/>
<input
type="text"
value={especialidade}
onChange={e => setEspecialidade(e.target.value.toUpperCase())}
placeholder={role === 'protetico' ? 'Especialidade (ex: METAL)' : 'Especialidade (ex: ORTO)'}
className="w-full px-3 py-2 bg-white border border-purple-200 rounded-lg text-xs font-bold text-gray-700 outline-none focus:border-purple-400 uppercase"
/>
</div>
{saveError && <p className="text-xs text-red-600 font-bold">{saveError}</p>}
<button
type="button"
onClick={handleSaveDiretorio}
disabled={saving}
className="w-full bg-purple-600 hover:bg-purple-700 text-white py-2.5 rounded-xl text-xs font-black uppercase tracking-widest flex items-center justify-center gap-2 disabled:opacity-50 transition-colors"
>
{saving ? <Loader2 size={14} className="animate-spin" /> : <MapPin size={14} />}
Publicar no Diretório
</button>
</>
)}
</div>
)}
</div>
<button
onClick={() => HybridBackend.logout()}
className="flex items-center gap-2 mx-auto text-xs font-black text-gray-400 hover:text-red-500 uppercase tracking-widest transition-colors"
>
<LogOut size={14} />
Sair da conta
</button>
</div>
<div className="px-8 py-4 bg-gray-50 border-t border-gray-100 flex items-center justify-center">
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">{APP_VERSION}</span>
</div>
</div>
</div>
);
};