3e64514b08
Financeiro V1 (Épicos 1–4): - FKs de origem no lançamento (paciente/profissional/procedimento/agendamento/contrato/realizado), soft delete e cron "Atrasado" - prontuário de procedimentos realizados (fotos antes/depois) + regra sem-comissão (garantia/reabertura do mesmo profissional) - normalizeFinanceiro: corrige CHECK capitalizado vs enforceUppercase (lançamentos não salvavam pela UI) - baixa de pagamento (pago_em), despesas (fornecedor/centro de custo/recorrente), competência por data de conclusão - relatório financeiro (período, paciente, profissional, forma, convênio, glosa) - orçamento Assinado -> contas a receber; contrato vigente -> cobrança recorrente; renovação automática de contrato Financeiro V2 (Comissão/Repasse): - regras de % (padrão/procedimento/override por profissional) + custo de laboratório - base selecionável (recebido/produção), fechamento persistido, pagar (gera despesa) + comprovante + estorno Glosas de convênio: por item de GTO, recurso/protocolo/prazo, recuperar/estornar, impacto no relatório GTO -> Financeiro: finalizar gera RECEITA (convênio a receber); LancarGTO passa a persistir; convênios reais (planos) Contratos: modelo "Atendimento a Convênios / Repasse (Dentista Credenciado)" + variáveis de convênio Salas (redesenho): perfil "Dono de Sala"; locação sempre ativa; menus MINHAS/ALUGAR SALAS; sala como workspace isolado (seletor agrupado Clínicas x Salas, tenantGuard por dono); financeiro de locação (reserva confirmada -> recebível); Painel da Sala (KPIs, agenda visual, recebíveis, relatório por sala) Docs: BACKLOG-FINANCEIRO-V1, AUDITORIA-FUNCIONAL-SCOREODONTO, CHECKLIST-DEPLOY-PRODUCAO Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
622 lines
32 KiB
TypeScript
622 lines
32 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
|
import {
|
|
GraduationCap, ChevronRight, ChevronLeft, CheckCircle2,
|
|
Clock, XCircle, CreditCard, ExternalLink, Loader2,
|
|
Award, Stethoscope, User, DollarSign, AlertCircle, RefreshCw,
|
|
Banknote
|
|
} from 'lucide-react';
|
|
import { HybridBackend } from '../services/backend.ts';
|
|
import { useToast } from '../contexts/ToastContext.tsx';
|
|
import { PageHeader } from '../components/PageHeader.tsx';
|
|
|
|
// ─── Dados do formulário ──────────────────────────────────────────
|
|
|
|
const TITULACOES = ['ESPECIALISTA', 'MESTRE', 'DOUTOR', 'PHD'];
|
|
const ESPECIALIDADES = [
|
|
'ORTODONTIA', 'ORTOPEDIA FACIAL', 'ALINHADORES INVISÍVEIS',
|
|
'ORTO-CIRÚRGICA', 'ORTODONTIA LINGUAL', 'APARELHO AUTOLIGADO',
|
|
'CONTENÇÃO E RECIDIVA', 'ORTOPEDIA DOS MAXILARES',
|
|
];
|
|
|
|
// ─── Status display ───────────────────────────────────────────────
|
|
|
|
function StatusBanner({ candidatura, onPagar }: { candidatura: any; onPagar: () => void }) {
|
|
const statusMap: Record<string, { icon: React.ReactNode; cor: string; titulo: string; subtitulo: string }> = {
|
|
PENDENTE: {
|
|
icon: <Clock size={32} className="text-amber-500" />,
|
|
cor: 'bg-amber-50 border-amber-200',
|
|
titulo: 'CANDIDATURA EM ANÁLISE',
|
|
subtitulo: 'Sua candidatura foi recebida e está sendo analisada pela equipe Scoreodonto. Você receberá uma notificação quando houver atualização.',
|
|
},
|
|
AGUARDANDO_PAGAMENTO: {
|
|
icon: <CreditCard size={32} className="text-teal-500" />,
|
|
cor: 'bg-teal-50 border-teal-200',
|
|
titulo: 'APROVADO — AGUARDANDO PAGAMENTO DA TAXA',
|
|
subtitulo: 'Parabéns! Sua candidatura foi aprovada. Realize o pagamento da taxa de ativação para liberar seu perfil como tutor.',
|
|
},
|
|
ATIVO: {
|
|
icon: <CheckCircle2 size={32} className="text-green-500" />,
|
|
cor: 'bg-green-50 border-green-200',
|
|
titulo: 'PERFIL ATIVO — VOCÊ É UM TUTOR SCOREODONTO!',
|
|
subtitulo: 'Seu perfil está ativo e disponível para todas as clínicas do sistema. Acesse o Painel do Tutor para ver e responder as solicitações.',
|
|
},
|
|
REJEITADO: {
|
|
icon: <XCircle size={32} className="text-red-500" />,
|
|
cor: 'bg-red-50 border-red-200',
|
|
titulo: 'CANDIDATURA NÃO APROVADA',
|
|
subtitulo: candidatura.observacao_admin || 'Sua candidatura não foi aprovada neste momento. Você pode submeter uma nova candidatura após 30 dias.',
|
|
},
|
|
};
|
|
|
|
const info = statusMap[candidatura.status];
|
|
if (!info) return null;
|
|
|
|
return (
|
|
<div className={`border rounded-2xl p-6 ${info.cor}`}>
|
|
<div className="flex items-start gap-4">
|
|
{info.icon}
|
|
<div className="flex-1">
|
|
<h3 className="font-black text-gray-800 uppercase text-sm mb-1">{info.titulo}</h3>
|
|
<p className="text-xs text-gray-600 font-medium leading-relaxed">{info.subtitulo}</p>
|
|
|
|
{candidatura.status === 'AGUARDANDO_PAGAMENTO' && (
|
|
<button
|
|
onClick={onPagar}
|
|
className="mt-4 flex items-center gap-2 px-5 py-2.5 bg-teal-600 hover:bg-teal-700 text-white rounded-xl text-[11px] font-black uppercase transition-colors shadow-sm"
|
|
>
|
|
<CreditCard size={14} /> PAGAR TAXA DE ATIVAÇÃO
|
|
</button>
|
|
)}
|
|
|
|
{candidatura.status === 'REJEITADO' && (
|
|
<p className="mt-2 text-[10px] text-red-600 font-bold uppercase">
|
|
Candidatura em {new Date(candidatura.created_at).toLocaleDateString()}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─── Bloco de resumo do perfil ativo ─────────────────────────────
|
|
|
|
// ─── Seção de credenciais de pagamento (tutor ativo) ─────────────
|
|
|
|
function CredenciaisPagamento({ candidatura }: { candidatura: any }) {
|
|
const toast = useToast();
|
|
const [tutor, setTutor] = useState<any>(null);
|
|
const [pixKey, setPixKey] = useState('');
|
|
const [mpEmail, setMpEmail] = useState('');
|
|
const [aceitouSplit, setAceitouSplit] = useState(false);
|
|
const [comissaoPct, setComissaoPct] = useState<number | null>(null);
|
|
const [salvando, setSalvando] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const userData = JSON.parse(localStorage.getItem('SCOREODONTO_USER_DATA') || '{}');
|
|
HybridBackend.getTutorPorUsuario(userData.id || userData.userId).then(r => {
|
|
if (r.success && r.tutor) {
|
|
setTutor(r.tutor);
|
|
setPixKey(r.tutor.pix_key || '');
|
|
setMpEmail(r.tutor.mp_email || '');
|
|
setAceitouSplit(!!r.tutor.aceite_split_pct);
|
|
}
|
|
});
|
|
HybridBackend.getAdminTutorConfig().then(r => {
|
|
if (r.success) setComissaoPct(parseFloat(r.config?.comissao_pct) || 20);
|
|
});
|
|
}, []);
|
|
|
|
const save = async () => {
|
|
if (!pixKey.trim()) { toast.error('CHAVE PIX OBRIGATÓRIA.'); return; }
|
|
if (!aceitouSplit) { toast.error('VOCÊ DEVE ACEITAR O PERCENTUAL DA PLATAFORMA.'); return; }
|
|
if (!tutor?.id) { toast.error('PERFIL DE TUTOR NÃO ENCONTRADO.'); return; }
|
|
setSalvando(true);
|
|
try {
|
|
const res = await HybridBackend.updateCredenciaisPagamentoTutor(tutor.id, { pix_key: pixKey, mp_email: mpEmail, aceitar_split: aceitouSplit });
|
|
if (!res.success) { toast.error(res.message || 'ERRO.'); return; }
|
|
toast.success('CREDENCIAIS SALVAS E SPLIT ACEITO!');
|
|
} catch { toast.error('ERRO AO SALVAR.'); }
|
|
finally { setSalvando(false); }
|
|
};
|
|
|
|
return (
|
|
<div className="bg-white border border-gray-200 rounded-2xl p-6 space-y-4">
|
|
<h4 className="font-black text-gray-700 uppercase text-sm flex items-center gap-2">
|
|
<DollarSign size={16} className="text-green-500" /> DADOS PARA RECEBIMENTO
|
|
</h4>
|
|
|
|
{/* Split info */}
|
|
{comissaoPct !== null && (
|
|
<div className="bg-emerald-50 border border-emerald-100 rounded-xl p-4 grid grid-cols-2 gap-4 text-center">
|
|
<div>
|
|
<p className="text-[9px] font-black text-gray-400 uppercase">COMISSÃO DA PLATAFORMA</p>
|
|
<p className="text-2xl font-black text-emerald-600">{comissaoPct}%</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-[9px] font-black text-gray-400 uppercase">SEU REPASSE POR CONSULTA</p>
|
|
<p className="text-2xl font-black text-green-600">{100 - comissaoPct}%</p>
|
|
{candidatura.preco_por_consulta && (
|
|
<p className="text-[10px] text-green-500 font-bold">
|
|
≈ R$ {((1 - comissaoPct / 100) * Number(candidatura.preco_por_consulta)).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">CHAVE PIX PARA RECEBIMENTO *</label>
|
|
<input type="text" value={pixKey} onChange={e => setPixKey(e.target.value)}
|
|
className="w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-medium focus:outline-none focus:border-green-400"
|
|
placeholder="CPF, e-mail, celular ou chave aleatória" />
|
|
</div>
|
|
<div>
|
|
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">E-MAIL MERCADOPAGO (OPCIONAL)</label>
|
|
<input type="email" value={mpEmail} onChange={e => setMpEmail(e.target.value)}
|
|
className="w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-medium focus:outline-none focus:border-green-400"
|
|
placeholder="email@mercadopago.com" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Aceite do split */}
|
|
<label className={`flex items-start gap-3 p-4 rounded-xl border-2 cursor-pointer transition-all ${aceitouSplit ? 'border-green-500 bg-green-50' : 'border-gray-200 hover:border-green-300'}`}
|
|
onClick={() => setAceitouSplit(!aceitouSplit)}>
|
|
<div className={`w-5 h-5 rounded border-2 flex items-center justify-center flex-shrink-0 mt-0.5 transition-all ${aceitouSplit ? 'bg-green-500 border-green-500' : 'border-gray-300'}`}>
|
|
{aceitouSplit && <CheckCircle2 size={12} className="text-white" />}
|
|
</div>
|
|
<p className="text-[11px] font-black text-gray-700 uppercase leading-relaxed">
|
|
LI E ACEITO O MODELO DE SPLIT: A PLATAFORMA SCOREODONTO RETERÁ {comissaoPct ?? '—'}% DO VALOR DE CADA CONSULTORIA DE TUTORIA.
|
|
MEU REPASSE SERÁ DE {comissaoPct !== null ? 100 - comissaoPct : '—'}% VIA PIX APÓS CONFIRMAÇÃO DO PAGAMENTO PELA CLÍNICA.
|
|
</p>
|
|
</label>
|
|
|
|
{tutor?.aceite_split_pct && (
|
|
<div className="flex items-center gap-2 text-green-600 text-[10px] font-black uppercase">
|
|
<CheckCircle2 size={14} /> SPLIT ACEITO EM {tutor.aceite_split_at ? new Date(tutor.aceite_split_at).toLocaleDateString('pt-BR') : '—'} — {tutor.aceite_split_pct}% DE COMISSÃO
|
|
</div>
|
|
)}
|
|
|
|
<button onClick={save} disabled={salvando || !pixKey.trim() || !aceitouSplit}
|
|
className="w-full py-2.5 bg-green-600 hover:bg-green-700 text-white rounded-xl text-[11px] font-black uppercase transition-colors disabled:opacity-40 flex items-center justify-center gap-2">
|
|
{salvando ? <><Loader2 size={13} className="animate-spin" /> SALVANDO...</> : <><CheckCircle2 size={13} /> SALVAR CREDENCIAIS E ACEITAR SPLIT</>}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function PerfilAtivoCard({ candidatura }: { candidatura: any }) {
|
|
return (
|
|
<div className="bg-white border border-gray-200 rounded-2xl p-6 space-y-4">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-14 h-14 bg-emerald-100 rounded-full flex items-center justify-center text-emerald-600 font-black text-lg">
|
|
{candidatura.nome?.charAt(0)}
|
|
</div>
|
|
<div>
|
|
<h3 className="font-black text-gray-800 text-lg uppercase">{candidatura.nome}</h3>
|
|
<p className="text-[11px] text-gray-500 font-bold uppercase">{candidatura.titulacao} • {candidatura.especialidade_principal}</p>
|
|
{candidatura.cro && <p className="text-[10px] text-gray-400 font-bold">CRO {candidatura.cro_uf}/{candidatura.cro}</p>}
|
|
</div>
|
|
<div className="ml-auto text-right">
|
|
{candidatura.preco_por_consulta && (
|
|
<div>
|
|
<p className="text-2xl font-black text-emerald-600">R$ {Number(candidatura.preco_por_consulta).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}</p>
|
|
<p className="text-[10px] text-gray-400 font-bold uppercase">POR CONSULTA</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{candidatura.bio && (
|
|
<p className="text-xs text-gray-600 border-t border-gray-100 pt-3 leading-relaxed">{candidatura.bio}</p>
|
|
)}
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{candidatura.anos_experiencia && (
|
|
<div className="bg-gray-50 rounded-xl p-3">
|
|
<p className="text-[9px] font-black text-gray-400 uppercase">EXPERIÊNCIA</p>
|
|
<p className="font-black text-gray-700 text-sm">{candidatura.anos_experiencia} ANOS</p>
|
|
</div>
|
|
)}
|
|
{candidatura.instituicao && (
|
|
<div className="bg-gray-50 rounded-xl p-3">
|
|
<p className="text-[9px] font-black text-gray-400 uppercase">INSTITUIÇÃO</p>
|
|
<p className="font-black text-gray-700 text-sm truncate">{candidatura.instituicao}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─── Formulário em 3 passos ───────────────────────────────────────
|
|
|
|
interface FormData {
|
|
nome: string; email: string; telefone: string; cro: string; cro_uf: string;
|
|
titulacao: string; instituicao: string; ano_conclusao: string;
|
|
especialidade_principal: string; especialidades_adicionais: string[];
|
|
anos_experiencia: string;
|
|
bio: string; preco_por_consulta: string; portfolio_url: string; linkedin_url: string;
|
|
}
|
|
|
|
const EMPTY: FormData = {
|
|
nome: '', email: '', telefone: '', cro: '', cro_uf: '',
|
|
titulacao: '', instituicao: '', ano_conclusao: '',
|
|
especialidade_principal: '', especialidades_adicionais: [],
|
|
anos_experiencia: '',
|
|
bio: '', preco_por_consulta: '', portfolio_url: '', linkedin_url: '',
|
|
};
|
|
|
|
function CandidaturaForm({ onSubmitted, initialData }: { onSubmitted: () => void; initialData?: Partial<FormData> }) {
|
|
const toast = useToast();
|
|
const [step, setStep] = useState(0);
|
|
const [form, setForm] = useState<FormData>({ ...EMPTY, ...initialData });
|
|
const [salvando, setSalvando] = useState(false);
|
|
|
|
const set = (field: keyof FormData, value: any) => setForm(prev => ({ ...prev, [field]: value }));
|
|
|
|
const toggleEsp = (esp: string) => {
|
|
const cur = form.especialidades_adicionais;
|
|
set('especialidades_adicionais', cur.includes(esp) ? cur.filter(e => e !== esp) : [...cur, esp]);
|
|
};
|
|
|
|
const steps = [
|
|
{
|
|
label: 'DADOS PESSOAIS',
|
|
icon: <User size={16} />,
|
|
valid: () => form.nome && form.cro && form.cro_uf && form.email,
|
|
content: (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="md:col-span-2">
|
|
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">NOME COMPLETO *</label>
|
|
<input type="text" value={form.nome} onChange={e => set('nome', e.target.value.toUpperCase())}
|
|
className="w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-bold focus:outline-none focus:border-emerald-400 uppercase" placeholder="DR. JOÃO SILVA" />
|
|
</div>
|
|
<div>
|
|
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">E-MAIL *</label>
|
|
<input type="email" value={form.email} onChange={e => set('email', e.target.value)}
|
|
className="w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-medium focus:outline-none focus:border-emerald-400" placeholder="dr@email.com" />
|
|
</div>
|
|
<div>
|
|
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">TELEFONE</label>
|
|
<input type="text" value={form.telefone} onChange={e => set('telefone', e.target.value)}
|
|
className="w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-medium focus:outline-none focus:border-emerald-400" placeholder="(11) 99999-9999" />
|
|
</div>
|
|
<div>
|
|
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">CRO *</label>
|
|
<input type="text" value={form.cro} onChange={e => set('cro', e.target.value)}
|
|
className="w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-bold focus:outline-none focus:border-emerald-400" placeholder="123456" />
|
|
</div>
|
|
<div>
|
|
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">CRO UF *</label>
|
|
<input type="text" maxLength={2} value={form.cro_uf} onChange={e => set('cro_uf', e.target.value.toUpperCase())}
|
|
className="w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-bold focus:outline-none focus:border-emerald-400 uppercase" placeholder="SP" />
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
label: 'FORMAÇÃO ACADÊMICA',
|
|
icon: <Award size={16} />,
|
|
valid: () => form.titulacao && form.especialidade_principal && form.anos_experiencia,
|
|
content: (
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">TITULAÇÃO *</label>
|
|
<select value={form.titulacao} onChange={e => set('titulacao', e.target.value)}
|
|
className="w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-bold focus:outline-none focus:border-emerald-400">
|
|
<option value="">— SELECIONE —</option>
|
|
{TITULACOES.map(t => <option key={t} value={t}>{t}</option>)}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">ANOS DE EXPERIÊNCIA *</label>
|
|
<input type="number" min={0} max={50} value={form.anos_experiencia} onChange={e => set('anos_experiencia', e.target.value)}
|
|
className="w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-bold focus:outline-none focus:border-emerald-400" placeholder="10" />
|
|
</div>
|
|
<div>
|
|
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">INSTITUIÇÃO DE FORMAÇÃO</label>
|
|
<input type="text" value={form.instituicao} onChange={e => set('instituicao', e.target.value.toUpperCase())}
|
|
className="w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-bold focus:outline-none focus:border-emerald-400 uppercase" placeholder="USP, UNICAMP..." />
|
|
</div>
|
|
<div>
|
|
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">ANO DE CONCLUSÃO</label>
|
|
<input type="number" min={1980} max={2030} value={form.ano_conclusao} onChange={e => set('ano_conclusao', e.target.value)}
|
|
className="w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-bold focus:outline-none focus:border-emerald-400" placeholder="2015" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="text-[10px] font-black text-gray-400 uppercase block mb-2">ESPECIALIDADE PRINCIPAL *</label>
|
|
<select value={form.especialidade_principal} onChange={e => set('especialidade_principal', e.target.value)}
|
|
className="w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-bold focus:outline-none focus:border-emerald-400">
|
|
<option value="">— SELECIONE —</option>
|
|
{ESPECIALIDADES.map(e => <option key={e} value={e}>{e}</option>)}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="text-[10px] font-black text-gray-400 uppercase block mb-2">ESPECIALIDADES ADICIONAIS (OPCIONAL)</label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{ESPECIALIDADES.filter(e => e !== form.especialidade_principal).map(esp => (
|
|
<button key={esp} type="button" onClick={() => toggleEsp(esp)}
|
|
className={`px-3 py-1.5 rounded-full text-[10px] font-black uppercase border transition-all ${
|
|
form.especialidades_adicionais.includes(esp)
|
|
? 'bg-emerald-600 text-white border-emerald-600'
|
|
: 'bg-white text-gray-600 border-gray-200 hover:border-emerald-300'
|
|
}`}>
|
|
{esp}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
label: 'PERFIL PÚBLICO',
|
|
icon: <Stethoscope size={16} />,
|
|
valid: () => form.bio && form.preco_por_consulta,
|
|
content: (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">BIO / APRESENTAÇÃO * <span className="text-gray-400 font-normal normal-case">(visível para as clínicas)</span></label>
|
|
<textarea value={form.bio} onChange={e => set('bio', e.target.value)}
|
|
placeholder="Descreva sua experiência, abordagem clínica e diferenciais como tutor..."
|
|
className="w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-medium focus:outline-none focus:border-emerald-400 h-28 resize-none" />
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">PREÇO POR CONSULTA DE TUTORIA (R$) *</label>
|
|
<input type="number" min={0} step={10} value={form.preco_por_consulta} onChange={e => set('preco_por_consulta', e.target.value)}
|
|
className="w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-bold focus:outline-none focus:border-emerald-400" placeholder="350" />
|
|
<p className="text-[10px] text-gray-400 mt-1">VALOR COBRADO DIRETAMENTE DA CLÍNICA QUE CONTRATAR VOCÊ</p>
|
|
</div>
|
|
<div>
|
|
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">LINKEDIN (OPCIONAL)</label>
|
|
<input type="url" value={form.linkedin_url} onChange={e => set('linkedin_url', e.target.value)}
|
|
className="w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-medium focus:outline-none focus:border-emerald-400" placeholder="https://linkedin.com/in/..." />
|
|
</div>
|
|
<div className="md:col-span-2">
|
|
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">LINK PARA PORTFÓLIO / CURRÍCULO (OPCIONAL)</label>
|
|
<input type="url" value={form.portfolio_url} onChange={e => set('portfolio_url', e.target.value)}
|
|
className="w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-medium focus:outline-none focus:border-emerald-400" placeholder="https://..." />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
];
|
|
|
|
const handleSubmit = async () => {
|
|
setSalvando(true);
|
|
try {
|
|
const userData = JSON.parse(localStorage.getItem('SCOREODONTO_USER_DATA') || '{}');
|
|
const res = await HybridBackend.submitCandidatura({
|
|
usuario_id: userData.id || userData.userId,
|
|
...form,
|
|
anos_experiencia: parseInt(form.anos_experiencia) || null,
|
|
ano_conclusao: parseInt(form.ano_conclusao) || null,
|
|
preco_por_consulta: parseFloat(form.preco_por_consulta) || null,
|
|
});
|
|
if (!res.success) { toast.error(res.message || 'ERRO AO ENVIAR.'); return; }
|
|
toast.success('CANDIDATURA ENVIADA! AGUARDE ANÁLISE DA EQUIPE SCOREODONTO.');
|
|
onSubmitted();
|
|
} catch { toast.error('ERRO AO ENVIAR CANDIDATURA.'); }
|
|
finally { setSalvando(false); }
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Steps indicator */}
|
|
<div className="flex items-center gap-2">
|
|
{steps.map((s, i) => (
|
|
<React.Fragment key={i}>
|
|
<div className={`flex items-center gap-2 px-3 py-1.5 rounded-xl text-[10px] font-black uppercase transition-all ${
|
|
i === step ? 'bg-emerald-600 text-white' : i < step ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-400'
|
|
}`}>
|
|
{i < step ? <CheckCircle2 size={12} /> : s.icon}
|
|
<span className="hidden sm:inline">{s.label}</span>
|
|
<span className="sm:hidden">{i + 1}</span>
|
|
</div>
|
|
{i < steps.length - 1 && <div className="flex-1 h-px bg-gray-200" />}
|
|
</React.Fragment>
|
|
))}
|
|
</div>
|
|
|
|
{/* Conteúdo do passo */}
|
|
<div className="bg-white border border-gray-200 rounded-2xl p-6">
|
|
<h3 className="font-black text-gray-700 uppercase text-sm mb-5 flex items-center gap-2">
|
|
{steps[step].icon} {steps[step].label}
|
|
</h3>
|
|
{steps[step].content}
|
|
</div>
|
|
|
|
{/* Navegação */}
|
|
<div className="flex gap-3">
|
|
{step > 0 && (
|
|
<button onClick={() => setStep(s => s - 1)}
|
|
className="flex items-center gap-2 px-4 py-2.5 border border-gray-200 rounded-xl text-[11px] font-black uppercase hover:bg-gray-50 transition-colors">
|
|
<ChevronLeft size={14} /> VOLTAR
|
|
</button>
|
|
)}
|
|
{step < steps.length - 1 ? (
|
|
<button onClick={() => setStep(s => s + 1)} disabled={!steps[step].valid()}
|
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl text-[11px] font-black uppercase transition-colors disabled:opacity-40">
|
|
PRÓXIMO <ChevronRight size={14} />
|
|
</button>
|
|
) : (
|
|
<button onClick={handleSubmit} disabled={salvando || !steps[step].valid()}
|
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 bg-green-600 hover:bg-green-700 text-white rounded-xl text-[11px] font-black uppercase transition-colors disabled:opacity-40">
|
|
{salvando ? <><Loader2 size={13} className="animate-spin" /> ENVIANDO...</> : <><CheckCircle2 size={13} /> ENVIAR CANDIDATURA</>}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─── Modal de pagamento MP ────────────────────────────────────────
|
|
|
|
function PagamentoModal({ candidaturaId, onClose, onPago }: { candidaturaId: string; onClose: () => void; onPago: () => void }) {
|
|
const toast = useToast();
|
|
const [loading, setLoading] = useState(false);
|
|
const [checkoutData, setCheckoutData] = useState<{ checkout_url: string; taxa: number } | null>(null);
|
|
|
|
const iniciar = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await HybridBackend.iniciarPagamentoCandidatura(candidaturaId);
|
|
if (!res.success) { toast.error(res.message || 'ERRO AO GERAR LINK DE PAGAMENTO.'); return; }
|
|
setCheckoutData(res);
|
|
} catch { toast.error('ERRO AO GERAR LINK.'); }
|
|
finally { setLoading(false); }
|
|
};
|
|
|
|
useEffect(() => { iniciar(); }, []);
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4 backdrop-blur-sm">
|
|
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md">
|
|
<div className="p-6 space-y-5">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-teal-100 rounded-xl flex items-center justify-center">
|
|
<CreditCard size={18} className="text-teal-600" />
|
|
</div>
|
|
<div>
|
|
<h3 className="font-black text-gray-800 uppercase text-sm">PAGAMENTO DA TAXA DE ATIVAÇÃO</h3>
|
|
<p className="text-[10px] text-gray-400 font-bold uppercase">VIA MERCADOPAGO — PIX / CARTÃO</p>
|
|
</div>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-8 gap-3 text-gray-400">
|
|
<Loader2 size={22} className="animate-spin" />
|
|
<span className="text-xs font-bold uppercase">GERANDO LINK DE PAGAMENTO...</span>
|
|
</div>
|
|
) : checkoutData ? (
|
|
<>
|
|
<div className="bg-teal-50 border border-teal-100 rounded-xl p-4 text-center">
|
|
<p className="text-3xl font-black text-teal-600">R$ {Number(checkoutData.taxa).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}</p>
|
|
<p className="text-[10px] text-teal-500 font-bold uppercase mt-1">TAXA DE ATIVAÇÃO — PAGAMENTO ÚNICO</p>
|
|
</div>
|
|
<div className="bg-gray-50 border border-gray-200 rounded-xl p-3">
|
|
<p className="text-[10px] font-black text-gray-400 uppercase mb-1">FORMAS DE PAGAMENTO ACEITAS</p>
|
|
<p className="text-xs text-gray-600 font-medium">PIX · CARTÃO DE CRÉDITO · BOLETO</p>
|
|
</div>
|
|
<a href={checkoutData.checkout_url} target="_blank" rel="noopener noreferrer"
|
|
className="flex items-center justify-center gap-2 w-full py-3 bg-teal-600 hover:bg-teal-700 text-white rounded-xl font-black text-[11px] uppercase transition-colors shadow-sm">
|
|
<ExternalLink size={14} /> PAGAR AGORA VIA MERCADOPAGO
|
|
</a>
|
|
<p className="text-[10px] text-gray-400 text-center">APÓS O PAGAMENTO APROVADO SEU PERFIL SERÁ ATIVADO AUTOMATICAMENTE</p>
|
|
</>
|
|
) : (
|
|
<div className="flex items-center gap-2 text-red-500 text-xs font-bold uppercase">
|
|
<AlertCircle size={16} /> ERRO AO GERAR LINK. TENTE NOVAMENTE.
|
|
</div>
|
|
)}
|
|
|
|
<button onClick={onClose} className="w-full py-2.5 border border-gray-200 rounded-xl text-[11px] font-black uppercase hover:bg-gray-50 transition-colors">
|
|
FECHAR
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─── View principal ───────────────────────────────────────────────
|
|
|
|
export const TutorCandidaturaView: React.FC = () => {
|
|
const [candidatura, setCandidatura] = useState<any>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [showPagamento, setShowPagamento] = useState(false);
|
|
|
|
const userData = JSON.parse(localStorage.getItem('SCOREODONTO_USER_DATA') || '{}');
|
|
const userId = userData.id || userData.userId;
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await HybridBackend.getMinhaCandidatura(userId);
|
|
if (res.success) setCandidatura(res.candidatura);
|
|
} catch {}
|
|
finally { setLoading(false); }
|
|
}, [userId]);
|
|
|
|
useEffect(() => { load(); }, [load]);
|
|
|
|
// Abre modal de pagamento se voltou do MP com ?payment=success/pending
|
|
useEffect(() => {
|
|
const params = new URLSearchParams(window.location.search);
|
|
if (params.get('payment')) { load(); }
|
|
}, []);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<PageHeader title="CANDIDATURA PARA TUTOR ORTODÔNTICO">
|
|
<button onClick={load} className="p-2 hover:bg-gray-100 rounded-lg transition-colors text-gray-400">
|
|
<RefreshCw size={15} />
|
|
</button>
|
|
</PageHeader>
|
|
|
|
{loading ? (
|
|
<div className="flex items-center justify-center h-40"><Loader2 size={28} className="animate-spin text-gray-300" /></div>
|
|
) : !candidatura ? (
|
|
// Sem candidatura — mostra intro + formulário
|
|
<div className="space-y-6">
|
|
<div className="bg-emerald-50 border border-emerald-100 rounded-2xl p-6">
|
|
<div className="flex items-start gap-4">
|
|
<GraduationCap size={32} className="text-emerald-500 flex-shrink-0" />
|
|
<div>
|
|
<h3 className="font-black text-gray-800 uppercase text-sm mb-2">TORNE-SE UM TUTOR ORTODÔNTICO SCOREODONTO</h3>
|
|
<p className="text-xs text-gray-600 font-medium leading-relaxed mb-3">
|
|
Como tutor, você disponibiliza seu conhecimento para clínicas de todo o Brasil através da plataforma.
|
|
Receba solicitações de segunda opinião, planejamento e mentoria clínica — no seu tempo, no seu preço.
|
|
</p>
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
{[
|
|
{ icon: '🌎', label: 'Visibilidade nacional', desc: 'Seu perfil disponível para todas as clínicas do sistema' },
|
|
{ icon: '💰', label: 'Você define o preço', desc: 'Precifique cada consulta de tutoria conforme desejar' },
|
|
{ icon: '⏱️', label: 'Flexível', desc: 'Responda no seu próprio tempo, sem compromisso de horário' },
|
|
].map(item => (
|
|
<div key={item.label} className="bg-white rounded-xl p-3 border border-emerald-100">
|
|
<p className="text-lg mb-1">{item.icon}</p>
|
|
<p className="text-[10px] font-black text-gray-700 uppercase">{item.label}</p>
|
|
<p className="text-[9px] text-gray-400 mt-0.5">{item.desc}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<CandidaturaForm onSubmitted={load} />
|
|
</div>
|
|
) : (
|
|
// Tem candidatura — mostra status
|
|
<div className="space-y-5">
|
|
<StatusBanner candidatura={candidatura} onPagar={() => setShowPagamento(true)} />
|
|
{candidatura.status === 'ATIVO' && (
|
|
<>
|
|
<PerfilAtivoCard candidatura={candidatura} />
|
|
<CredenciaisPagamento candidatura={candidatura} />
|
|
</>
|
|
)}
|
|
{candidatura.status === 'REJEITADO' && (
|
|
<div className="mt-4">
|
|
<h4 className="font-black text-gray-700 uppercase text-sm mb-3">SUBMETER NOVA CANDIDATURA</h4>
|
|
<CandidaturaForm onSubmitted={load} initialData={{ nome: candidatura.nome, email: candidatura.email, cro: candidatura.cro, cro_uf: candidatura.cro_uf }} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{showPagamento && candidatura && (
|
|
<PagamentoModal
|
|
candidaturaId={candidatura.id}
|
|
onClose={() => setShowPagamento(false)}
|
|
onPago={() => { setShowPagamento(false); load(); }}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|