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:
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user