feat(financeiro+salas): motor financeiro V1/V2, glosas, GTO→financeiro, contratos de convênio e redesenho de Salas
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>
This commit is contained in:
@@ -29,8 +29,8 @@ function StatusBanner({ candidatura, onPagar }: { candidatura: any; onPagar: ()
|
||||
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-blue-500" />,
|
||||
cor: 'bg-blue-50 border-blue-200',
|
||||
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.',
|
||||
},
|
||||
@@ -62,7 +62,7 @@ function StatusBanner({ candidatura, onPagar }: { candidatura: any; onPagar: ()
|
||||
{candidatura.status === 'AGUARDANDO_PAGAMENTO' && (
|
||||
<button
|
||||
onClick={onPagar}
|
||||
className="mt-4 flex items-center gap-2 px-5 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-xl text-[11px] font-black uppercase transition-colors shadow-sm"
|
||||
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>
|
||||
@@ -128,10 +128,10 @@ function CredenciaisPagamento({ candidatura }: { candidatura: any }) {
|
||||
|
||||
{/* Split info */}
|
||||
{comissaoPct !== null && (
|
||||
<div className="bg-indigo-50 border border-indigo-100 rounded-xl p-4 grid grid-cols-2 gap-4 text-center">
|
||||
<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-indigo-600">{comissaoPct}%</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>
|
||||
@@ -190,7 +190,7 @@ 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-indigo-100 rounded-full flex items-center justify-center text-indigo-600 font-black text-lg">
|
||||
<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>
|
||||
@@ -201,7 +201,7 @@ function PerfilAtivoCard({ candidatura }: { candidatura: any }) {
|
||||
<div className="ml-auto text-right">
|
||||
{candidatura.preco_por_consulta && (
|
||||
<div>
|
||||
<p className="text-2xl font-black text-indigo-600">R$ {Number(candidatura.preco_por_consulta).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}</p>
|
||||
<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>
|
||||
)}
|
||||
@@ -269,27 +269,27 @@ function CandidaturaForm({ onSubmitted, initialData }: { onSubmitted: () => void
|
||||
<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-indigo-400 uppercase" placeholder="DR. JOÃO SILVA" />
|
||||
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-indigo-400" placeholder="dr@email.com" />
|
||||
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-indigo-400" placeholder="(11) 99999-9999" />
|
||||
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-indigo-400" placeholder="123456" />
|
||||
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-indigo-400 uppercase" placeholder="SP" />
|
||||
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>
|
||||
),
|
||||
@@ -304,7 +304,7 @@ function CandidaturaForm({ onSubmitted, initialData }: { onSubmitted: () => void
|
||||
<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-indigo-400">
|
||||
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>
|
||||
@@ -312,23 +312,23 @@ function CandidaturaForm({ onSubmitted, initialData }: { onSubmitted: () => void
|
||||
<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-indigo-400" placeholder="10" />
|
||||
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-indigo-400 uppercase" placeholder="USP, UNICAMP..." />
|
||||
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-indigo-400" placeholder="2015" />
|
||||
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-indigo-400">
|
||||
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>
|
||||
@@ -340,8 +340,8 @@ function CandidaturaForm({ onSubmitted, initialData }: { onSubmitted: () => void
|
||||
<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-indigo-600 text-white border-indigo-600'
|
||||
: 'bg-white text-gray-600 border-gray-200 hover:border-indigo-300'
|
||||
? 'bg-emerald-600 text-white border-emerald-600'
|
||||
: 'bg-white text-gray-600 border-gray-200 hover:border-emerald-300'
|
||||
}`}>
|
||||
{esp}
|
||||
</button>
|
||||
@@ -361,24 +361,24 @@ function CandidaturaForm({ onSubmitted, initialData }: { onSubmitted: () => void
|
||||
<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-indigo-400 h-28 resize-none" />
|
||||
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-indigo-400" placeholder="350" />
|
||||
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-indigo-400" placeholder="https://linkedin.com/in/..." />
|
||||
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-indigo-400" placeholder="https://..." />
|
||||
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>
|
||||
@@ -411,7 +411,7 @@ function CandidaturaForm({ onSubmitted, initialData }: { onSubmitted: () => void
|
||||
{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-indigo-600 text-white' : i < step ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-400'
|
||||
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>
|
||||
@@ -440,7 +440,7 @@ function CandidaturaForm({ onSubmitted, initialData }: { onSubmitted: () => void
|
||||
)}
|
||||
{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-indigo-600 hover:bg-indigo-700 text-white rounded-xl text-[11px] font-black uppercase transition-colors disabled:opacity-40">
|
||||
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>
|
||||
) : (
|
||||
@@ -478,8 +478,8 @@ function PagamentoModal({ candidaturaId, onClose, onPago }: { candidaturaId: str
|
||||
<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-blue-100 rounded-xl flex items-center justify-center">
|
||||
<CreditCard size={18} className="text-blue-600" />
|
||||
<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>
|
||||
@@ -494,16 +494,16 @@ function PagamentoModal({ candidaturaId, onClose, onPago }: { candidaturaId: str
|
||||
</div>
|
||||
) : checkoutData ? (
|
||||
<>
|
||||
<div className="bg-blue-50 border border-blue-100 rounded-xl p-4 text-center">
|
||||
<p className="text-3xl font-black text-blue-600">R$ {Number(checkoutData.taxa).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}</p>
|
||||
<p className="text-[10px] text-blue-500 font-bold uppercase mt-1">TAXA DE ATIVAÇÃO — PAGAMENTO ÚNICO</p>
|
||||
<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-blue-600 hover:bg-blue-700 text-white rounded-xl font-black text-[11px] uppercase transition-colors shadow-sm">
|
||||
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>
|
||||
@@ -563,9 +563,9 @@ export const TutorCandidaturaView: React.FC = () => {
|
||||
) : !candidatura ? (
|
||||
// Sem candidatura — mostra intro + formulário
|
||||
<div className="space-y-6">
|
||||
<div className="bg-indigo-50 border border-indigo-100 rounded-2xl p-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-indigo-500 flex-shrink-0" />
|
||||
<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">
|
||||
@@ -578,7 +578,7 @@ export const TutorCandidaturaView: React.FC = () => {
|
||||
{ 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-indigo-100">
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user