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
+8 -8
View File
@@ -18,7 +18,7 @@ const STATUS: Record<StatusKey, { label: string; cor: string; bg: string }> = {
coroa: { label: 'COROA', cor: '#d97706', bg: '#fffbeb' },
tratamento: { label: 'TRAT. CANAL',cor: '#7c3aed', bg: '#f5f3ff' },
implante: { label: 'IMPLANTE', cor: '#0d9488', bg: '#f0fdfa' },
fratura: { label: 'FRATURA', cor: '#ea580c', bg: '#fff7ed' },
fratura: { label: 'FRATURA', cor: '#0d9488', bg: '#fff7ed' },
};
const STATUS_ORDER: StatusKey[] = ['higido', 'carie', 'restaurado', 'tratamento', 'coroa', 'implante', 'fratura', 'ausente'];
@@ -31,7 +31,7 @@ const Tooth: React.FC<{ num: string; st?: ToothState; selected: boolean; onClick
<button
onClick={onClick}
title={cfg ? `${num} · ${cfg.label}` : num}
className={`relative flex flex-col items-center justify-center w-7 h-9 sm:w-8 sm:h-10 rounded-md border text-[9px] font-black transition-all ${selected ? 'ring-2 ring-blue-500 ring-offset-1 scale-110 z-10' : 'hover:scale-105'}`}
className={`relative flex flex-col items-center justify-center w-7 h-9 sm:w-8 sm:h-10 rounded-md border text-[9px] font-black transition-all ${selected ? 'ring-2 ring-teal-500 ring-offset-1 scale-110 z-10' : 'hover:scale-105'}`}
style={{ borderColor: cfg ? cfg.cor : '#e2e8f0', backgroundColor: cfg ? cfg.bg : '#fff', color: cfg ? cfg.cor : '#64748b' }}
>
{st?.status === 'ausente' && <span className="absolute inset-0 flex items-center justify-center text-base leading-none" style={{ color: '#1f2937' }}>×</span>}
@@ -119,7 +119,7 @@ export const AvaliacaoEditor: React.FC<Props> = ({ agendamentoId, avaliacaoId, p
<div className="bg-white w-full sm:max-w-3xl sm:rounded-2xl shadow-2xl flex flex-col max-h-screen sm:max-h-[92vh] overflow-hidden" onClick={e => e.stopPropagation()}>
{/* header */}
<div className="flex items-center gap-3 px-4 sm:px-6 py-4 border-b border-gray-100">
<div className="w-9 h-9 rounded-xl bg-blue-50 flex items-center justify-center text-blue-600 flex-shrink-0"><ClipboardList size={18} /></div>
<div className="w-9 h-9 rounded-xl bg-teal-50 flex items-center justify-center text-teal-600 flex-shrink-0"><ClipboardList size={18} /></div>
<div className="min-w-0 flex-1">
<p className="text-sm font-black uppercase text-gray-800 truncate leading-tight">Avaliação clínica</p>
<p className="text-[11px] text-gray-400 font-bold uppercase truncate">{pacienteNome || aval?.paciente_nome || '—'}</p>
@@ -153,8 +153,8 @@ export const AvaliacaoEditor: React.FC<Props> = ({ agendamentoId, avaliacaoId, p
{/* Painel do dente selecionado */}
{sel ? (
<div className="border border-blue-100 bg-blue-50/40 rounded-xl p-3 space-y-3">
<p className="text-[11px] font-black uppercase text-blue-700">Dente {sel}</p>
<div className="border border-teal-100 bg-teal-50/40 rounded-xl p-3 space-y-3">
<p className="text-[11px] font-black uppercase text-teal-700">Dente {sel}</p>
<div className="flex flex-wrap gap-1.5">
{STATUS_ORDER.map(k => {
const active = selSt?.status === k;
@@ -168,7 +168,7 @@ export const AvaliacaoEditor: React.FC<Props> = ({ agendamentoId, avaliacaoId, p
})}
</div>
<input value={selSt?.nota || ''} onChange={e => setToothNota(e.target.value)} placeholder="Observação do dente (opcional)"
className="w-full px-3 py-2 rounded-lg border border-gray-200 text-xs focus:outline-none focus:border-blue-400" />
className="w-full px-3 py-2 rounded-lg border border-gray-200 text-xs focus:outline-none focus:border-teal-400" />
</div>
) : (
<p className="text-[11px] text-gray-400 font-medium text-center -mt-2">Toque em um dente para marcar o estado.</p>
@@ -178,7 +178,7 @@ export const AvaliacaoEditor: React.FC<Props> = ({ agendamentoId, avaliacaoId, p
<div>
<p className="text-[11px] font-black uppercase text-gray-500 mb-2">Observações gerais</p>
<textarea value={obs} onChange={e => setObs(e.target.value)} rows={3} placeholder="Diagnóstico, conduta, plano de tratamento…"
className="w-full px-3 py-2 rounded-xl border border-gray-200 text-sm resize-none focus:outline-none focus:border-blue-400 custom-scrollbar" />
className="w-full px-3 py-2 rounded-xl border border-gray-200 text-sm resize-none focus:outline-none focus:border-teal-400 custom-scrollbar" />
</div>
{/* Fotos dos dentes */}
@@ -186,7 +186,7 @@ export const AvaliacaoEditor: React.FC<Props> = ({ agendamentoId, avaliacaoId, p
<div className="flex items-center justify-between mb-2">
<p className="text-[11px] font-black uppercase text-gray-500">Fotos {sel ? `· dente ${sel}` : ''}</p>
<button onClick={() => fileRef.current?.click()} disabled={uploading}
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-blue-600 text-white text-[10px] font-black uppercase hover:bg-blue-700 disabled:opacity-50">
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-teal-600 text-white text-[10px] font-black uppercase hover:bg-teal-700 disabled:opacity-50">
{uploading ? <Loader2 size={13} className="animate-spin" /> : <Camera size={13} />} Adicionar
</button>
<input ref={fileRef} type="file" accept="image/*" capture="environment" className="hidden" onChange={onPick} />