import React, { useState, useEffect, useCallback } from 'react'; import { Percent, Wallet, History, Plus, Trash2, Lock, X, RefreshCw, Filter, AlertTriangle, ChevronDown, ChevronRight, CheckCircle, Paperclip, FileText } from 'lucide-react'; import { HybridBackend } from '../services/backend.ts'; import { useToast } from '../contexts/ToastContext.tsx'; import { useConfirm } from '../contexts/ConfirmContext.tsx'; import { PageHeader } from '../components/PageHeader.tsx'; const COLOR = '#0d9488'; const fmtBRL = (v: number) => 'R$ ' + Number(v || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 }); const hoje = new Date(); const inicioMes = new Date(hoje.getFullYear(), hoje.getMonth(), 1).toISOString().split('T')[0]; const fimHoje = hoje.toISOString().split('T')[0]; const dataBR = (d: string) => (d || '').slice(0, 10).split('-').reverse().join('/'); type Aba = 'relatorio' | 'regras' | 'historico'; export const ComissoesView: React.FC = () => { const toast = useToast(); const confirm = useConfirm(); const [aba, setAba] = useState('relatorio'); return (
{([['relatorio', 'Relatório / Fechamento', Wallet], ['regras', 'Regras de %', Percent], ['historico', 'Histórico', History]] as const).map(([id, label, Icon]) => ( ))}
{aba === 'relatorio' && } {aba === 'regras' && } {aba === 'historico' && }
); }; // ─── Aba 1: Relatório ao vivo + Fechar repasse ──────────────────────────────── const AbaRelatorio: React.FC<{ toast: any; confirm: any }> = ({ toast, confirm }) => { const [inicio, setInicio] = useState(inicioMes); const [fim, setFim] = useState(fimHoje); const [base, setBase] = useState<'recebido' | 'producao'>('recebido'); const [dados, setDados] = useState(null); const [loading, setLoading] = useState(false); const [fechando, setFechando] = useState(false); const [aberto, setAberto] = useState(null); const carregar = useCallback(() => { setLoading(true); HybridBackend.getComissoesRelatorio({ inicio, fim, base }).then(setDados).catch(() => setDados(null)).finally(() => setLoading(false)); }, [inicio, fim, base]); useEffect(() => { carregar(); }, [carregar]); const fechar = async () => { if (!dados?.grupos?.length) { toast.error('NADA A FECHAR NO PERÍODO.'); return; } if (!await confirm(`FECHAR REPASSE de ${dataBR(inicio)} a ${dataBR(fim)} (base ${base.toUpperCase()})? Serão gravados ${dados.grupos.length} fechamento(s) — total ${fmtBRL(dados.total_comissao)}. Esta ação fica registrada para auditoria.`)) return; setFechando(true); try { const r = await HybridBackend.fecharRepasse({ inicio, fim, base }); toast.success(`REPASSE FECHADO: ${r.criados} PROFISSIONAL(IS), TOTAL ${fmtBRL(r.total_comissao)}.`); } catch (e: any) { toast.error((e.message || 'ERRO').toUpperCase()); } finally { setFechando(false); } }; return (
{/* Filtros */}
setInicio(e.target.value)} className="block bg-gray-50 border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-bold text-gray-700 outline-none focus:border-teal-400" />
setFim(e.target.value)} className="block bg-gray-50 border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-bold text-gray-700 outline-none focus:border-teal-400" />

{base === 'recebido' ? 'Recebido: só lançamentos PAGOS (caixa).' : 'Produção: tudo lançado no período (competência), pago ou não.'} Garantias/reaberturas sem comissão são ignoradas.

{loading ?
: (

Comissão por profissional

{Number(dados?.total_custo_lab) > 0 && (

Custo lab

{fmtBRL(dados?.total_custo_lab || 0)}

)}

Total comissão

{fmtBRL(dados?.total_comissao || 0)}

{(dados?.grupos || []).length === 0 ? (
Nenhuma comissão no período.
) : (
{dados.grupos.map((g: any) => (
{aberto === g.profissional_id && (
{g.detalhe.map((d: any) => ( ))}
DescriçãoValorLab%Comissão
{d.descricao} {fmtBRL(d.valor)} {Number(d.custo_lab) > 0 ? '-' + fmtBRL(d.custo_lab) : '—'} {d.percentual}% {fmtBRL(d.comissao)}
)}
))}
)}
)}
); }; // ─── Aba 2: Regras de % (padrão + por procedimento + override por profissional) ── const AbaRegras: React.FC<{ toast: any; confirm: any }> = ({ toast, confirm }) => { const [regras, setRegras] = useState([]); const [procs, setProcs] = useState([]); const [dentistas, setDentistas] = useState([]); const [loading, setLoading] = useState(false); const [padrao, setPadrao] = useState(''); const [novoProc, setNovoProc] = useState(''); const [novoPct, setNovoPct] = useState(''); const [novoLab, setNovoLab] = useState(''); const [novoProf, setNovoProf] = useState(''); const [novoProfPct, setNovoProfPct] = useState(''); const carregar = useCallback(async () => { setLoading(true); try { const [r, p, d] = await Promise.all([HybridBackend.getComissaoRegras(), HybridBackend.getProcedimentos(), HybridBackend.getDentistas()]); setRegras(Array.isArray(r) ? r : []); setProcs(Array.isArray(p) ? p : []); setDentistas(Array.isArray(d) ? d : []); const def = (r || []).find((x: any) => !x.procedimento_id && !x.profissional_id); setPadrao(def ? String(def.percentual) : ''); } catch { /* silent */ } finally { setLoading(false); } }, []); useEffect(() => { carregar(); }, [carregar]); const salvarPadrao = async () => { const v = Number(padrao); if (!(v >= 0 && v <= 100)) { toast.error('PERCENTUAL ENTRE 0 E 100.'); return; } try { await HybridBackend.saveComissaoRegra({ percentual: v }); toast.success('PERCENTUAL PADRÃO SALVO.'); carregar(); } catch (e: any) { toast.error((e.message || 'ERRO').toUpperCase()); } }; const addRegra = async () => { const v = Number(novoPct); if (!novoProc) { toast.error('SELECIONE O PROCEDIMENTO.'); return; } if (!(v >= 0 && v <= 100)) { toast.error('PERCENTUAL ENTRE 0 E 100.'); return; } try { await HybridBackend.saveComissaoRegra({ procedimento_id: novoProc, percentual: v, custo_lab: Number(novoLab) || 0 }); toast.success('REGRA SALVA.'); setNovoProc(''); setNovoPct(''); setNovoLab(''); carregar(); } catch (e: any) { toast.error((e.message || 'ERRO').toUpperCase()); } }; const addOverride = async () => { const v = Number(novoProfPct); if (!novoProf) { toast.error('SELECIONE O PROFISSIONAL.'); return; } if (!(v >= 0 && v <= 100)) { toast.error('PERCENTUAL ENTRE 0 E 100.'); return; } try { await HybridBackend.saveComissaoRegra({ profissional_id: novoProf, percentual: v }); toast.success('OVERRIDE SALVO.'); setNovoProf(''); setNovoProfPct(''); carregar(); } catch (e: any) { toast.error((e.message || 'ERRO').toUpperCase()); } }; const excluir = async (id: string) => { if (!await confirm('EXCLUIR esta regra de comissão?')) return; try { await HybridBackend.deleteComissaoRegra(id); toast.success('REGRA EXCLUÍDA.'); carregar(); } catch (e: any) { toast.error((e.message || 'ERRO').toUpperCase()); } }; const regrasProc = regras.filter(r => r.procedimento_id); const regrasProf = regras.filter(r => r.profissional_id); return (
{/* Padrão */}

Percentual padrão da clínica

Aplicado quando não há override do profissional nem regra do procedimento.

setPadrao(e.target.value)} placeholder="0" className="w-28 bg-gray-50 border border-gray-200 rounded-xl pl-3 pr-8 py-2.5 text-sm font-bold text-gray-700 outline-none focus:border-teal-400" />

Prioridade: override do profissional → regra do procedimento → padrão.

{/* Override por profissional */}

Override por profissional

% fixo para um profissional, prevalece sobre as demais regras.

setNovoProfPct(e.target.value)} placeholder="0" className="w-24 bg-gray-50 border border-gray-200 rounded-xl pl-3 pr-8 py-2.5 text-sm font-bold text-gray-700 outline-none focus:border-teal-400" />
{regrasProf.length === 0 ?

Nenhum override.

: (
{regrasProf.map(r => (
{r.profissional_nome || r.profissional_id}
{r.percentual}%
))}
)}
{/* Por procedimento (+ custo de lab) */}

Percentual por procedimento

Opcional: custo de laboratório deduzido do valor antes da comissão.

setNovoPct(e.target.value)} placeholder="0" className="w-20 bg-gray-50 border border-gray-200 rounded-xl pl-3 pr-7 py-2.5 text-sm font-bold text-gray-700 outline-none focus:border-teal-400" />
setNovoLab(e.target.value)} placeholder="0,00" className="w-28 bg-gray-50 border border-gray-200 rounded-xl px-3 py-2.5 text-sm font-bold text-gray-700 outline-none focus:border-teal-400" />
{loading ?
: regrasProc.length === 0 ? (

Nenhuma regra específica — usando o padrão.

) : (
{regrasProc.map(r => (
{r.procedimento_nome || r.procedimento_id}
{Number(r.custo_lab) > 0 && lab {fmtBRL(r.custo_lab)}} {r.percentual}%
))}
)}
); }; // ─── Aba 3: Histórico de fechamentos (auditoria) ────────────────────────────── const AbaHistorico: React.FC<{ toast: any; confirm: any }> = ({ toast, confirm }) => { const [lista, setLista] = useState([]); const [loading, setLoading] = useState(false); const carregar = useCallback(() => { setLoading(true); HybridBackend.getRepasses().then(r => setLista(Array.isArray(r) ? r : [])).catch(() => setLista([])).finally(() => setLoading(false)); }, []); useEffect(() => { carregar(); }, [carregar]); const cancelar = async (r: any) => { const msg = r.status === 'pago' ? `CANCELAR o repasse PAGO de ${r.profissional_nome} (${fmtBRL(r.comissao_valor)})? A despesa gerada no Financeiro será ESTORNADA.` : `CANCELAR o fechamento de ${r.profissional_nome} (${fmtBRL(r.comissao_valor)})? Fica registrado na auditoria.`; if (!await confirm(msg)) return; try { await HybridBackend.cancelarRepasse(r.id); toast.success(r.status === 'pago' ? 'REPASSE CANCELADO — DESPESA ESTORNADA.' : 'FECHAMENTO CANCELADO.'); carregar(); } catch (e: any) { toast.error((e.message || 'ERRO').toUpperCase()); } }; const pagar = async (r: any) => { if (!await confirm(`MARCAR como PAGO o repasse de ${r.profissional_nome} (${fmtBRL(r.comissao_valor)})? Será lançada uma DESPESA no Financeiro (centro de custo COMISSÕES).`)) return; try { await HybridBackend.pagarRepasse(r.id); toast.success('REPASSE PAGO — DESPESA LANÇADA NO FINANCEIRO.'); carregar(); } catch (e: any) { toast.error((e.message || 'ERRO').toUpperCase()); } }; const anexarComprovante = (r: any) => { const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*,application/pdf'; input.onchange = async () => { const f = input.files?.[0]; if (!f) return; try { await HybridBackend.uploadRepasseComprovante(r.id, f); toast.success('COMPROVANTE ANEXADO.'); carregar(); } catch (e: any) { toast.error((e.message || 'ERRO').toUpperCase()); } }; input.click(); }; const badge = (s: string) => s === 'cancelado' ? 'bg-red-100 text-red-600' : s === 'pago' ? 'bg-teal-100 text-teal-700' : 'bg-amber-100 text-amber-700'; if (loading) return
; if (lista.length === 0) return
Nenhum repasse fechado ainda.
; return (
{['Profissional', 'Período', 'Base', 'Lanç.', 'Comissão', 'Status', 'Por', ''].map(h => )} {lista.map(r => ( ))}
{h}
{r.profissional_nome} {dataBR(r.inicio)} – {dataBR(r.fim)} {r.base} {r.qtd_lancamentos} {fmtBRL(r.comissao_valor)}{Number(r.custo_lab_total) > 0 && lab {fmtBRL(r.custo_lab_total)}} {r.status} {r.status === 'pago' && r.pago_em && {dataBR(r.pago_em)}} {r.criado_por_nome || '—'} {r.status === 'fechado' && ( )} {r.status === 'pago' && ( r.comprovante_url ? : )} {(r.status === 'fechado' || r.status === 'pago') && ( )}
); };