a991f1fe2c
- buildInfo.ts: hook useAppVersion() consome GET /api/version (fonte única = backend),
com fallback ao carimbo do bundle; deixa de depender do APP_VERSION manual.
- Versão exibida em Configurações ("Sobre o sistema") + telas públicas (Login/WaitingInvite);
removida do rodapé do Sidebar.
- compose.prod/deploy-prod.sh: injetam APP_VERSION (=tag) e APP_ENV=PROD em runtime,
desacoplando a versão semântica do build (commit/build seguem da imagem).
- constants.ts marcado legado + bump V1.0.15 (transitório até o Passo 5; evita colisão de tag).
- DEPLOY-PRODUCAO.md reescrito com status ✅/🧪/🎯 + nota de fase transitória.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
239 lines
14 KiB
TypeScript
239 lines
14 KiB
TypeScript
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 { useAppVersion } from '../buildInfo.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 ver = useAppVersion();
|
|
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-teal-600 to-emerald-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-teal-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-teal-50 border border-teal-100 rounded-2xl p-4 text-left mb-4">
|
|
<p className="text-[10px] font-black text-teal-600 uppercase tracking-widest mb-1">
|
|
O que fazer agora?
|
|
</p>
|
|
<ul className="space-y-1.5 text-sm text-teal-800 font-medium">
|
|
<li className="flex items-start gap-2">
|
|
<span className="text-teal-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-teal-400 mt-0.5">2.</span>
|
|
Aguarde o convite ser enviado
|
|
</li>
|
|
<li className="flex items-start gap-2">
|
|
<span className="text-teal-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">{ver.version}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|