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:
VPS 4 Builder
2026-06-16 00:28:07 +02:00
parent 92a0ec3917
commit 3e64514b08
62 changed files with 6528 additions and 1035 deletions
+34 -34
View File
@@ -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>