/**
* Supera Glia - Tab Comercial (Vendas)
* @module tabs/ComercialTab
*/
(function() {
'use strict';
SuperaGlia.initComercialTab = function() {
const { useState, useEffect, useMemo } = SuperaGlia.hooks;
const { API_BASE } = SuperaGlia.config;
const { Icon } = SuperaGlia;
const { formatMoney, formatDate } = SuperaGlia.utils;
const { ResponsiveContainer, AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend } = SuperaGlia.charts;
SuperaGlia.ComercialTab = () => {
const [vendas, setVendas] = useState([]);
const [carregando, setCarregando] = useState(true);
const [erro, setErro] = useState(null);
// Filtros
const [dataInicio, setDataInicio] = useState(() => {
const d = new Date(); d.setMonth(d.getMonth() - 1);
return d.toISOString().split('T')[0];
});
const [dataFim, setDataFim] = useState(() => new Date().toISOString().split('T')[0]);
const [filtroProduto, setFiltroProduto] = useState('');
const [filtroVendedor, setFiltroVendedor] = useState('');
const [filtroMidia, setFiltroMidia] = useState('');
const [filtroInteresse, setFiltroInteresse] = useState('');
const [buscaVenda, setBuscaVenda] = useState('');
const [agrupamento, setAgrupamento] = useState('dia');
// Estados para listas de filtros
const [produtos, setProdutos] = useState([]);
const [vendedores, setVendedores] = useState([]);
const [midias, setMidias] = useState([]);
const [interesses, setInteresses] = useState([]);
const carregarVendas = async () => {
setCarregando(true);
setErro(null);
try {
const params = new URLSearchParams({ dataInicio, dataFim });
if (filtroProduto) params.append('produto', filtroProduto);
if (filtroVendedor) params.append('vendedor', filtroVendedor);
const res = await fetch(`${API_BASE}/financeiro/vendas?${params}`).then(r => r.json()).catch(() => ({ vendas: [] }));
const vendasData = res?.vendas || [];
setVendas(vendasData);
// Extrair listas únicas para filtros
setProdutos([...new Set(vendasData.map(v => v.produto).filter(Boolean))].sort());
setVendedores([...new Set(vendasData.map(v => v.vendedor).filter(Boolean))].sort());
setMidias([...new Set(vendasData.map(v => v.midia_contato).filter(Boolean))].sort());
setInteresses([...new Set(vendasData.map(v => v.interesse).filter(Boolean))].sort());
} catch (e) {
console.error('Erro ao carregar vendas:', e);
setErro('Erro ao carregar dados de vendas');
} finally {
setCarregando(false);
}
};
useEffect(() => { carregarVendas(); }, []);
// Aplicar filtros locais
const vendasFiltradas = useMemo(() => {
return vendas.filter(v => {
if (buscaVenda && !v.nome?.toLowerCase().includes(buscaVenda.toLowerCase()) &&
!v.vendedor?.toLowerCase().includes(buscaVenda.toLowerCase()) &&
!v.produto?.toLowerCase().includes(buscaVenda.toLowerCase())) return false;
if (filtroProduto && v.produto !== filtroProduto) return false;
if (filtroVendedor && v.vendedor !== filtroVendedor) return false;
if (filtroMidia && v.midia_contato !== filtroMidia) return false;
if (filtroInteresse && v.interesse !== filtroInteresse) return false;
return true;
});
}, [vendas, buscaVenda, filtroProduto, filtroVendedor, filtroMidia, filtroInteresse]);
// Calcular totais
const totais = useMemo(() => {
const result = {
quantidade: vendasFiltradas.length,
valorBruto: vendasFiltradas.reduce((s, v) => s + (parseFloat(v.valor_bruto) || 0), 0),
valorDesconto: vendasFiltradas.reduce((s, v) => s + (parseFloat(v.valor_desconto) || 0), 0),
valorEntrada: vendasFiltradas.reduce((s, v) => s + (parseFloat(v.valor_entrada) || 0), 0)
};
result.valorLiquido = result.valorBruto - result.valorDesconto;
return result;
}, [vendasFiltradas]);
// Função para obter chave de agrupamento
const getChaveAgrupamento = (dataStr) => {
if (!dataStr) return 'Sem data';
const data = dataStr.split('T')[0];
const [ano, mes, dia] = data.split('-');
if (agrupamento === 'mes') {
const meses = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'];
return `${meses[parseInt(mes) - 1]}/${ano}`;
} else if (agrupamento === 'semana') {
const d = new Date(parseInt(ano), parseInt(mes) - 1, parseInt(dia));
const primeiroDia = new Date(d.getFullYear(), 0, 1);
const numSemana = Math.ceil(((d - primeiroDia) / 86400000 + primeiroDia.getDay() + 1) / 7);
return `Sem ${numSemana}/${ano}`;
}
return data;
};
// Dados para gráfico
const dadosGrafico = useMemo(() => {
const agrupado = {};
vendasFiltradas.forEach(v => {
const chave = getChaveAgrupamento(v.data_venda);
if (!agrupado[chave]) {
agrupado[chave] = { periodo: chave, quantidade: 0, valorBruto: 0, dataSort: v.data_venda || '' };
}
agrupado[chave].quantidade++;
agrupado[chave].valorBruto += parseFloat(v.valor_bruto) || 0;
});
return Object.values(agrupado).sort((a, b) => a.dataSort.localeCompare(b.dataSort));
}, [vendasFiltradas, agrupamento]);
// Função para exportar PDF
const exportarPDF = () => {
const printWindow = window.open('', '_blank');
const html = `
Relatório de Vendas - Supera São Bento
Total de Vendas
${totais.quantidade}
Valor Bruto
${formatMoney(totais.valorBruto)}
Descontos
${formatMoney(totais.valorDesconto)}
Valor Líquido
${formatMoney(totais.valorLiquido)}
| Data | Cliente | Vendedor | Produto |
Valor Bruto | Desconto | Entrada | Parcelas |
${vendasFiltradas.map(v => `
| ${formatDate(v.data_venda)} |
${v.nome || '-'} |
${v.vendedor || '-'} |
${v.produto || '-'} |
${formatMoney(v.valor_bruto)} |
${formatMoney(v.valor_desconto)} |
${formatMoney(v.valor_entrada)} |
${v.num_parcelas || '-'} |
`).join('')}
`;
printWindow.document.write(html);
printWindow.document.close();
printWindow.onload = () => { printWindow.print(); };
};
if (carregando) {
return React.createElement('div', { className: 'flex items-center justify-center h-64' },
React.createElement('div', { className: 'text-center' },
React.createElement('div', { className: 'w-10 h-10 border-4 border-orange-200 border-t-orange-500 rounded-full animate-spin mx-auto mb-4' }),
React.createElement('p', { className: 'text-gray-500' }, 'Carregando dados comerciais...')
)
);
}
return React.createElement('div', { className: 'space-y-6' },
// Cards de Resumo
React.createElement('div', { className: 'grid grid-cols-1 md:grid-cols-4 gap-4' },
[
{ label: 'Total de Vendas', value: totais.quantidade, icon: 'shopping-cart', color: 'from-blue-500 to-blue-600' },
{ label: 'Valor Bruto', value: formatMoney(totais.valorBruto), icon: 'dollar-sign', color: 'from-green-500 to-green-600' },
{ label: 'Descontos', value: formatMoney(totais.valorDesconto), icon: 'percent', color: 'from-amber-500 to-amber-600' },
{ label: 'Valor Líquido', value: formatMoney(totais.valorLiquido), icon: 'trending-up', color: 'from-orange-500 to-orange-600' }
].map((card, i) => React.createElement('div', {
key: i,
className: 'bg-white rounded-xl shadow-sm border border-gray-100 p-5 stat-card'
},
React.createElement('div', { className: 'flex items-center justify-between mb-2' },
React.createElement('span', { className: 'text-sm text-gray-500' }, card.label),
React.createElement('div', { className: `w-10 h-10 rounded-xl bg-gradient-to-br ${card.color} flex items-center justify-center` },
React.createElement(Icon, { name: card.icon, className: 'w-5 h-5 text-white' })
)
),
React.createElement('p', { className: 'text-2xl font-bold text-gray-900' }, card.value)
))
),
// Filtros
React.createElement('div', { className: 'bg-white rounded-xl shadow-sm border border-gray-100 p-5' },
React.createElement('div', { className: 'flex items-center justify-between mb-4' },
React.createElement('h3', { className: 'text-sm font-semibold text-gray-700 flex items-center gap-2' },
React.createElement(Icon, { name: 'filter', className: 'w-4 h-4' }),
'Filtros'
),
React.createElement('div', { className: 'flex gap-2' },
React.createElement('button', {
onClick: carregarVendas,
className: 'flex items-center gap-2 px-4 py-2 bg-orange-500 text-white rounded-lg text-sm font-medium hover:bg-orange-600 transition-colors'
},
React.createElement(Icon, { name: 'search', className: 'w-4 h-4' }),
'Buscar'
),
React.createElement('button', {
onClick: exportarPDF,
className: 'flex items-center gap-2 px-4 py-2 bg-gray-700 text-white rounded-lg text-sm font-medium hover:bg-gray-800 transition-colors'
},
React.createElement(Icon, { name: 'file-text', className: 'w-4 h-4' }),
'Exportar PDF'
)
)
),
React.createElement('div', { className: 'grid grid-cols-1 md:grid-cols-3 lg:grid-cols-6 gap-4' },
React.createElement('div', null,
React.createElement('label', { className: 'block text-xs font-medium text-gray-500 mb-1' }, 'Data Início'),
React.createElement('input', { type: 'date', value: dataInicio, onChange: e => setDataInicio(e.target.value), className: 'w-full px-3 py-2 border border-gray-200 rounded-lg text-sm' })
),
React.createElement('div', null,
React.createElement('label', { className: 'block text-xs font-medium text-gray-500 mb-1' }, 'Data Fim'),
React.createElement('input', { type: 'date', value: dataFim, onChange: e => setDataFim(e.target.value), className: 'w-full px-3 py-2 border border-gray-200 rounded-lg text-sm' })
),
React.createElement('div', null,
React.createElement('label', { className: 'block text-xs font-medium text-gray-500 mb-1' }, 'Produto'),
React.createElement('select', { value: filtroProduto, onChange: e => setFiltroProduto(e.target.value), className: 'w-full px-3 py-2 border border-gray-200 rounded-lg text-sm' },
React.createElement('option', { value: '' }, 'Todos'),
produtos.map(p => React.createElement('option', { key: p, value: p }, p))
)
),
React.createElement('div', null,
React.createElement('label', { className: 'block text-xs font-medium text-gray-500 mb-1' }, 'Vendedor'),
React.createElement('select', { value: filtroVendedor, onChange: e => setFiltroVendedor(e.target.value), className: 'w-full px-3 py-2 border border-gray-200 rounded-lg text-sm' },
React.createElement('option', { value: '' }, 'Todos'),
vendedores.map(v => React.createElement('option', { key: v, value: v }, v))
)
),
React.createElement('div', null,
React.createElement('label', { className: 'block text-xs font-medium text-gray-500 mb-1' }, 'Mídia'),
React.createElement('select', { value: filtroMidia, onChange: e => setFiltroMidia(e.target.value), className: 'w-full px-3 py-2 border border-gray-200 rounded-lg text-sm' },
React.createElement('option', { value: '' }, 'Todas'),
midias.map(m => React.createElement('option', { key: m, value: m }, m))
)
),
React.createElement('div', null,
React.createElement('label', { className: 'block text-xs font-medium text-gray-500 mb-1' }, 'Agrupamento'),
React.createElement('select', { value: agrupamento, onChange: e => setAgrupamento(e.target.value), className: 'w-full px-3 py-2 border border-gray-200 rounded-lg text-sm' },
React.createElement('option', { value: 'dia' }, 'Por Dia'),
React.createElement('option', { value: 'semana' }, 'Por Semana'),
React.createElement('option', { value: 'mes' }, 'Por Mês')
)
)
)
),
// Gráfico
dadosGrafico.length > 0 && React.createElement('div', { className: 'bg-white rounded-xl shadow-sm border border-gray-100 p-5' },
React.createElement('h3', { className: 'text-sm font-semibold text-gray-700 mb-4 flex items-center gap-2' },
React.createElement(Icon, { name: 'bar-chart-2', className: 'w-4 h-4' }),
'Evolução de Vendas'
),
React.createElement(ResponsiveContainer, { width: '100%', height: 300 },
React.createElement(AreaChart, { data: dadosGrafico },
React.createElement(CartesianGrid, { strokeDasharray: '3 3', stroke: '#f1f5f9' }),
React.createElement(XAxis, { dataKey: 'periodo', tick: { fontSize: 11 } }),
React.createElement(YAxis, { tick: { fontSize: 11 }, yAxisId: 'left' }),
React.createElement(YAxis, { tick: { fontSize: 11 }, yAxisId: 'right', orientation: 'right', tickFormatter: v => formatMoney(v).replace('R$', '') }),
React.createElement(Tooltip, { formatter: (value, name) => name === 'Valor Bruto' ? [formatMoney(value), 'Valor Bruto'] : [value, 'Quantidade'] }),
React.createElement(Legend),
React.createElement(Area, { yAxisId: 'left', type: 'monotone', dataKey: 'quantidade', name: 'Quantidade', stroke: '#F37021', fill: '#FEF3EC', strokeWidth: 2 }),
React.createElement(Area, { yAxisId: 'right', type: 'monotone', dataKey: 'valorBruto', name: 'Valor Bruto', stroke: '#10B981', fill: '#D1FAE5', strokeWidth: 2 })
)
)
),
// Tabela de Vendas
React.createElement('div', { className: 'bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden' },
React.createElement('div', { className: 'p-4 border-b border-gray-100 flex items-center justify-between' },
React.createElement('h3', { className: 'text-sm font-semibold text-gray-700 flex items-center gap-2' },
React.createElement(Icon, { name: 'list', className: 'w-4 h-4' }),
`Vendas (${vendasFiltradas.length})`
),
React.createElement('input', {
type: 'text',
placeholder: 'Buscar...',
value: buscaVenda,
onChange: e => setBuscaVenda(e.target.value),
className: 'px-3 py-1.5 border border-gray-200 rounded-lg text-sm w-48'
})
),
React.createElement('div', { className: 'overflow-x-auto' },
React.createElement('table', { className: 'w-full' },
React.createElement('thead', { className: 'bg-gray-50' },
React.createElement('tr', null,
['Data', 'Cliente', 'Vendedor', 'Produto', 'Valor Bruto', 'Desconto', 'Entrada', 'Parcelas'].map(h =>
React.createElement('th', { key: h, className: `text-${h.includes('Valor') || h === 'Desconto' || h === 'Entrada' ? 'right' : 'left'} text-xs font-semibold text-gray-500 uppercase px-4 py-3` }, h)
)
)
),
React.createElement('tbody', { className: 'divide-y divide-gray-100' },
vendasFiltradas.length === 0
? React.createElement('tr', null, React.createElement('td', { colSpan: 8, className: 'text-center text-gray-500 py-8' }, 'Nenhuma venda encontrada'))
: vendasFiltradas.slice(0, 100).map((v, i) =>
React.createElement('tr', { key: i, className: 'hover:bg-gray-50 transition-colors' },
React.createElement('td', { className: 'px-4 py-3 text-sm text-gray-900' }, formatDate(v.data_venda)),
React.createElement('td', { className: 'px-4 py-3 text-sm font-medium text-gray-900' }, v.nome || '-'),
React.createElement('td', { className: 'px-4 py-3 text-sm text-gray-700' }, v.vendedor || '-'),
React.createElement('td', { className: 'px-4 py-3' },
React.createElement('span', { className: 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800' }, v.produto || '-')
),
React.createElement('td', { className: 'px-4 py-3 text-sm text-right font-medium text-gray-900' }, formatMoney(v.valor_bruto)),
React.createElement('td', { className: 'px-4 py-3 text-sm text-right text-amber-600' }, formatMoney(v.valor_desconto)),
React.createElement('td', { className: 'px-4 py-3 text-sm text-right text-green-600' }, formatMoney(v.valor_entrada)),
React.createElement('td', { className: 'px-4 py-3 text-sm text-center text-gray-700' }, v.num_parcelas || '-')
)
)
)
)
),
vendasFiltradas.length > 100 && React.createElement('div', { className: 'p-3 bg-gray-50 border-t text-center text-sm text-gray-500' },
`Mostrando 100 de ${vendasFiltradas.length} registros`
)
)
);
};
};
})();