/** * Rede Analytics React Component * Página de analytics das transações Rede com Grid.js */ import React, { useEffect, useRef, useState } from 'react'; import { __ } from '@wordpress/i18n'; import { addFilter } from '@wordpress/hooks'; import { Grid, html } from 'gridjs'; import { decode } from '@toon-format/toon'; import 'gridjs/dist/theme/mermaid.css'; // Definição das colunas padrão const DEFAULT_COLUMNS = [ { id: 'gateway', name: 'Card/PIX', visible: true }, { id: 'cvv_sent', name: 'CVV Enviado', visible: true }, { id: 'type', name: 'Tipo', visible: true }, { id: 'installments', name: 'Parcelas', visible: true }, { id: 'installment_amount', name: 'Vlr. Parcela', visible: true }, { id: 'brand', name: 'Bandeira', visible: true }, { id: 'expiry', name: 'Vencimento', visible: true }, { id: 'datetime', name: 'Data/Hora', visible: true }, { id: 'total', name: 'Total', visible: true }, { id: 'subtotal', name: 'Subtotal', visible: true }, { id: 'shipping', name: 'Frete', visible: true }, { id: 'interest_discount', name: 'Juros/Desc.', visible: true }, { id: 'currency', name: 'Moeda', visible: true }, { id: 'capture', name: 'Captura', visible: true }, { id: 'recurrent', name: 'Recorrente', visible: true }, { id: 'auth_3ds', name: '3DS Auth', visible: true }, { id: 'tid', name: 'TID/PaymentId', visible: true }, { id: 'environment', name: 'Ambiente', visible: true }, { id: 'payment_gateway', name: 'Gateway', visible: true }, { id: 'order_id', name: 'Order ID', visible: true }, { id: 'reference', name: 'Reference', visible: true }, { id: 'pv', name: 'PV', visible: true }, { id: 'token', name: 'Token', visible: true }, { id: 'return_code', name: 'Return Code', visible: true }, { id: 'http_status', name: 'HTTP Status', visible: true }, { id: 'holder_name', name: 'Portador', visible: true }, { id: 'whatsapp', name: 'Suporte', visible: true } ]; // Componente principal para Analytics do Rede // Sistema de Tooltip usando DOM puro para compatibilidade com Grid.js class TooltipManager { private static instance: TooltipManager; private tooltipElement: HTMLDivElement | null = null; private arrowElement: HTMLDivElement | null = null; private currentTarget: HTMLElement | null = null; static getInstance(): TooltipManager { if (!TooltipManager.instance) { TooltipManager.instance = new TooltipManager(); } return TooltipManager.instance; } private createTooltipElement(): HTMLDivElement { const tooltip = document.createElement('div'); tooltip.className = 'rede-tooltip-manager'; tooltip.style.cssText = ` position: absolute; background-color: #333; color: white; padding: 8px 12px; border-radius: 4px; font-size: 12px; white-space: nowrap; z-index: 999999; box-shadow: 0 2px 8px rgba(0,0,0,0.3); pointer-events: none; display: none; min-width: fit-content; width: auto; height: auto; `; // Adicionar div para o conteúdo (sem position absolute) const content = document.createElement('div'); content.className = 'tooltip-content'; tooltip.appendChild(content); document.body.appendChild(tooltip); return tooltip; } private createArrowElement(): HTMLDivElement { const arrow = document.createElement('div'); arrow.className = 'rede-tooltip-arrow'; arrow.style.cssText = ` position: absolute; width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 6px solid #333; z-index: 999998; pointer-events: none; display: none; `; document.body.appendChild(arrow); return arrow; } showTooltip(target: HTMLElement, content: string): void { if (!this.tooltipElement) { this.tooltipElement = this.createTooltipElement(); } if (!this.arrowElement) { this.arrowElement = this.createArrowElement(); } this.currentTarget = target; // Definir conteúdo no div específico para o conteúdo const contentDiv = this.tooltipElement.querySelector('.tooltip-content') as HTMLElement; if (contentDiv) { contentDiv.textContent = content; } else { // Fallback caso o elemento não seja encontrado this.tooltipElement.textContent = content; } // Aplicar estilo simples sem corte de texto this.tooltipElement.style.whiteSpace = 'normal'; this.tooltipElement.style.maxWidth = '300px'; this.tooltipElement.style.wordBreak = 'break-word'; this.tooltipElement.style.display = 'block'; this.tooltipElement.style.overflow = 'visible'; this.tooltipElement.style.lineHeight = '1.4'; // Calcular posição base const rect = target.getBoundingClientRect(); const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; // Posição da setinha: mais acima do elemento const arrowTop = rect.top + scrollTop - 15; // 15px acima do elemento const arrowLeft = rect.left + scrollLeft + (rect.width / 2); // Posicionar a setinha this.arrowElement.style.top = arrowTop + 'px'; this.arrowElement.style.left = arrowLeft + 'px'; this.arrowElement.style.transform = 'translateX(-50%)'; this.arrowElement.style.display = 'block'; const tooltipTop = Math.ceil(arrowTop); const tooltipLeft = arrowLeft; // Posicionar o balão grudado na setinha this.tooltipElement.style.top = tooltipTop + 'px'; this.tooltipElement.style.left = tooltipLeft + 'px'; this.tooltipElement.style.transform = 'translateX(-50%) translateY(-100%)'; } hideTooltip(): void { if (this.tooltipElement) { this.tooltipElement.style.display = 'none'; } if (this.arrowElement) { this.arrowElement.style.display = 'none'; } this.currentTarget = null; } private constructor() { // Esconder tooltip quando o mouse sair da área document.addEventListener('mouseover', (e) => { if (this.currentTarget && !this.currentTarget.contains(e.target as Node) && e.target !== this.tooltipElement) { this.hideTooltip(); } }); // Esconder tooltip quando Shift for pressionado (para evitar conflito com Shift+scroll) document.addEventListener('keydown', (e) => { if (e.key === 'Shift' && this.currentTarget) { this.hideTooltip(); } }); } } const RedeAnalyticsPage = () => { const gridRef = useRef(null); const [transactionData, setTransactionData] = useState([]); const [loading, setLoading] = useState(true); const [loadingMore, setLoadingMore] = useState(false); const [error, setError] = useState(null); // Estados para configuração de colunas const [columnConfig, setColumnConfig] = useState(DEFAULT_COLUMNS); const [showColumnConfig, setShowColumnConfig] = useState(false); const [draggedItem, setDraggedItem] = useState(null); const [dragOverItem, setDragOverItem] = useState(null); // Estados de paginação const [currentPage, setCurrentPage] = useState(1); const [hasNextPage, setHasNextPage] = useState(true); const [totalCount, setTotalCount] = useState(0); // Estados para filtros de data const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [activeFilter, setActiveFilter] = useState('hoje'); // 'hoje', '7dias', '30dias', 'personalizado' // Estados para configurações de paginação const [queryLimit, setQueryLimit] = useState(50); // Limite de consultas do banco (mais registros para encontrar transações Rede) const [perPageLimit, setPerPageLimit] = useState(10); // Transações por página exibidas no grid // Função para decodificar dados TOON usando a biblioteca @toon-format/toon const decodeToonData = (toonString: string) => { try { return decode(toonString); } catch (e) { console.error('Erro ao decodificar TOON:', e); return null; } }; // Funções para gerenciamento de colunas const saveColumnConfig = (config: typeof DEFAULT_COLUMNS) => { localStorage.setItem('rede_analytics_columns', JSON.stringify(config)); }; const loadColumnConfig = () => { try { const saved = localStorage.getItem('rede_analytics_columns'); if (saved) { const parsed = JSON.parse(saved); // Filtrar apenas colunas válidas que existem em DEFAULT_COLUMNS const validColumns = parsed.filter((savedCol: any) => DEFAULT_COLUMNS.find(defaultCol => defaultCol.id === savedCol.id) ); // Adicionar colunas padrão que não existem na configuração salva const mergedConfig = [...validColumns]; DEFAULT_COLUMNS.forEach(defaultCol => { if (!mergedConfig.find(col => col.id === defaultCol.id)) { mergedConfig.push(defaultCol); } }); return mergedConfig; } } catch (e) { console.error('Erro ao carregar configuração de colunas:', e); } return DEFAULT_COLUMNS; }; const moveColumn = (fromIndex: number, toIndex: number) => { const newConfig = [...columnConfig]; const [movedItem] = newConfig.splice(fromIndex, 1); newConfig.splice(toIndex, 0, movedItem); setColumnConfig(newConfig); saveColumnConfig(newConfig); }; const moveColumnUp = (index: number) => { if (index > 0) { moveColumn(index, index - 1); } }; const moveColumnDown = (index: number) => { if (index < columnConfig.length - 1) { moveColumn(index, index + 1); } }; const toggleColumnVisibility = (index: number) => { const newConfig = [...columnConfig]; newConfig[index].visible = !newConfig[index].visible; setColumnConfig(newConfig); saveColumnConfig(newConfig); }; const resetColumnConfig = () => { setColumnConfig(DEFAULT_COLUMNS); saveColumnConfig(DEFAULT_COLUMNS); }; // Drag and Drop handlers const handleDragStart = (e: React.DragEvent, index: number) => { setDraggedItem(index); e.dataTransfer.effectAllowed = 'move'; }; const handleDragOver = (e: React.DragEvent, index: number) => { e.preventDefault(); setDragOverItem(index); }; const handleDragLeave = () => { setDragOverItem(null); }; const handleDrop = (e: React.DragEvent, dropIndex: number) => { e.preventDefault(); if (draggedItem !== null && draggedItem !== dropIndex) { moveColumn(draggedItem, dropIndex); } setDraggedItem(null); setDragOverItem(null); }; // Função para decodificar entidades HTML corretamente const decodeHtmlEntities = (str: string) => { if (!str) return str; // Criar elemento temporário para decodificar entidades HTML const tempDiv = document.createElement('div'); tempDiv.innerHTML = str; return tempDiv.textContent || tempDiv.innerText || str; }; // Função para mapear bandeira ao arquivo de imagem const getBrandImage = (brand: string) => { if (!brand || brand === 'N/A' || brand.trim() === '') { return null; // Não aplicar imagem } const brandLower = brand.toLowerCase(); const gatewayBrandsUrl = (window as any).lknRedeAjax?.gateway_brands_url; if (!gatewayBrandsUrl) { return null; } // Mapear bandeiras conhecidas if (brandLower.includes('visa')) { return `${gatewayBrandsUrl}visa.webp`; } else if (brandLower.includes('master')) { return `${gatewayBrandsUrl}mastercard.webp`; } else if (brandLower.includes('elo')) { return `${gatewayBrandsUrl}elo.webp`; } else if (brandLower.includes('amex') || brandLower.includes('american express')) { return `${gatewayBrandsUrl}amex.webp`; } else if (brandLower.includes('diners')) { return `${gatewayBrandsUrl}diners.webp`; } else if (brandLower.includes('hipercard') || brandLower.includes('hiper')) { return `${gatewayBrandsUrl}hipercard.webp`; } else if (brandLower.includes('discover')) { return `${gatewayBrandsUrl}discover.webp`; } else if (brandLower.includes('jcb')) { return `${gatewayBrandsUrl}jcb.webp`; } else if (brandLower.includes('aura')) { return `${gatewayBrandsUrl}aura.webp`; } else if (brandLower.includes('paypal')) { return `${gatewayBrandsUrl}paypal.webp`; } else if (brandLower.includes('pix')) { return `${gatewayBrandsUrl}pix.webp`; } else { // Bandeira não reconhecida mas existe valor return `${gatewayBrandsUrl}other.webp`; } }; // Função para gerar HTML de bandeira com tooltip const generateBrandTooltipHTML = (brand: string): string => { const imageUrl = getBrandImage(brand); if (!imageUrl) { return brand; } return `${escapeHtml(brand)}`; }; // Função para gerar HTML de código com tooltip const generateCodeTooltipHTML = (code: string, label: string): string => { if (!code || code === 'N/A') { return code; } const shortCode = escapeHtml(code.split(' - ')[0]); const fullTooltip = escapeHtml(`${label}: ${code}`); return `
${shortCode}
i
`; }; /** * Aplica a lógica de censura baseada no tamanho da string. * @param {string|number} value - O valor a ser mascarado. * @returns {string} - O valor mascarado. */ function maskValue(value) { if (value === null || value === undefined || value === 'null') { return value; } const strValue = String(value); const len = strValue.length; // Mostra no máximo 4, mas nunca mais que 1/3 da string const keep = Math.min(4, Math.floor(len / 3)); const start = strValue.slice(0, keep); // Tratamento do slice(-0) const safeEnd = keep > 0 ? strValue.slice(-keep) : ''; // Preenchimento do meio const middle = '*'.repeat(Math.max(1, len - (keep * 2))); return `${start}${middle}${safeEnd}`; } // Função para gerar mensagem completa para debug const generateWhatsAppMessage = (transactionData: any) => { const pluginSlugs = { 'integration_rede_pix': 'lkn-integration-rede-for-woocommerce', 'rede_credit': 'lkn-integration-rede-for-woocommerce', 'rede_debit': 'lkn-integration-rede-for-woocommerce', 'rede_pix': 'rede-for-woocommerce-pro', } const analyticsData = (window as any).lknRedeAnalytics || {}; const fields = [ // Sistema `Pedido: ${transactionData.system?.order_id || 'N/A'}`, `Data/Hora: ${transactionData.system?.request_datetime || 'N/A'}`, `Ambiente: ${transactionData.system?.environment || 'N/A'}`, `Plugin: lkn-integration-rede-for-woocommerce v${transactionData.system?.version_free || 'N/A'} (Lançamento v${analyticsData.version_free || 'N/A'})`, `Plugin dependente: ${transactionData.system?.version_pro && transactionData.system?.version_pro !== 'N/A' ? `rede-for-woocommerce-pro v${transactionData.system?.version_pro || 'N/A'} (Lançamento v${analyticsData.version_pro || 'N/A'})` : 'N/A'}`, `Site: ${analyticsData.site_domain || 'N/A'}`, `Gateway: ${transactionData.system?.gateway || 'N/A'}`, `Reference: ${transactionData.system?.reference || 'N/A'}`, // Dados do cartão `Cartão/PIX: ${transactionData.gateway?.masked || 'N/A'}`, `CVV Enviado: ${transactionData.transaction?.cvv_sent || 'N/A'}`, `Tipo do Cartão: ${transactionData.gateway?.type || 'N/A'}`, `Bandeira: ${transactionData.gateway?.brand || 'N/A'}`, `Vencimento: ${transactionData.gateway?.expiry || 'N/A'}`, `Portador: ${transactionData.gateway?.holder_name || 'N/A'}`, // Dados da transação `Parcelas: ${transactionData.transaction?.installments || 'N/A'}`, `Valor Parcela: ${transactionData.transaction?.installment_amount || 'N/A'}`, `Captura: ${transactionData.transaction?.capture || 'N/A'}`, `Recorrente: ${transactionData.transaction?.recurrent || 'N/A'}`, `3DS Auth: ${transactionData.transaction?.['3ds_auth'] || 'N/A'}`, `TID/PaymentId: ${maskValue(transactionData.transaction?.tid) || 'N/A'}`, // Valores `Total: ${transactionData.amounts?.total || 'N/A'}`, `Subtotal: ${transactionData.amounts?.subtotal || 'N/A'}`, `Frete: ${transactionData.amounts?.shipping || 'N/A'}`, `Juros/Desc: ${transactionData.amounts?.interest_discount || 'N/A'}`, `Moeda: ${transactionData.amounts?.currency || 'N/A'}`, // Credentials `PV: ${transactionData.credentials?.pv_masked || 'N/A'}`, `Token: ${transactionData.credentials?.token_masked || 'N/A'}`, // Resposta da API (essencial para debug) `Return Code: ${transactionData.response?.return_code || 'N/A'}`, `HTTP Status: ${transactionData.response?.http_status || 'N/A'}` ]; return `#suporte Olá! Preciso de suporte com meu gateway de pagamento Rede. Estou com problemas na transação e segue os dados para verificação: ${fields.join(' | ')}. Aguardo retorno, obrigado!`; }; // Função para gerar link do WhatsApp const generateWhatsAppLink = (transactionData: any) => { const message = generateWhatsAppMessage(transactionData); return `https://api.whatsapp.com/send/?phone=${(window as any).lknRedeAjax.whatsapp_number || ''}&text=${encodeURIComponent(message)}`; }; // Função para buscar dados via AJAX const fetchTransactionData = async (page = 1, append = false, customStartDate?: string, customEndDate?: string) => { try { if (page === 1) { setLoading(true); } else { setLoadingMore(true); } if (page === 1) { setError(null); } // Usar as datas customizadas se fornecidas, senão usar as do estado const effectiveStartDate = customStartDate !== undefined ? customStartDate : startDate; const effectiveEndDate = customEndDate !== undefined ? customEndDate : endDate; const response = await fetch((window as any).lknRedeAjax.ajax_url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ action: (window as any).lknRedeAjax.action_get_recent_orders, nonce: (window as any).lknRedeAjax.nonce, response_format: 'toon', page: page.toString(), query_limit: queryLimit.toString(), start_date: effectiveStartDate, end_date: effectiveEndDate }) }); // Verificar Content-Type para determinar formato da resposta const contentType = response.headers.get('Content-Type') || ''; const isJsonResponse = contentType.includes('application/json'); const isTextResponse = contentType.includes('text/plain'); let result; if (isTextResponse) { // Resposta em formato TOON (text/plain) const responseText = await response.text(); result = decodeToonData(responseText); if (!result) { throw new Error('Falha ao decodificar resposta TOON'); } } else if (isJsonResponse) { // Resposta em formato JSON padrão do WordPress result = await response.json(); // Se é um wrapper JSON com dados TOON dentro if (result.success && result.data?.format === 'toon' && result.data?.toon_data) { const toonData = decodeToonData(result.data.toon_data); if (toonData) { result = toonData; } } } else { // Fallback: tentar como JSON primeiro, depois TOON try { result = await response.json(); } catch { const responseText = await response.text(); result = decodeToonData(responseText); if (!result) { throw new Error('Formato de resposta não reconhecido'); } } } if (result.success) { // Processar os dados recebidos let formattedData = []; if (result.data.orders && Array.isArray(result.data.orders)) { formattedData = result.data.orders.map((order: any) => { // Se tem transaction_data, usar diretamente if (order.transaction_data) { return { ...order.transaction_data, order_id: order.order_id, data_format: order.data_format }; } // Se for formato antigo, usar order diretamente return order; }).filter(item => item !== null && item !== undefined); } else { console.warn('Formato de dados inesperado:', result.data); formattedData = []; } if (append) { // Acumular dados existentes setTransactionData(prev => [...prev, ...formattedData]); } else { // Substituir dados setTransactionData(formattedData); } // Atualizar estado de paginação const pagination = result.data.pagination; setCurrentPage(pagination.page); setHasNextPage(pagination.has_next); setTotalCount(pagination.total_count); } else { setError(result.data?.message || 'Erro ao carregar dados'); } } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Erro de conexão ao carregar dados'; setError(errorMessage); console.error('Erro na requisição AJAX:', err); } finally { setLoading(false); setLoadingMore(false); } }; // Função para carregar mais dados const loadMoreData = () => { if (hasNextPage && !loadingMore) { fetchTransactionData(currentPage + 1, true); } }; // Função para aplicar filtros de data const applyDateFilters = () => { setCurrentPage(1); setTransactionData([]); fetchTransactionData(1, false); }; // Função para limpar filtros de data const clearDateFilters = () => { const today = new Date(); const todayFormatted = formatDateForInput(today); setStartDate(todayFormatted); setEndDate(todayFormatted); setActiveFilter('hoje'); setCurrentPage(1); setTransactionData([]); fetchTransactionData(1, false, todayFormatted, todayFormatted); }; // Funções para filtros rápidos de data const formatDateForInput = (date: Date) => { return date.toISOString().split('T')[0]; }; const setDateFilter = (filterType: string) => { const today = new Date(); let startDateValue = ''; let endDateValue = formatDateForInput(today); switch (filterType) { case 'hoje': startDateValue = formatDateForInput(today); break; case '7dias': const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(today.getDate() - 7); startDateValue = formatDateForInput(sevenDaysAgo); break; case '30dias': const thirtyDaysAgo = new Date(); thirtyDaysAgo.setDate(today.getDate() - 30); startDateValue = formatDateForInput(thirtyDaysAgo); break; default: return; } // Atualizar os estados setStartDate(startDateValue); setEndDate(endDateValue); setActiveFilter(filterType); setCurrentPage(1); setTransactionData([]); // Aplicar filtro automaticamente passando as datas calculadas diretamente setTimeout(() => { fetchTransactionData(1, false, startDateValue, endDateValue); }, 0); }; // Função para detectar quando as datas são alteradas manualmente const handleDateChange = (type: 'start' | 'end', value: string) => { if (type === 'start') { setStartDate(value); } else { setEndDate(value); } // Mudar para personalizado quando as datas são alteradas manualmente if (activeFilter !== 'personalizado') { setActiveFilter('personalizado'); } }; // Função para exportar dados em CSV const exportToCSV = () => { if (transactionData.length === 0) { alert(__('No data to export', 'woo-rede')); return; } // Cabeçalhos das colunas baseados na configuração (apenas colunas visíveis) const visibleColumnsConfig = columnConfig.filter(col => col.visible); const headers = visibleColumnsConfig.map(col => col.name); // Converter dados para CSV baseado na configuração de colunas const csvContent = [headers, ...transactionData.map(transaction => visibleColumnsConfig.map(colConfig => { let value = ''; // Extrair valor baseado no ID da coluna switch (colConfig.id) { case 'gateway': value = transaction.gateway?.masked || 'N/A'; break; case 'cvv_sent': value = transaction.transaction?.cvv_sent || 'N/A'; break; case 'type': value = transaction.gateway?.type || 'N/A'; break; case 'installments': value = getValueOrDefault(transaction.transaction?.installments); break; case 'installment_amount': value = getValueOrDefault(transaction.transaction?.installment_amount); break; case 'brand': value = transaction.gateway?.brand || 'N/A'; break; case 'expiry': value = transaction.gateway?.expiry || 'N/A'; break; case 'datetime': value = transaction.system?.request_datetime || 'N/A'; break; case 'total': value = getValueOrDefault(transaction.amounts?.total); break; case 'subtotal': value = getValueOrDefault(transaction.amounts?.subtotal); break; case 'shipping': value = getValueOrDefault(transaction.amounts?.shipping); break; case 'interest_discount': value = getValueOrDefault(transaction.amounts?.interest_discount); break; case 'currency': value = transaction.amounts?.currency || 'N/A'; break; case 'capture': value = transaction.transaction?.capture || 'N/A'; break; case 'recurrent': value = transaction.transaction?.recurrent || 'N/A'; break; case 'auth_3ds': value = transaction.transaction?.['3ds_auth'] || 'N/A'; break; case 'tid': value = transaction.transaction?.tid || 'N/A'; break; case 'environment': value = transaction.system?.environment || 'N/A'; break; case 'payment_gateway': value = transaction.system?.gateway || 'N/A'; break; case 'order_id': value = transaction.system?.order_id || 'N/A'; break; case 'reference': value = transaction.system?.reference || 'N/A'; break; case 'pv': value = transaction.credentials?.pv_masked || 'N/A'; break; case 'token': value = transaction.credentials?.token_masked || 'N/A'; break; case 'return_code': value = transaction.response?.return_code || 'N/A'; break; case 'http_status': value = transaction.response?.http_status || 'N/A'; break; case 'holder_name': value = transaction.gateway?.holder_name || 'N/A'; break; default: value = 'N/A'; } // Escapar aspas e envolver em aspas se contém vírgula return `"${String(value).replace(/"/g, '""')}"`; }) )] .map(row => row.join(',')) .join('\n'); // Download do arquivo const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', `rede-transacoes-${new Date().toISOString().split('T')[0]}.csv`); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); }; // Função para exportar dados em XLS (Excel) const exportToXLS = () => { if (transactionData.length === 0) { alert(__('No data to export', 'woo-rede')); return; } // Cabeçalhos das colunas baseados na configuração (apenas colunas visíveis) const visibleColumnsConfig = columnConfig.filter(col => col.visible); const headers = visibleColumnsConfig.map(col => col.name); // Gerar HTML table que o Excel pode interpretar let xlsContent = ''; // Cabeçalho xlsContent += ''; headers.forEach(header => { xlsContent += ``; }); xlsContent += ''; // Dados baseados na configuração de colunas transactionData.forEach(transaction => { xlsContent += ''; visibleColumnsConfig.forEach(colConfig => { let value = ''; // Extrair valor baseado no ID da coluna (mesmo switch do CSV) switch (colConfig.id) { case 'gateway': value = transaction.gateway?.masked || 'N/A'; break; case 'cvv_sent': value = transaction.transaction?.cvv_sent || 'N/A'; break; case 'type': value = transaction.gateway?.type || 'N/A'; break; case 'installments': value = getValueOrDefault(transaction.transaction?.installments); break; case 'installment_amount': value = getValueOrDefault(transaction.transaction?.installment_amount); break; case 'brand': value = transaction.gateway?.brand || 'N/A'; break; case 'expiry': value = transaction.gateway?.expiry || 'N/A'; break; case 'datetime': value = transaction.system?.request_datetime || 'N/A'; break; case 'total': value = getValueOrDefault(transaction.amounts?.total); break; case 'subtotal': value = getValueOrDefault(transaction.amounts?.subtotal); break; case 'shipping': value = getValueOrDefault(transaction.amounts?.shipping); break; case 'interest_discount': value = getValueOrDefault(transaction.amounts?.interest_discount); break; case 'currency': value = transaction.amounts?.currency || 'N/A'; break; case 'capture': value = transaction.transaction?.capture || 'N/A'; break; case 'recurrent': value = transaction.transaction?.recurrent || 'N/A'; break; case 'auth_3ds': value = transaction.transaction?.['3ds_auth'] || 'N/A'; break; case 'tid': value = transaction.transaction?.tid || 'N/A'; break; case 'environment': value = transaction.system?.environment || 'N/A'; break; case 'payment_gateway': value = transaction.system?.gateway || 'N/A'; break; case 'order_id': value = transaction.system?.order_id || 'N/A'; break; case 'reference': value = transaction.system?.reference || 'N/A'; break; case 'pv': value = transaction.credentials?.pv_masked || 'N/A'; break; case 'token': value = transaction.credentials?.token_masked || 'N/A'; break; case 'return_code': value = transaction.response?.return_code || 'N/A'; break; case 'http_status': value = transaction.response?.http_status || 'N/A'; break; case 'holder_name': value = transaction.gateway?.holder_name || 'N/A'; break; default: value = 'N/A'; } xlsContent += ``; }); xlsContent += ''; }); xlsContent += '
${escapeHtml(header)}
${escapeHtml(String(value))}
'; // Download do arquivo const blob = new Blob([xlsContent], { type: 'application/vnd.ms-excel;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', `rede-transacoes-${new Date().toISOString().split('T')[0]}.xls`); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); }; // Função auxiliar para escapar caracteres HTML const escapeHtml = (text: any): string => { if (text === null || text === undefined) return ''; return String(text) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }; // Função auxiliar para verificar se um valor é zero ou se deve ser tratado como N/A const getValueOrDefault = (value: any, defaultValue: string = 'N/A'): any => { // Se o valor é exatamente 0 (number ou string), retornar '0' if (value === 0 || value === '0') { return '0'; } // Se o valor existe e não é null/undefined/empty string, retornar o valor if (value !== null && value !== undefined && value !== '') { return value; } // Caso contrário, retornar o valor padrão return defaultValue; }; // Função para gerar dados da linha baseados na configuração de colunas const generateRowData = (transaction: any) => { // Se transaction já é um array (formato antigo), usar diretamente if (Array.isArray(transaction)) { // Mapear array antigo para nova estrutura baseada na configuração const originalData = [ transaction[0] || 'N/A', // Cartão transaction[1] || 'N/A', // CVV Enviado transaction[2] || 'N/A', // Tipo transaction[3] || 'N/A', // Parcelas transaction[4] || 'N/A', // Vlr. Parcela transaction[5] || 'N/A', // Bandeira transaction[6] || 'N/A', // Vencimento transaction[7] || 'N/A', // Data/Hora transaction[8] || 'N/A', // Total transaction[9] || 'N/A', // Subtotal transaction[10] || 'N/A', // Frete transaction[11] || 'N/A', // Juros/Desc. transaction[12] || 'N/A', // Moeda transaction[13] || 'N/A', // Captura transaction[14] || 'N/A', // Recorrente transaction[15] || 'N/A', // 3DS Auth transaction[16] || 'N/A', // TID transaction[17] || 'N/A', // Ambiente transaction[18] || 'N/A', // Gateway transaction[19] || 'N/A', // Order ID transaction[20] || 'N/A', // Reference transaction[21] || 'N/A', // PV transaction[22] || 'N/A', // Token transaction[23] || 'N/A', // Return Code transaction[24] || 'N/A', // HTTP Status transaction[25] || 'N/A', // Portador transaction[26] || html( ` ${escapeHtml(__('Support', 'woo-rede'))} ` ) // Suporte ]; const rowData: any[] = []; const defaultColumns = DEFAULT_COLUMNS; columnConfig.forEach(column => { if (!column.visible) return; const columnIndex = defaultColumns.findIndex(col => col.id === column.id); if (columnIndex >= 0 && columnIndex < originalData.length) { rowData.push(originalData[columnIndex]); } else { rowData.push('N/A'); } }); return rowData; } // Se transaction é objeto (nova estrutura), usar a lógica anterior const rowData: any[] = []; columnConfig.forEach(column => { if (!column.visible) return; let value: any = ''; switch (column.id) { case 'gateway': value = (transaction && transaction.gateway && transaction.gateway.masked) ? transaction.gateway.masked : 'N/A'; break; case 'cvv_sent': value = (transaction && transaction.transaction && transaction.transaction.cvv_sent) ? transaction.transaction.cvv_sent : 'N/A'; break; case 'type': value = (transaction && transaction.gateway && transaction.gateway.type) ? transaction.gateway.type : 'N/A'; break; case 'installments': value = getValueOrDefault((transaction && transaction.transaction && transaction.transaction.installments !== undefined) ? transaction.transaction.installments : null); break; case 'installment_amount': value = getValueOrDefault((transaction && transaction.transaction && transaction.transaction.installment_amount !== undefined) ? transaction.transaction.installment_amount : null); break; case 'brand': value = (transaction && transaction.gateway && transaction.gateway.brand) ? transaction.gateway.brand : 'N/A'; break; case 'expiry': value = (transaction && transaction.gateway && transaction.gateway.expiry) ? transaction.gateway.expiry : 'N/A'; break; case 'datetime': value = (transaction && transaction.system && transaction.system.request_datetime) ? transaction.system.request_datetime : 'N/A'; break; case 'total': value = getValueOrDefault(transaction.amounts?.total); break; case 'subtotal': value = getValueOrDefault(transaction.amounts?.subtotal); break; case 'shipping': value = getValueOrDefault(transaction.amounts?.shipping); break; case 'interest_discount': value = getValueOrDefault(transaction.amounts?.interest_discount); break; case 'currency': value = transaction.amounts?.currency || 'N/A'; break; case 'capture': value = (transaction && transaction.transaction && transaction.transaction.capture) ? transaction.transaction.capture : 'N/A'; break; case 'recurrent': value = (transaction && transaction.transaction && transaction.transaction.recurrent) ? transaction.transaction.recurrent : 'N/A'; break; case 'auth_3ds': value = (transaction && transaction.transaction && transaction.transaction['3ds_auth']) ? transaction.transaction['3ds_auth'] : 'N/A'; break; case 'tid': value = (transaction && transaction.transaction && transaction.transaction.tid) ? transaction.transaction.tid : 'N/A'; break; case 'environment': value = (transaction && transaction.system && transaction.system.environment) ? transaction.system.environment : 'N/A'; break; case 'payment_gateway': value = (transaction && transaction.system && transaction.system.gateway) ? transaction.system.gateway : 'N/A'; break; case 'order_id': if (transaction && transaction.system && transaction.system.order_id && transaction.system.order_id !== 'N/A') { const orderId = transaction.system.order_id; const siteDomain = analyticsData.site_domain || ''; const orderUrl = `${siteDomain}/wp-admin/admin.php?page=wc-orders&action=edit&id=${orderId}`; value = html( ` ${escapeHtml(orderId)} ` ); } else { value = 'N/A'; } break; case 'reference': value = (transaction && transaction.system && transaction.system.reference) ? transaction.system.reference : 'N/A'; break; case 'pv': value = (transaction && transaction.credentials && transaction.credentials.pv_masked) ? transaction.credentials.pv_masked : 'N/A'; break; case 'token': value = (transaction && transaction.credentials && transaction.credentials.token_masked) ? transaction.credentials.token_masked : 'N/A'; break; case 'return_code': value = (transaction && transaction.response && transaction.response.return_code) ? transaction.response.return_code : 'N/A'; break; case 'http_status': value = (transaction && transaction.response && transaction.response.http_status) ? transaction.response.http_status : 'N/A'; break; case 'holder_name': value = (transaction && transaction.gateway && transaction.gateway.holder_name) ? transaction.gateway.holder_name : 'N/A'; break; case 'whatsapp': value = html( ` ${escapeHtml(__('Support', 'woo-rede'))} ` ); break; default: value = 'N/A'; } rowData.push(value); }); return rowData; }; // Carregar configuração de colunas ao montar o componente useEffect(() => { setColumnConfig(loadColumnConfig()); // Garantir que o container de configuração sempre comece fechado setShowColumnConfig(false); }, []); // Buscar dados quando o componente for montado e aplicar filtro "hoje" por padrão useEffect(() => { setDateFilter('hoje'); }, []); // Configurar e renderizar o Grid quando os dados estiverem prontos useEffect(() => { if (gridRef.current && !loading) { // Gerar colunas baseadas na configuração const visibleColumns = columnConfig .filter(col => col.visible) .map(col => ({ name: col.name, resizable: true, sort: true, formatter: (cell: any, row: any, column: any) => { // Para colunas especiais, gerar HTML com tooltips if (col.id === 'brand' && cell !== 'N/A') { return html(generateBrandTooltipHTML(cell)); } if ((col.id === 'return_code' || col.id === 'http_status') && cell !== 'N/A') { const label = col.id === 'return_code' ? 'Return Code' : 'HTTP Status'; return html(generateCodeTooltipHTML(cell, label)); } return cell; } })); // Gerar dados das linhas baseados na configuração const tableData = transactionData.map(transaction => generateRowData(transaction)); // Configuração do Grid.js const grid = new Grid({ columns: visibleColumns, data: tableData, search: true, sort: true, pagination: { limit: perPageLimit } as any, className: { table: 'rede-transactions-table', header: 'rede-table-header', tbody: 'rede-table-body' }, style: { table: { 'white-space': 'nowrap' } }, language: { search: { placeholder: __('Search transactions...', 'woo-rede') }, pagination: { previous: __('Previous', 'woo-rede'), next: __('Next', 'woo-rede'), navigate: (page: number, pages: number) => `${__('Page', 'woo-rede')} ${page} ${__('of', 'woo-rede')} ${pages}`, page: (page: number) => `${__('Page', 'woo-rede')} ${page}`, showing: __('Showing', 'woo-rede'), of: __('of', 'woo-rede'), to: __('to', 'woo-rede'), results: () => __('records', 'woo-rede') }, loading: __('Loading...', 'woo-rede'), noRecordsFound: __('No transactions found', 'woo-rede'), error: __('An error occurred while loading data', 'woo-rede') } }); // Renderizar o grid grid.render(gridRef.current); // Adicionar event listeners para tooltips após renderização const setupTooltips = () => { const tooltipManager = TooltipManager.getInstance(); const tooltipTriggers = gridRef.current?.querySelectorAll('.tooltip-trigger'); tooltipTriggers?.forEach(trigger => { const element = trigger as HTMLElement; const tooltipText = element.getAttribute('data-tooltip'); if (tooltipText) { element.addEventListener('mouseenter', () => { tooltipManager.showTooltip(element, tooltipText); }); element.addEventListener('mouseleave', () => { tooltipManager.hideTooltip(); }); } }); }; // Configurar tooltips após renderização inicial e após mudanças de página setTimeout(setupTooltips, 0); // Re-configurar tooltips quando a paginação mudar const observer = new MutationObserver(() => { setTimeout(setupTooltips, 0); }); if (gridRef.current) { observer.observe(gridRef.current, { childList: true, subtree: true }); } // Cleanup return () => { if (grid) { grid.destroy(); } observer.disconnect(); }; } }, [transactionData, loading, perPageLimit, columnConfig]); // Dependências: transactionData, loading, perPageLimit e columnConfig // Verificar se a licença está inativa para mostrar apenas o screenshot const analyticsData = (window as any).lknRedeAnalytics || {}; if (analyticsData.plugin_license === 'inactive') { return (
{/* Screenshot da funcionalidade como link */} {__('Click
); } return (
{/* Interface de Configuração de Colunas - Fora do container principal */} {showColumnConfig === true && (

{__('Column Configuration', 'woo-rede')}

{columnConfig.map((column, index) => (
handleDragStart(e, index)} onDragOver={(e) => handleDragOver(e, index)} onDragLeave={handleDragLeave} onDrop={(e) => handleDrop(e, index)} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 12px', backgroundColor: column.visible ? '#ffffff' : '#f1f3f4', border: dragOverItem === index ? '2px dashed #0073aa' : '1px solid #e0e0e0', borderRadius: '6px', cursor: 'grab', opacity: draggedItem === index ? 0.5 : 1, transition: 'all 0.2s ease', fontSize: '13px', boxShadow: column.visible ? '0 1px 3px rgba(0,0,0,0.1)' : 'none' }} >
{index + 1}.
))}
{__('💡 Tips:', 'woo-rede')}
• {__('Check/uncheck boxes to show/hide columns', 'woo-rede')}
• {__('Use ↑↓ or drag cards to reorder columns', 'woo-rede')}
• {__('Settings are saved automatically', 'woo-rede')}
)} {/* Tabela de Transações */}

{__('Rede Transactions', 'woo-rede')}

{/* Seção de Configuração de Transações */}
{/* Título das últimas transações */}

{__('Recent transactions:', 'woo-rede')}

{/* Limite de consultas por sessão */}
setQueryLimit(Math.max(1, parseInt(e.target.value) || 1))} min="1" max="1000" style={{ padding: '5px', border: '1px solid #ddd', borderRadius: '4px', width: '80px' }} />
{/* Query Dates */}

{__('Query Dates:', 'woo-rede')}

{ e.preventDefault(); applyDateFilters(); }} style={{ display: 'flex', gap: '10px', alignItems: 'center', flexWrap: 'wrap' }}>
handleDateChange('start', e.target.value)} style={{ padding: '5px', border: '1px solid #ddd', borderRadius: '4px' }} />
handleDateChange('end', e.target.value)} style={{ padding: '5px', border: '1px solid #ddd', borderRadius: '4px' }} />
{/* Linha divisória */}
{/* Controles de filtro e paginação em linha */}
{/* Botões de filtro rápido */}
{/* Transações por página */}
setPerPageLimit(Math.max(1, parseInt(e.target.value) || 1))} min="1" max="100" style={{ padding: '5px', border: '1px solid #ddd', borderRadius: '4px', width: '80px' }} />
{loading && (

{__('Loading transactions...', 'woo-rede')}

)} {error && (

{__('Erro:', 'woo-rede')} {error}

)} {!loading && !error && ( <> {/* Informações de paginação */}
{__('Showing', 'woo-rede')} {transactionData.length} {__('of total', 'woo-rede')} {totalCount} {__('transactions', 'woo-rede')} {currentPage > 1 && ( ({__('Page', 'woo-rede')} {currentPage}) )}
{/* Botão carregar mais */} {hasNextPage && (
)} )}
); }; // Registra a página nos filtros do WooCommerce Admin function initRedeAnalytics() { // Registra a página nos relatórios do WooCommerce Admin addFilter( 'woocommerce_admin_reports_list', 'rede-transactions', (reports) => [ ...reports, { report: 'rede-transactions', title: __('Rede Transactions', 'woo-rede'), component: RedeAnalyticsPage } ] ); // Registra a página no sistema de roteamento do WooCommerce Admin addFilter( 'woocommerce_admin_pages', 'rede-transactions', (pages) => [ ...pages, { container: RedeAnalyticsPage, path: '/analytics/rede-transactions', wpOpenMenu: 'toplevel_page_woocommerce', capability: 'view_woocommerce_reports', navArgs: { id: 'woocommerce-analytics-rede-transactions' } } ] ); } // Inicializa a extensão if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initRedeAnalytics); } else { initRedeAnalytics(); } export default RedeAnalyticsPage;