3e64514b08
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>
387 lines
28 KiB
TypeScript
387 lines
28 KiB
TypeScript
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<Aba>('relatorio');
|
||
|
||
return (
|
||
<div className="space-y-6 pb-10">
|
||
<PageHeader title="COMISSÕES & REPASSE" />
|
||
<div className="flex gap-2 border-b border-gray-200">
|
||
{([['relatorio', 'Relatório / Fechamento', Wallet], ['regras', 'Regras de %', Percent], ['historico', 'Histórico', History]] as const).map(([id, label, Icon]) => (
|
||
<button key={id} onClick={() => setAba(id)}
|
||
className={`flex items-center gap-2 px-4 py-2.5 text-xs font-black uppercase tracking-wide border-b-2 -mb-px transition-colors ${aba === id ? 'border-teal-600 text-teal-700' : 'border-transparent text-gray-400 hover:text-gray-600'}`}>
|
||
<Icon size={15} /> {label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{aba === 'relatorio' && <AbaRelatorio toast={toast} confirm={confirm} />}
|
||
{aba === 'regras' && <AbaRegras toast={toast} confirm={confirm} />}
|
||
{aba === 'historico' && <AbaHistorico toast={toast} confirm={confirm} />}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ─── 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<any | null>(null);
|
||
const [loading, setLoading] = useState(false);
|
||
const [fechando, setFechando] = useState(false);
|
||
const [aberto, setAberto] = useState<string | null>(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 (
|
||
<div className="space-y-6">
|
||
{/* Filtros */}
|
||
<div className="bg-white p-5 rounded-2xl shadow-sm border border-gray-100 flex flex-wrap items-end gap-4">
|
||
<div className="space-y-1.5">
|
||
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Início</label>
|
||
<input type="date" value={inicio} onChange={e => 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" />
|
||
</div>
|
||
<div className="space-y-1.5">
|
||
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Fim</label>
|
||
<input type="date" value={fim} onChange={e => 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" />
|
||
</div>
|
||
<div className="space-y-1.5">
|
||
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Base de cálculo</label>
|
||
<div className="inline-flex rounded-xl border border-gray-200 overflow-hidden">
|
||
<button onClick={() => setBase('recebido')} className={`px-4 py-2.5 text-xs font-bold uppercase transition-colors ${base === 'recebido' ? 'bg-teal-600 text-white' : 'bg-white text-gray-500 hover:bg-gray-50'}`}>Recebido</button>
|
||
<button onClick={() => setBase('producao')} className={`px-4 py-2.5 text-xs font-bold uppercase transition-colors ${base === 'producao' ? 'bg-teal-600 text-white' : 'bg-white text-gray-500 hover:bg-gray-50'}`}>Produção</button>
|
||
</div>
|
||
</div>
|
||
<button onClick={carregar} className="bg-gray-100 text-gray-600 px-4 py-2.5 rounded-xl font-black text-xs uppercase hover:bg-gray-200 transition-colors flex items-center gap-2"><Filter size={14} /> Atualizar</button>
|
||
<button onClick={fechar} disabled={fechando || !dados?.grupos?.length} className="ml-auto text-white px-5 py-2.5 rounded-xl font-black text-xs uppercase transition-colors flex items-center gap-2 disabled:opacity-50" style={{ backgroundColor: COLOR }}>
|
||
{fechando ? <RefreshCw size={14} className="animate-spin" /> : <Lock size={14} />} Fechar repasse
|
||
</button>
|
||
</div>
|
||
|
||
<p className="text-[11px] text-gray-400 font-bold uppercase flex items-center gap-1.5">
|
||
<AlertTriangle size={12} className="text-amber-500" />
|
||
{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.
|
||
</p>
|
||
|
||
{loading ? <div className="flex justify-center py-16"><RefreshCw className="animate-spin text-teal-600" size={30} /></div> : (
|
||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
|
||
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
|
||
<h3 className="text-sm font-black text-gray-800 uppercase">Comissão por profissional</h3>
|
||
<div className="flex items-center gap-6">
|
||
{Number(dados?.total_custo_lab) > 0 && (
|
||
<div className="text-right">
|
||
<p className="text-[10px] font-black text-gray-400 uppercase">Custo lab</p>
|
||
<p className="text-sm font-black text-amber-600">{fmtBRL(dados?.total_custo_lab || 0)}</p>
|
||
</div>
|
||
)}
|
||
<div className="text-right">
|
||
<p className="text-[10px] font-black text-gray-400 uppercase">Total comissão</p>
|
||
<p className="text-lg font-black text-teal-700">{fmtBRL(dados?.total_comissao || 0)}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{(dados?.grupos || []).length === 0 ? (
|
||
<div className="py-12 text-center text-gray-400 text-sm font-bold uppercase">Nenhuma comissão no período.</div>
|
||
) : (
|
||
<div className="divide-y divide-gray-100">
|
||
{dados.grupos.map((g: any) => (
|
||
<div key={g.profissional_id}>
|
||
<button onClick={() => setAberto(aberto === g.profissional_id ? null : g.profissional_id)} className="w-full flex items-center gap-3 px-6 py-4 hover:bg-gray-50/50 transition-colors text-left">
|
||
{aberto === g.profissional_id ? <ChevronDown size={16} className="text-gray-400" /> : <ChevronRight size={16} className="text-gray-400" />}
|
||
<div className="flex-1 min-w-0">
|
||
<p className="font-black text-gray-800 text-sm uppercase truncate">{g.profissional_nome}</p>
|
||
<p className="text-[10px] text-gray-400 font-bold uppercase">{g.qtd} lançamento(s) · base {fmtBRL(g.base_valor)} · {g.percentual_medio}% médio</p>
|
||
</div>
|
||
<span className="text-base font-black text-teal-700 shrink-0">{fmtBRL(g.comissao_valor)}</span>
|
||
</button>
|
||
{aberto === g.profissional_id && (
|
||
<div className="px-6 pb-4 bg-gray-50/40">
|
||
<table className="w-full text-xs">
|
||
<thead><tr className="text-[10px] font-black text-gray-400 uppercase">
|
||
<th className="text-left py-1.5">Descrição</th><th className="text-right">Valor</th><th className="text-right">Lab</th><th className="text-right">%</th><th className="text-right">Comissão</th>
|
||
</tr></thead>
|
||
<tbody>
|
||
{g.detalhe.map((d: any) => (
|
||
<tr key={d.id} className="border-t border-gray-100">
|
||
<td className="py-1.5 font-bold text-gray-600 uppercase">{d.descricao}</td>
|
||
<td className="text-right text-gray-500">{fmtBRL(d.valor)}</td>
|
||
<td className="text-right text-amber-600">{Number(d.custo_lab) > 0 ? '-' + fmtBRL(d.custo_lab) : '—'}</td>
|
||
<td className="text-right text-gray-500">{d.percentual}%</td>
|
||
<td className="text-right font-black text-teal-700">{fmtBRL(d.comissao)}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ─── 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<any[]>([]);
|
||
const [procs, setProcs] = useState<any[]>([]);
|
||
const [dentistas, setDentistas] = useState<any[]>([]);
|
||
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 (
|
||
<div className="space-y-6 max-w-3xl">
|
||
{/* Padrão */}
|
||
<div className="bg-white p-5 rounded-2xl shadow-sm border border-gray-100">
|
||
<h3 className="text-sm font-black text-gray-800 uppercase mb-1">Percentual padrão da clínica</h3>
|
||
<p className="text-[11px] text-gray-400 font-bold mb-3">Aplicado quando não há override do profissional nem regra do procedimento.</p>
|
||
<div className="flex items-center gap-2">
|
||
<div className="relative">
|
||
<input type="number" min="0" max="100" value={padrao} onChange={e => 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" />
|
||
<Percent size={14} className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-300" />
|
||
</div>
|
||
<button onClick={salvarPadrao} className="text-white px-4 py-2.5 rounded-xl font-black text-xs uppercase" style={{ backgroundColor: COLOR }}>Salvar</button>
|
||
</div>
|
||
<p className="text-[10px] text-gray-400 font-bold mt-3 uppercase">Prioridade: override do profissional → regra do procedimento → padrão.</p>
|
||
</div>
|
||
|
||
{/* Override por profissional */}
|
||
<div className="bg-white p-5 rounded-2xl shadow-sm border border-gray-100">
|
||
<h3 className="text-sm font-black text-gray-800 uppercase mb-1">Override por profissional</h3>
|
||
<p className="text-[11px] text-gray-400 font-bold mb-3">% fixo para um profissional, prevalece sobre as demais regras.</p>
|
||
<div className="flex flex-wrap items-end gap-2 mb-4">
|
||
<select value={novoProf} onChange={e => setNovoProf(e.target.value)} className="flex-1 min-w-[180px] 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">
|
||
<option value="">Selecione o profissional…</option>
|
||
{dentistas.map(d => <option key={d.id} value={d.id}>{d.nome}</option>)}
|
||
</select>
|
||
<div className="relative">
|
||
<input type="number" min="0" max="100" value={novoProfPct} onChange={e => 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" />
|
||
<Percent size={14} className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-300" />
|
||
</div>
|
||
<button onClick={addOverride} className="text-white px-4 py-2.5 rounded-xl font-black text-xs uppercase flex items-center gap-1.5" style={{ backgroundColor: COLOR }}><Plus size={14} /> Adicionar</button>
|
||
</div>
|
||
{regrasProf.length === 0 ? <p className="text-sm text-gray-400 font-bold uppercase py-2 text-center">Nenhum override.</p> : (
|
||
<div className="divide-y divide-gray-100">
|
||
{regrasProf.map(r => (
|
||
<div key={r.id} className="flex items-center justify-between py-2.5">
|
||
<span className="text-sm font-bold text-gray-700 uppercase">{r.profissional_nome || r.profissional_id}</span>
|
||
<div className="flex items-center gap-3">
|
||
<span className="text-sm font-black text-teal-700">{r.percentual}%</span>
|
||
<button onClick={() => excluir(r.id)} className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"><Trash2 size={15} /></button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Por procedimento (+ custo de lab) */}
|
||
<div className="bg-white p-5 rounded-2xl shadow-sm border border-gray-100">
|
||
<h3 className="text-sm font-black text-gray-800 uppercase mb-1">Percentual por procedimento</h3>
|
||
<p className="text-[11px] text-gray-400 font-bold mb-3">Opcional: custo de laboratório deduzido do valor antes da comissão.</p>
|
||
<div className="flex flex-wrap items-end gap-2 mb-4">
|
||
<select value={novoProc} onChange={e => setNovoProc(e.target.value)} className="flex-1 min-w-[160px] 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">
|
||
<option value="">Selecione o procedimento…</option>
|
||
{procs.map(p => <option key={p.id} value={p.id}>{p.nome}</option>)}
|
||
</select>
|
||
<div>
|
||
<label className="text-[9px] font-black text-gray-400 uppercase block mb-1">%</label>
|
||
<div className="relative">
|
||
<input type="number" min="0" max="100" value={novoPct} onChange={e => 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" />
|
||
<Percent size={13} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-gray-300" />
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label className="text-[9px] font-black text-gray-400 uppercase block mb-1">Custo lab (R$)</label>
|
||
<input type="number" min="0" value={novoLab} onChange={e => 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" />
|
||
</div>
|
||
<button onClick={addRegra} className="text-white px-4 py-2.5 rounded-xl font-black text-xs uppercase flex items-center gap-1.5" style={{ backgroundColor: COLOR }}><Plus size={14} /> Adicionar</button>
|
||
</div>
|
||
{loading ? <div className="flex justify-center py-6"><RefreshCw className="animate-spin text-teal-500" size={20} /></div> : regrasProc.length === 0 ? (
|
||
<p className="text-sm text-gray-400 font-bold uppercase py-3 text-center">Nenhuma regra específica — usando o padrão.</p>
|
||
) : (
|
||
<div className="divide-y divide-gray-100">
|
||
{regrasProc.map(r => (
|
||
<div key={r.id} className="flex items-center justify-between py-2.5">
|
||
<span className="text-sm font-bold text-gray-700 uppercase">{r.procedimento_nome || r.procedimento_id}</span>
|
||
<div className="flex items-center gap-3">
|
||
{Number(r.custo_lab) > 0 && <span className="text-[10px] font-black text-amber-600 uppercase">lab {fmtBRL(r.custo_lab)}</span>}
|
||
<span className="text-sm font-black text-teal-700">{r.percentual}%</span>
|
||
<button onClick={() => excluir(r.id)} className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"><Trash2 size={15} /></button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ─── Aba 3: Histórico de fechamentos (auditoria) ──────────────────────────────
|
||
const AbaHistorico: React.FC<{ toast: any; confirm: any }> = ({ toast, confirm }) => {
|
||
const [lista, setLista] = useState<any[]>([]);
|
||
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 <div className="flex justify-center py-16"><RefreshCw className="animate-spin text-teal-600" size={28} /></div>;
|
||
if (lista.length === 0) return <div className="py-16 text-center text-gray-400 text-sm font-bold uppercase">Nenhum repasse fechado ainda.</div>;
|
||
|
||
return (
|
||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-x-auto">
|
||
<table className="w-full text-left text-xs">
|
||
<thead className="bg-gray-50/60">
|
||
<tr>{['Profissional', 'Período', 'Base', 'Lanç.', 'Comissão', 'Status', 'Por', ''].map(h => <th key={h} className="px-4 py-3 text-[10px] font-black text-gray-400 uppercase tracking-widest">{h}</th>)}</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-gray-100">
|
||
{lista.map(r => (
|
||
<tr key={r.id} className={`hover:bg-gray-50/50 ${r.status === 'cancelado' ? 'opacity-50' : ''}`}>
|
||
<td className="px-4 py-3 font-black text-gray-800 uppercase">{r.profissional_nome}</td>
|
||
<td className="px-4 py-3 text-gray-500 font-bold whitespace-nowrap">{dataBR(r.inicio)} – {dataBR(r.fim)}</td>
|
||
<td className="px-4 py-3"><span className="px-2 py-0.5 rounded-full text-[9px] font-black uppercase bg-gray-100 text-gray-600">{r.base}</span></td>
|
||
<td className="px-4 py-3 text-gray-500">{r.qtd_lancamentos}</td>
|
||
<td className="px-4 py-3 font-black text-teal-700">{fmtBRL(r.comissao_valor)}{Number(r.custo_lab_total) > 0 && <span className="block text-[9px] font-bold text-amber-600">lab {fmtBRL(r.custo_lab_total)}</span>}</td>
|
||
<td className="px-4 py-3">
|
||
<span className={`px-2 py-0.5 rounded-full text-[9px] font-black uppercase ${badge(r.status)}`}>{r.status}</span>
|
||
{r.status === 'pago' && r.pago_em && <span className="block text-[9px] text-gray-400 mt-0.5">{dataBR(r.pago_em)}</span>}
|
||
</td>
|
||
<td className="px-4 py-3 text-gray-400 uppercase">{r.criado_por_nome || '—'}</td>
|
||
<td className="px-4 py-3 text-right whitespace-nowrap">
|
||
{r.status === 'fechado' && (
|
||
<button onClick={() => pagar(r)} title="MARCAR PAGO" className="p-1.5 text-green-600 hover:bg-green-50 rounded-lg transition-colors"><CheckCircle size={15} /></button>
|
||
)}
|
||
{r.status === 'pago' && (
|
||
r.comprovante_url
|
||
? <a href={r.comprovante_url} target="_blank" rel="noopener noreferrer" title="VER COMPROVANTE" className="inline-flex p-1.5 text-teal-600 hover:bg-teal-50 rounded-lg transition-colors"><FileText size={15} /></a>
|
||
: <button onClick={() => anexarComprovante(r)} title="ANEXAR COMPROVANTE" className="p-1.5 text-gray-400 hover:text-teal-600 hover:bg-teal-50 rounded-lg transition-colors"><Paperclip size={15} /></button>
|
||
)}
|
||
{(r.status === 'fechado' || r.status === 'pago') && (
|
||
<button onClick={() => cancelar(r)} title={r.status === 'pago' ? 'CANCELAR (ESTORNA DESPESA)' : 'CANCELAR'} className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"><X size={15} /></button>
|
||
)}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
);
|
||
};
|