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:
@@ -0,0 +1,507 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
GraduationCap, CheckCircle2, XCircle, Clock, Settings,
|
||||
DollarSign, Users, RefreshCw, Loader2, Eye, CreditCard,
|
||||
AlertCircle, ChevronDown, ChevronUp, Banknote, Key, Percent, FileText
|
||||
} from 'lucide-react';
|
||||
import { HybridBackend } from '../services/backend.ts';
|
||||
import { useToast } from '../contexts/ToastContext.tsx';
|
||||
import { PageHeader } from '../components/PageHeader.tsx';
|
||||
|
||||
// ─── Status ───────────────────────────────────────────────────────
|
||||
|
||||
const STATUS_STYLE: Record<string, { cls: string; label: string; icon: React.ReactNode }> = {
|
||||
PENDENTE: { cls: 'bg-amber-100 text-amber-700', label: 'PENDENTE', icon: <Clock size={12} /> },
|
||||
AGUARDANDO_PAGAMENTO:{ cls: 'bg-blue-100 text-blue-700', label: 'AGUARD. PAGAMENTO', icon: <CreditCard size={12} /> },
|
||||
ATIVO: { cls: 'bg-green-100 text-green-700', label: 'ATIVO', icon: <CheckCircle2 size={12} /> },
|
||||
REJEITADO: { cls: 'bg-red-100 text-red-700', label: 'REJEITADO', icon: <XCircle size={12} /> },
|
||||
};
|
||||
|
||||
const TITULACAO_BADGE: Record<string, string> = {
|
||||
ESPECIALISTA: 'bg-blue-100 text-blue-700',
|
||||
MESTRE: 'bg-purple-100 text-purple-700',
|
||||
DOUTOR: 'bg-indigo-100 text-indigo-700',
|
||||
PHD: 'bg-violet-100 text-violet-700',
|
||||
};
|
||||
|
||||
// ─── Card de candidatura ──────────────────────────────────────────
|
||||
|
||||
const CandidaturaCard: React.FC<{ c: any; onAction: () => void }> = ({ c, onAction }) => {
|
||||
const toast = useToast();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [obs, setObs] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const st = STATUS_STYLE[c.status] || STATUS_STYLE.PENDENTE;
|
||||
|
||||
const action = async (status: string) => {
|
||||
if (status === 'REJEITADO' && !obs.trim()) {
|
||||
toast.error('INFORME O MOTIVO DA REJEIÇÃO.');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await HybridBackend.updateStatusCandidatura(c.id, status, obs || undefined);
|
||||
if (!res.success) { toast.error(res.message || 'ERRO.'); return; }
|
||||
toast.success(`CANDIDATURA ${status === 'REJEITADO' ? 'REJEITADA' : 'APROVADA'}!`);
|
||||
onAction();
|
||||
} catch { toast.error('ERRO.'); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
|
||||
const confirmarPagamento = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await HybridBackend.confirmarPagamentoCandidatura(c.id);
|
||||
if (!res.success) { toast.error(res.message || 'ERRO.'); return; }
|
||||
toast.success('PAGAMENTO CONFIRMADO — TUTOR ATIVADO!');
|
||||
onAction();
|
||||
} catch { toast.error('ERRO.'); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-xl overflow-hidden">
|
||||
<button onClick={() => setOpen(!open)}
|
||||
className="w-full flex items-center gap-4 p-4 hover:bg-gray-50 transition-colors text-left">
|
||||
<div className="w-10 h-10 bg-indigo-100 rounded-full flex items-center justify-center font-black text-indigo-600 flex-shrink-0">
|
||||
{c.nome?.charAt(0)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-black text-sm text-gray-800 truncate uppercase">{c.nome}</p>
|
||||
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
|
||||
<p className="text-[10px] text-gray-500 font-bold uppercase">{c.especialidade_principal} • CRO {c.cro_uf}/{c.cro}</p>
|
||||
{c.titulacao && (
|
||||
<span className={`text-[9px] font-black px-1.5 py-0.5 rounded-full uppercase ${TITULACAO_BADGE[c.titulacao] || 'bg-gray-100 text-gray-600'}`}>{c.titulacao}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<span className={`flex items-center gap-1 text-[9px] font-black px-2 py-0.5 rounded-full uppercase ${st.cls}`}>
|
||||
{st.icon} {st.label}
|
||||
</span>
|
||||
<p className="text-[10px] text-gray-400 font-bold hidden sm:block">{new Date(c.created_at).toLocaleDateString()}</p>
|
||||
{open ? <ChevronUp size={16} className="text-gray-400" /> : <ChevronDown size={16} className="text-gray-400" />}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="px-4 pb-4 border-t border-gray-100 pt-4 space-y-4">
|
||||
{/* Dados */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 text-xs">
|
||||
<div><p className="text-[9px] font-black text-gray-400 uppercase">E-MAIL</p><p className="font-bold text-gray-700">{c.email || '—'}</p></div>
|
||||
<div><p className="text-[9px] font-black text-gray-400 uppercase">TELEFONE</p><p className="font-bold text-gray-700">{c.telefone || '—'}</p></div>
|
||||
<div><p className="text-[9px] font-black text-gray-400 uppercase">EXPERIÊNCIA</p><p className="font-bold text-gray-700">{c.anos_experiencia ? `${c.anos_experiencia} ANOS` : '—'}</p></div>
|
||||
<div><p className="text-[9px] font-black text-gray-400 uppercase">INSTITUIÇÃO</p><p className="font-bold text-gray-700">{c.instituicao || '—'}</p></div>
|
||||
<div><p className="text-[9px] font-black text-gray-400 uppercase">ANO CONCLUSÃO</p><p className="font-bold text-gray-700">{c.ano_conclusao || '—'}</p></div>
|
||||
<div>
|
||||
<p className="text-[9px] font-black text-gray-400 uppercase">PREÇO / CONSULTA</p>
|
||||
<p className="font-black text-indigo-600">{c.preco_por_consulta ? `R$ ${Number(c.preco_por_consulta).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}` : '—'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{c.bio && (
|
||||
<div className="bg-gray-50 border border-gray-100 rounded-lg p-3">
|
||||
<p className="text-[9px] font-black text-gray-400 uppercase mb-1">BIO</p>
|
||||
<p className="text-xs text-gray-600">{c.bio}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{c.portfolio_url && (
|
||||
<a href={c.portfolio_url} target="_blank" rel="noopener noreferrer" className="text-[10px] font-bold text-blue-600 underline">PORTFÓLIO →</a>
|
||||
)}
|
||||
|
||||
{/* Ações */}
|
||||
{c.status === 'PENDENTE' && (
|
||||
<div className="space-y-3 pt-2 border-t border-gray-100">
|
||||
<textarea value={obs} onChange={e => setObs(e.target.value)}
|
||||
placeholder="Observação (obrigatória se rejeitar)..."
|
||||
className="w-full border border-gray-200 rounded-xl px-3 py-2 text-xs font-medium resize-none h-16 focus:outline-none focus:border-indigo-400" />
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => action('AGUARDANDO_PAGAMENTO')} disabled={loading}
|
||||
className="flex-1 py-2 bg-green-600 hover:bg-green-700 text-white rounded-xl text-[10px] font-black uppercase transition-colors disabled:opacity-40 flex items-center justify-center gap-1">
|
||||
{loading ? <Loader2 size={12} className="animate-spin" /> : <CheckCircle2 size={12} />} APROVAR
|
||||
</button>
|
||||
<button onClick={() => action('REJEITADO')} disabled={loading}
|
||||
className="flex-1 py-2 bg-red-600 hover:bg-red-700 text-white rounded-xl text-[10px] font-black uppercase transition-colors disabled:opacity-40 flex items-center justify-center gap-1">
|
||||
{loading ? <Loader2 size={12} className="animate-spin" /> : <XCircle size={12} />} REJEITAR
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{c.status === 'AGUARDANDO_PAGAMENTO' && (
|
||||
<div className="pt-2 border-t border-gray-100">
|
||||
<p className="text-[10px] text-gray-400 font-bold uppercase mb-2">CONFIRMAR PAGAMENTO RECEBIDO MANUALMENTE</p>
|
||||
<button onClick={confirmarPagamento} disabled={loading}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-xl text-[10px] font-black uppercase transition-colors disabled:opacity-40">
|
||||
{loading ? <Loader2 size={12} className="animate-spin" /> : <Banknote size={12} />} CONFIRMAR PAGAMENTO
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{c.observacao_admin && (
|
||||
<div className="bg-amber-50 border border-amber-100 rounded-lg p-3">
|
||||
<p className="text-[9px] font-black text-amber-600 uppercase mb-1">OBSERVAÇÃO DO ADMIN</p>
|
||||
<p className="text-xs text-amber-700">{c.observacao_admin}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Aba configurações ────────────────────────────────────────────
|
||||
|
||||
const ConfigTab: React.FC = () => {
|
||||
const toast = useToast();
|
||||
const [cfg, setCfg] = useState({ taxa_inscricao: '', chave_pix: '', instrucoes_pagamento: '', mp_access_token: '', mp_public_key: '', comissao_pct: '20', texto_responsabilidade: '' });
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [salvando, setSalvando] = useState(false);
|
||||
const [showToken, setShowToken] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
HybridBackend.getAdminTutorConfig().then(r => {
|
||||
if (r.success && r.config) {
|
||||
setCfg({ taxa_inscricao: r.config.taxa_inscricao || '', chave_pix: r.config.chave_pix || '', instrucoes_pagamento: r.config.instrucoes_pagamento || '', mp_access_token: r.config.mp_access_token || '', mp_public_key: r.config.mp_public_key || '', comissao_pct: String(r.config.comissao_pct ?? '20'), texto_responsabilidade: r.config.texto_responsabilidade || '' });
|
||||
}
|
||||
}).finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const save = async () => {
|
||||
setSalvando(true);
|
||||
try {
|
||||
const res = await HybridBackend.updateAdminTutorConfig({
|
||||
...cfg,
|
||||
taxa_inscricao: parseFloat(cfg.taxa_inscricao) || 0,
|
||||
comissao_pct: parseFloat(cfg.comissao_pct) || 20,
|
||||
});
|
||||
if (!res.success) { toast.error('ERRO AO SALVAR.'); return; }
|
||||
toast.success('CONFIGURAÇÕES SALVAS!');
|
||||
} catch { toast.error('ERRO AO SALVAR.'); }
|
||||
finally { setSalvando(false); }
|
||||
};
|
||||
|
||||
if (loading) return <div className="flex items-center justify-center h-32"><Loader2 size={22} className="animate-spin text-gray-300" /></div>;
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* Comissão da plataforma */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5 space-y-4">
|
||||
<h4 className="font-black text-gray-700 uppercase text-xs flex items-center gap-2"><Percent size={14} className="text-indigo-500" /> SPLIT DE PAGAMENTO</h4>
|
||||
<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">COMISSÃO DA PLATAFORMA (%)</label>
|
||||
<input type="number" min={0} max={100} step={1} value={cfg.comissao_pct} onChange={e => setCfg(p => ({ ...p, comissao_pct: 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-indigo-400" placeholder="20" />
|
||||
<p className="text-[10px] text-gray-400 mt-1">EX: 20% → TUTOR RECEBE 80% DO VALOR DE CADA CONSULTORIA</p>
|
||||
</div>
|
||||
<div className="bg-indigo-50 border border-indigo-100 rounded-xl p-3 flex items-start gap-2">
|
||||
<AlertCircle size={14} className="text-indigo-500 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-[10px] text-indigo-700 font-bold uppercase leading-relaxed">
|
||||
O TUTOR DEVE ACEITAR ESTE % EXPLICITAMENTE AO CADASTRAR SUAS CREDENCIAIS DE PAGAMENTO.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Texto do termo de responsabilidade */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5 space-y-4">
|
||||
<h4 className="font-black text-gray-700 uppercase text-xs flex items-center gap-2"><FileText size={14} className="text-amber-500" /> TEXTO DO TERMO DE RESPONSABILIDADE</h4>
|
||||
<div>
|
||||
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">TEXTO EXIBIDO AO DENTISTA AO SOLICITAR TUTORIA</label>
|
||||
<textarea value={cfg.texto_responsabilidade} onChange={e => setCfg(p => ({ ...p, texto_responsabilidade: e.target.value }))}
|
||||
className="w-full border border-gray-200 rounded-xl px-3 py-2.5 text-xs font-medium h-40 resize-none focus:outline-none focus:border-amber-400"
|
||||
placeholder="Deixe em branco para usar o texto padrão do sistema..." />
|
||||
<p className="text-[10px] text-gray-400 mt-1">SE VAZIO, O SISTEMA USA O TEXTO PADRÃO JURIDICAMENTE REDIGIDO.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Taxa */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5 space-y-4">
|
||||
<h4 className="font-black text-gray-700 uppercase text-xs flex items-center gap-2"><DollarSign size={14} className="text-green-500" /> TAXA DE ATIVAÇÃO</h4>
|
||||
<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">VALOR DA TAXA (R$) *</label>
|
||||
<input type="number" min={0} step={10} value={cfg.taxa_inscricao} onChange={e => setCfg(p => ({ ...p, taxa_inscricao: 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-indigo-400" placeholder="299,90" />
|
||||
<p className="text-[10px] text-gray-400 mt-1">COBRADO UMA VEZ PARA ATIVAÇÃO DO PERFIL DE TUTOR</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">CHAVE PIX (FALLBACK MANUAL)</label>
|
||||
<input type="text" value={cfg.chave_pix} onChange={e => setCfg(p => ({ ...p, chave_pix: 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-indigo-400" placeholder="email@scoreodonto.com" />
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">INSTRUÇÕES DE PAGAMENTO (EXIBIDAS AO CANDIDATO)</label>
|
||||
<textarea value={cfg.instrucoes_pagamento} onChange={e => setCfg(p => ({ ...p, instrucoes_pagamento: e.target.value }))}
|
||||
className="w-full border border-gray-200 rounded-xl px-3 py-2.5 text-xs font-medium h-20 resize-none focus:outline-none focus:border-indigo-400"
|
||||
placeholder="Ex: Após o pagamento, seu perfil será ativado em até 24h. Em caso de dúvidas, contate suporte@scoreodonto.com" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MercadoPago */}
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-5 space-y-4">
|
||||
<h4 className="font-black text-gray-700 uppercase text-xs flex items-center gap-2"><Key size={14} className="text-blue-500" /> MERCADOPAGO — CREDENCIAIS</h4>
|
||||
<div className="bg-blue-50 border border-blue-100 rounded-lg p-3 text-[10px] text-blue-700 font-bold uppercase">
|
||||
<AlertCircle size={12} className="inline mr-1" />
|
||||
CONFIGURE COM AS CREDENCIAIS DA SUA CONTA MERCADOPAGO. O WEBHOOK SERÁ: {'/api/candidatura-tutor/webhook'}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">ACCESS TOKEN (PRODUÇÃO)</label>
|
||||
<div className="relative">
|
||||
<input type={showToken ? 'text' : 'password'} value={cfg.mp_access_token} onChange={e => setCfg(p => ({ ...p, mp_access_token: e.target.value }))}
|
||||
className="w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-mono focus:outline-none focus:border-indigo-400 pr-20" placeholder="APP_USR-..." />
|
||||
<button onClick={() => setShowToken(!showToken)} className="absolute right-3 top-1/2 -translate-y-1/2 text-[10px] font-black text-gray-400 hover:text-gray-600 uppercase">
|
||||
{showToken ? 'OCULTAR' : 'VER'}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-400 mt-1">OBTENHA EM MERCADOPAGO.COM → SUAS APLICAÇÕES → CREDENCIAIS</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[10px] font-black text-gray-400 uppercase block mb-1">PUBLIC KEY</label>
|
||||
<input type="text" value={cfg.mp_public_key} onChange={e => setCfg(p => ({ ...p, mp_public_key: e.target.value }))}
|
||||
className="w-full border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-mono focus:outline-none focus:border-indigo-400" placeholder="APP_USR-..." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button onClick={save} disabled={salvando}
|
||||
className="flex items-center gap-2 px-5 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl text-[11px] font-black uppercase transition-colors disabled:opacity-40 shadow-sm">
|
||||
{salvando ? <Loader2 size={13} className="animate-spin" /> : <CheckCircle2 size={13} />} SALVAR CONFIGURAÇÕES
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── View principal ───────────────────────────────────────────────
|
||||
|
||||
// ─── Aba Financeiro / Repasses ────────────────────────────────────
|
||||
|
||||
const FinanceiroTab: React.FC = () => {
|
||||
const toast = useToast();
|
||||
const [repasses, setRepasses] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await HybridBackend.getRepassesPendentes();
|
||||
if (res.success) setRepasses(res.repasses || []);
|
||||
} catch {}
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
const confirmar = async (id: string, tutor: string, valor: number) => {
|
||||
if (!window.confirm(`CONFIRMAR REPASSE DE R$ ${valor.toLocaleString('pt-BR', { minimumFractionDigits: 2 })} PARA ${tutor}?`)) return;
|
||||
const res = await HybridBackend.confirmarRepasse(id);
|
||||
if (!res.success) { toast.error('ERRO.'); return; }
|
||||
toast.success('REPASSE CONFIRMADO!');
|
||||
load();
|
||||
};
|
||||
|
||||
const pendentes = repasses.filter(r => r.status_repasse !== 'PAGO');
|
||||
const pagos = repasses.filter(r => r.status_repasse === 'PAGO');
|
||||
const totalPendente = pendentes.reduce((acc, r) => acc + (parseFloat(r.valor_tutor) || 0), 0);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Resumo */}
|
||||
<div className="grid grid-cols-3 gap-2 sm:gap-3">
|
||||
{[
|
||||
{ l: 'PENDENTES', v: pendentes.length, color: 'text-amber-600' },
|
||||
{ l: 'A REPASSAR', v: `R$ ${totalPendente.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}`, color: 'text-indigo-600' },
|
||||
{ l: 'REPASSADOS', v: pagos.length, color: 'text-green-600' },
|
||||
].map(s => (
|
||||
<div key={s.l} className="bg-white border border-gray-200 rounded-xl p-3 sm:p-4 text-center">
|
||||
<p className={`text-base sm:text-xl font-black ${s.color} truncate`}>{s.v}</p>
|
||||
<p className="text-[8px] sm:text-[9px] font-black text-gray-400 uppercase mt-1 leading-tight">{s.l}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-32"><Loader2 size={24} className="animate-spin text-gray-300" /></div>
|
||||
) : repasses.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-gray-400 gap-2">
|
||||
<Banknote size={28} />
|
||||
<p className="text-sm font-black uppercase">NENHUM REPASSE REGISTRADO</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{repasses.map(r => (
|
||||
<div key={r.id} className="bg-white border border-gray-200 rounded-xl p-4 space-y-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-black text-sm text-gray-800 uppercase truncate">{r.paciente_nome || '—'}</p>
|
||||
<p className="text-[10px] text-gray-500 font-bold uppercase">{r.tutor_nome} • {r.tipo}</p>
|
||||
<p className="text-[10px] text-gray-400 mt-0.5">{new Date(r.created_at).toLocaleDateString('pt-BR')}</p>
|
||||
</div>
|
||||
<div className="text-right flex-shrink-0 space-y-0.5">
|
||||
<p className="font-black text-sm text-indigo-600">R$ {Number(r.valor_tutor || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}</p>
|
||||
<p className="text-[9px] text-gray-400 font-bold">{r.pix_key ? `PIX: ${r.pix_key}` : 'SEM PIX'}</p>
|
||||
{r.comissao_pct && (
|
||||
<p className="text-[9px] text-gray-400">PLAT: {r.comissao_pct}% = R$ {Number(r.valor_plataforma || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-gray-100 pt-2 flex justify-end">
|
||||
{r.status_repasse === 'PAGO' ? (
|
||||
<span className="flex items-center gap-1 text-[9px] font-black px-2 py-1 rounded-full bg-green-100 text-green-700 uppercase">
|
||||
<CheckCircle2 size={10} /> PAGO {r.repasse_at ? new Date(r.repasse_at).toLocaleDateString('pt-BR') : ''}
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => confirmar(r.id, r.tutor_nome, parseFloat(r.valor_tutor) || 0)}
|
||||
className="flex items-center gap-1.5 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-xl text-[10px] font-black uppercase transition-colors">
|
||||
<Banknote size={12} /> CONFIRMAR REPASSE
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── View principal ───────────────────────────────────────────────
|
||||
|
||||
export const AdminGestaoTutoresView: React.FC = () => {
|
||||
const [tab, setTab] = useState<'candidaturas' | 'ativos' | 'config' | 'financeiro'>('candidaturas');
|
||||
const [candidaturas, setCandidaturas] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filtroStatus, setFiltroStatus] = useState('');
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await HybridBackend.getAdminCandidaturas(filtroStatus || undefined);
|
||||
if (res.success) setCandidaturas(res.candidaturas || []);
|
||||
} catch {}
|
||||
finally { setLoading(false); }
|
||||
}, [filtroStatus]);
|
||||
|
||||
useEffect(() => { if (tab !== 'config') load(); }, [tab, load]);
|
||||
|
||||
const pendentes = candidaturas.filter(c => c.status === 'PENDENTE').length;
|
||||
const aguardandoPgto = candidaturas.filter(c => c.status === 'AGUARDANDO_PAGAMENTO').length;
|
||||
const ativos = candidaturas.filter(c => c.status === 'ATIVO');
|
||||
|
||||
const tabs = [
|
||||
{ id: 'candidaturas' as const, label: 'CANDIDATURAS', badge: pendentes + aguardandoPgto },
|
||||
{ id: 'ativos' as const, label: `TUTORES ATIVOS (${ativos.length})`, badge: 0 },
|
||||
{ id: 'financeiro' as const, label: 'FINANCEIRO', badge: 0 },
|
||||
{ id: 'config' as const, label: 'CONFIGURAÇÕES', badge: 0 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="mb-4 md:mb-6">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<h2 className="text-lg md:text-2xl font-bold text-gray-900 uppercase leading-tight">GESTÃO DE TUTORES ORTODÔNTICOS</h2>
|
||||
<button onClick={load} className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors text-gray-400 flex-shrink-0">
|
||||
<RefreshCw size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
{[
|
||||
{ label: 'CANDIDATURAS', value: candidaturas.length, color: 'text-gray-700' },
|
||||
{ label: 'PENDENTES', value: pendentes, color: 'text-amber-600' },
|
||||
{ label: 'AGUARD. PGTO', value: aguardandoPgto, color: 'text-blue-600' },
|
||||
{ label: 'TUTORES ATIVOS', value: ativos.length, color: 'text-green-600' },
|
||||
].map(s => (
|
||||
<div key={s.label} className="bg-white border border-gray-200 rounded-xl p-4 text-center">
|
||||
<p className={`text-2xl font-black ${s.color}`}>{s.value}</p>
|
||||
<p className="text-[9px] font-black text-gray-400 uppercase mt-1">{s.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-1 p-1 bg-gray-100 rounded-xl overflow-x-auto">
|
||||
{tabs.map(t => (
|
||||
<button key={t.id} onClick={() => setTab(t.id)}
|
||||
className={`flex items-center gap-1.5 px-3 md:px-4 py-2 rounded-xl text-[10px] font-black uppercase transition-all whitespace-nowrap flex-shrink-0 ${tab === t.id ? 'bg-white text-gray-800 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}>
|
||||
{t.label}
|
||||
{t.badge > 0 && (
|
||||
<span className={`text-[8px] font-black px-1.5 py-0.5 rounded-full ${tab === t.id ? 'bg-red-500 text-white' : 'bg-red-100 text-red-600'}`}>{t.badge}</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Conteúdo */}
|
||||
{tab === 'config' ? (
|
||||
<ConfigTab />
|
||||
) : tab === 'financeiro' ? (
|
||||
<FinanceiroTab />
|
||||
) : loading ? (
|
||||
<div className="flex items-center justify-center h-40"><Loader2 size={28} className="animate-spin text-gray-300" /></div>
|
||||
) : tab === 'candidaturas' ? (
|
||||
<div className="space-y-4">
|
||||
{/* Filtro */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{['', 'PENDENTE', 'AGUARDANDO_PAGAMENTO', 'APROVADO', 'REJEITADO', 'ATIVO'].map(s => (
|
||||
<button key={s} onClick={() => setFiltroStatus(s)}
|
||||
className={`px-3 py-1.5 rounded-full text-[10px] font-black uppercase border transition-all ${filtroStatus === s ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white text-gray-500 border-gray-200 hover:border-indigo-300'}`}>
|
||||
{s || 'TODOS'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{candidaturas.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-gray-400 gap-2">
|
||||
<GraduationCap size={32} />
|
||||
<p className="text-sm font-black uppercase">NENHUMA CANDIDATURA</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{candidaturas.map(c => <CandidaturaCard key={c.id} c={c} onAction={load} />)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// Tutores ativos
|
||||
<div className="space-y-3">
|
||||
{ativos.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-gray-400 gap-2">
|
||||
<Users size={32} />
|
||||
<p className="text-sm font-black uppercase">NENHUM TUTOR ATIVO AINDA</p>
|
||||
</div>
|
||||
) : ativos.map(c => (
|
||||
<div key={c.id} className="bg-white border border-gray-200 rounded-xl p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center font-black text-green-600 flex-shrink-0">
|
||||
{c.nome?.charAt(0)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-black text-sm text-gray-800 uppercase truncate">{c.nome}</p>
|
||||
<p className="text-[10px] text-gray-500 font-bold uppercase truncate">CRO {c.cro_uf}/{c.cro} • {c.titulacao}</p>
|
||||
</div>
|
||||
<span className="flex items-center gap-1 text-[9px] font-black px-2 py-0.5 rounded-full bg-green-100 text-green-700 uppercase flex-shrink-0">
|
||||
<CheckCircle2 size={10} /> ATIVO
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between pl-[52px]">
|
||||
<p className="text-[10px] text-gray-500 font-bold uppercase truncate">{c.especialidade_principal}</p>
|
||||
<p className="font-black text-indigo-600 text-sm flex-shrink-0 ml-2">
|
||||
{c.preco_por_consulta ? `R$ ${Number(c.preco_por_consulta).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}` : '—'}
|
||||
<span className="text-[9px] text-gray-400 font-normal ml-1">/ consulta</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user