// Comissoes e Metas — os dois paineis de gestao dos Relatorios Comerciais // // Moram fora de RelatoriosComerciais.jsx so por tamanho: aquele arquivo ja // carrega os dados e os filtros, e estes dois paineis sao telas de escrita, // nao de leitura. Quem busca do banco continua sendo o pai; aqui so chegam // props e callbacks. // --------------------------------------------------------------------- // Comissoes // --------------------------------------------------------------------- // A comissao nasce Pendente quando o orcamento e aprovado (trigger da // migration 056) e caminha Pendente -> Liberada -> Paga na mao. O valor // fica congelado no momento da aprovacao de proposito: mudar o percentual // amanha nao pode reescrever o que ja foi pago ontem. const COMISSAO_VARIANT = { 'Pendente': 'warning', 'Liberada': 'primary', 'Paga': 'success', }; function PainelComissoes(p) { const [salvando, setSalvando] = React.useState(false); const [editRegras, setEditRegras] = React.useState(null); // null = nao esta editando // Data da comissao e a data da venda, nao a do registro: os dois coincidem // hoje, mas o backfill da migration criou comissoes com created_at de // agora para vendas antigas, e por created_at elas cairiam no mes errado. const dataVenda = (c) => (c.budget?.aprovado_em || c.created_at || '').slice(0, 10); const lista = React.useMemo(() => p.comissoes.filter(c => { const d = dataVenda(c); if (p.ini && d && d < p.ini) return false; if (p.fim && d && d > p.fim) return false; if (p.fResponsavel !== 'todos' && String(c.responsavel_id || '') !== p.fResponsavel) return false; return true; }), [p.comissoes, p.ini, p.fim, p.fResponsavel]); // Comissao viva de orcamento que nao esta mais aprovado. Depois da // migration 057 isso so acontece com comissao ja liberada ou paga: o // dinheiro saiu, a venda caiu, e alguem precisa resolver na mao. const divergente = (c) => !!c.budget && c.budget.status !== 'Aprovado'; const divergentes = lista.filter(divergente); const soma = (st) => lista .filter(c => (st ? c.status === st : true)) .reduce((s, c) => s + (Number(c.valor_comissao) || 0), 0); const nomeCli = (c) => c.cliente?.name || c.budget?.client_name || 'Sem cliente'; // Aviso honesto: com percentual 0 a tela mostra "R$ 0,00" em tudo e // parece quebrada. O problema nao e a tela, e a regra nao configurada. const semRegra = p.regras.length === 0 || p.regras.every(r => !Number(r.percentual)); // Comissao pendente cujo cliente continua sem tipo de venda no CRM: nenhuma // regra casa e ela vale 0% para sempre. Nao da para consertar aqui — o // campo mora no cadastro do cliente — entao a tela avisa e diz onde ir. const semTipo = lista.filter(c => c.status === 'Pendente' && !(c.cliente?.tipo_venda)); const clientesSemTipo = [...new Set(semTipo.map(nomeCli))]; const mudarStatus = async (c, novo) => { // Voltar para Pendente uma comissao cuja venda foi desfeita nao e // "voltar um passo": e desfazer a comissao. Manter a linha recriaria o // estado que a migration 057 existe para impedir — pendente sem venda // aprovada. O trigger do banco nao alcanca este caminho, porque quem // muda aqui e a comissao, nao o status do orcamento. if (novo === 'Pendente' && divergente(c)) { if (!window.confirm('A venda deste orçamento foi desfeita.\n\nReverter aqui APAGA a comissão. Confirmar?')) return; const { error } = await sb.from('comissoes').delete().eq('id', c.id); if (error) { p.onToast('Não consegui remover a comissão.', 'error'); return; } p.onRecarregar(); p.onToast('Comissão removida — o orçamento não está mais aprovado.'); return; } const patch = { status: novo }; if (novo === 'Liberada') { patch.liberada_em = new Date().toISOString(); patch.pago_em = null; patch.pago_por = null; } if (novo === 'Paga') { patch.pago_em = new Date().toISOString(); patch.pago_por = p.currentUser?.id || null; } if (novo === 'Pendente') { patch.liberada_em = null; patch.pago_em = null; patch.pago_por = null; } const { error } = await sb.from('comissoes').update(patch).eq('id', c.id); if (error) { p.onToast('Não consegui atualizar a comissão.', 'error'); return; } p.onMudou(c.id, patch); p.onToast(`Comissão marcada como ${novo}.`); }; const salvarRegras = async () => { setSalvando(true); const erros = []; for (const [tipo, valor] of Object.entries(editRegras)) { const pct = Math.min(100, Math.max(0, parseFloat(String(valor).replace(',', '.')) || 0)); const existente = p.regras.find(r => r.tipo_venda === tipo); const res = existente ? await sb.from('comissao_regras').update({ percentual: pct, updated_at: new Date().toISOString() }).eq('id', existente.id) : await sb.from('comissao_regras').insert({ tipo_venda: tipo, percentual: pct }); if (res.error) erros.push(tipo); } setSalvando(false); if (erros.length) { p.onToast(`Não consegui salvar: ${erros.join(', ')}.`, 'error'); return; } setEditRegras(null); p.onRecarregar(); p.onToast('Regras de comissão salvas.'); }; // As comissoes ja geradas guardam o percentual antigo. Quando a regra e // definida pela primeira vez, todas elas ficariam em zero para sempre — // por isso este botao. So mexe nas Pendentes: liberada ou paga e passado. // // O tipo de venda tambem e relido do CRM, nao so o percentual. A comissao // nasce com o tipo congelado na aprovacao, e uma venda aprovada antes de o // cliente ser classificado nasce "Nao classificado" — sem isso ela ficaria // presa em 0% para sempre, porque nenhuma regra casa com tipo nulo. // Classificar o cliente no CRM e clicar aqui e o caminho de conserto. const recalcularPendentes = async () => { if (!window.confirm('Recalcular todas as comissões PENDENTES?\n\nO tipo de venda é relido do cadastro atual do cliente no CRM, e o percentual, das regras atuais.\n\nComissões liberadas ou pagas não são alteradas.')) return; setSalvando(true); const pendentes = p.comissoes.filter(c => c.status === 'Pendente'); let alteradas = 0; for (const c of pendentes) { // Cliente apagado depois da venda: sem cadastro para reler, o tipo // congelado na comissao e a melhor informacao que existe. const tipo = c.cliente ? (c.cliente.tipo_venda || null) : (c.tipo_venda || null); const regra = p.regras.find(r => r.tipo_venda === tipo && r.ativo); const pct = Number(regra?.percentual) || 0; if (pct === Number(c.percentual) && tipo === (c.tipo_venda || null)) continue; const { error } = await sb.from('comissoes') .update({ tipo_venda: tipo, percentual: pct }).eq('id', c.id); if (!error) alteradas++; } setSalvando(false); p.onRecarregar(); p.onToast(alteradas > 0 ? `${alteradas} comissão(ões) recalculada(s).` : 'Nada a recalcular.'); }; const colunas = [ { label: 'Data da venda', get: c => (dataVenda(c) || '').split('-').reverse().join('/') }, { label: 'Orçamento', get: c => c.budget?.code || '' }, { label: 'Cliente', get: c => nomeCli(c) }, { label: 'Projeto/Ação', get: c => c.budget?.event_name || '' }, { label: 'Responsável', get: c => c.responsavel?.name || 'Sem responsável' }, { label: 'Tipo de venda', get: c => c.tipo_venda || 'Não classificado' }, { label: 'Valor da venda', get: c => (Number(c.valor_venda) || 0).toFixed(2).replace('.', ','), show: c => fmtBRL(Number(c.valor_venda) || 0) }, { label: '%', get: c => (Number(c.percentual) || 0).toFixed(2).replace('.', ','), show: c => (Number(c.percentual) || 0).toFixed(2).replace('.', ',') + '%' }, { label: 'Comissão', get: c => (Number(c.valor_comissao) || 0).toFixed(2).replace('.', ','), show: c => fmtBRL(Number(c.valor_comissao) || 0) }, { label: 'Situação', get: c => c.status || '' }, ]; return (