diff --git a/admin/assets/dashboard.css b/admin/assets/dashboard.css index bdd2aeb..ed0b573 100644 --- a/admin/assets/dashboard.css +++ b/admin/assets/dashboard.css @@ -89,6 +89,46 @@ body .card { display: none; } +/* ================================================================= + Table Loading State (overlay sin cambiar tamaño) + ================================================================= */ + +.dashboard-analytics-wrap .card.is-loading-table { + position: relative; + pointer-events: none; +} + +.dashboard-analytics-wrap .card.is-loading-table::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.7); + z-index: 10; +} + +.dashboard-analytics-wrap .card.is-loading-table::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 24px; + height: 24px; + margin: -12px 0 0 -12px; + border: 3px solid #f3f3f3; + border-top: 3px solid #FF8600; + border-radius: 50%; + z-index: 11; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + /* ================================================================= KPI Cards - Sin efecto hover ================================================================= */ @@ -123,6 +163,55 @@ body .card { border-color: #FF8600; } +/* ================================================================= + Pagination - Estilo igual al sitio principal + ================================================================= */ + +.dashboard-analytics-wrap .pagination { + gap: 0.25rem; + flex-wrap: wrap; +} + +.dashboard-analytics-wrap .pagination .page-item .page-link { + padding: 8px 16px; + margin: 0 4px; + border-radius: 6px; + border: 1px solid #e5e7eb; + font-size: 1rem; + min-height: 42px; + display: flex; + align-items: center; + justify-content: center; + color: #374151; + background-color: #fff; + text-decoration: none; + transition: all 0.2s ease; +} + +.dashboard-analytics-wrap .pagination .page-item .page-link:hover { + background-color: #f3f4f6; + border-color: #d1d5db; + color: #111827; +} + +.dashboard-analytics-wrap .pagination .page-item.active .page-link { + background-color: #FF8600; + border-color: #FF8600; + color: #fff; +} + +.dashboard-analytics-wrap .pagination .page-item.disabled .page-link { + background-color: #f9fafb; + border-color: #e5e7eb; + color: #9ca3af; + cursor: not-allowed; +} + +/* Botones de navegación (flechas) */ +.dashboard-analytics-wrap .pagination .page-item .page-link i { + font-size: 0.875rem; +} + /* ================================================================= Responsive ================================================================= */ @@ -135,4 +224,11 @@ body .card { .dashboard-analytics-wrap .card-body { padding: 0.75rem; } + + .dashboard-analytics-wrap .pagination .page-item .page-link { + padding: 6px 12px; + font-size: 0.875rem; + min-height: 36px; + margin: 0 2px; + } } diff --git a/admin/assets/dashboard.js b/admin/assets/dashboard.js index 11efe0f..e666d44 100644 --- a/admin/assets/dashboard.js +++ b/admin/assets/dashboard.js @@ -8,226 +8,307 @@ (function() { 'use strict'; + // State + var currentPages = { + 'top-searches': 1, + 'top-clicks': 1, + 'zero-results': 1 + }; + /** * Initialize Bootstrap tooltips */ function initTooltips() { - const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); + var tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); if (tooltipTriggerList.length > 0 && typeof bootstrap !== 'undefined') { - [...tooltipTriggerList].map(el => new bootstrap.Tooltip(el)); - } - } - - /** - * Copy markdown to clipboard - * Implementation in FASE 9 - */ - function copyMarkdown() { - const markdown = generateMarkdown(); - - navigator.clipboard.writeText(markdown).then(function() { - showSuccessMessage('Copiado: El resumen está en tu portapapeles.'); - }).catch(function(err) { - console.error('Error al copiar:', err); - showErrorMessage('Error al copiar al portapapeles.'); - }); - } - - /** - * Generate comprehensive markdown report from dashboard data (v2) - */ - function generateMarkdown() { - const data = window.roiApuDashboardData; - if (!data) { - return '# Error: No hay datos disponibles'; - } - - let md = ''; - - // Header - md += '# Reporte Analytics - Buscador APUs\n\n'; - md += `**Período**: Últimos ${data.period} días\n`; - md += `**Generado**: ${data.generated}\n`; - md += `**Sitio**: ${data.siteUrl}\n\n`; - - // KPIs Table - md += '## Métricas Clave\n\n'; - md += '| Métrica | Valor |\n'; - md += '|---------|-------|\n'; - md += `| Búsquedas totales | ${data.kpis.totalBusquedas} |\n`; - md += `| CTR (Click-Through Rate) | ${data.kpis.ctr} |\n`; - md += `| Búsquedas sin resultados | ${data.kpis.sinResultados} |\n`; - md += `| Posición promedio clicks | ${data.kpis.posProm} |\n\n`; - - // Click Distribution - if (data.clickDistribution && data.clickDistribution.length > 0) { - md += '## Distribución de Clicks por Posición\n\n'; - md += '| Posición | Clicks | Porcentaje |\n'; - md += '|----------|--------|------------|\n'; - data.clickDistribution.forEach(function(dist) { - md += `| ${dist.posicion} | ${dist.clicks} | ${dist.porcentaje}% |\n`; + Array.prototype.slice.call(tooltipTriggerList).forEach(function(el) { + new bootstrap.Tooltip(el); }); - md += '\n'; } - - md += '---\n\n'; - md += '## RECOMENDACIONES ACCIONABLES\n\n'; - - // 🔴 Urgent: Zero Results - if (data.zeroResults && data.zeroResults.length > 0) { - md += '### 🔴 ACCIÓN URGENTE: Contenido a Crear\n\n'; - md += 'Los usuarios buscan esto pero NO encuentran resultados:\n\n'; - md += '| Término | Frecuencia |\n'; - md += '|---------|------------|\n'; - data.zeroResults.forEach(function(term) { - md += `| ${term.term} | ${term.frecuencia} veces |\n`; - }); - md += '\n**Acción**: Crear APUs o contenido para estos términos.\n\n'; - } - - // 🟡 CTR 0% Section - if (data.ctrZero && data.ctrZero.length > 0) { - md += '### 🟡 REVISAR: Títulos con CTR 0%\n\n'; - md += 'Estos términos tienen resultados pero nadie hace click:\n\n'; - md += '| Término | Búsquedas | Resultados |\n'; - md += '|---------|-----------|------------|\n'; - data.ctrZero.forEach(function(term) { - md += `| ${term.term} | ${term.busquedas} | ${term.resultados} |\n`; - }); - md += '\n**Acción**: Mejorar títulos y descripciones de los APUs mostrados.\n\n'; - } - - // 🎯 Quick Wins - if (data.quickWins && data.quickWins.length > 0) { - md += '### 🎯 QUICK WINS: Oportunidades Fáciles\n\n'; - md += 'Términos en posición 4-10 con buen CTR (una pequeña mejora = top 3):\n\n'; - md += '| Término | Búsquedas | CTR | Pos. Actual |\n'; - md += '|---------|-----------|-----|-------------|\n'; - data.quickWins.forEach(function(term) { - md += `| ${term.term} | ${term.busquedas} | ${term.ctr}% | ${term.posProm} |\n`; - }); - md += '\n**Acción**: Optimizar estos APUs para subir al top 3.\n\n'; - } - - // 📉 Decay Content - if (data.decayContent && data.decayContent.length > 0) { - md += '### 📉 ATENCIÓN: Contenido en Decadencia\n\n'; - md += 'Posts que perdieron >20% clicks vs período anterior:\n\n'; - md += '| Título | Cambio | Clicks (antes → ahora) |\n'; - md += '|--------|--------|------------------------|\n'; - data.decayContent.forEach(function(post) { - md += `| [${post.title}](${post.url}) | ${post.cambioPct}% | ${post.clicksAnterior} → ${post.clicksActual} |\n`; - }); - md += '\n**Acción**: Revisar si el contenido está desactualizado.\n\n'; - } - - // 🟢 Star Content - if (data.contenidoEstrella && data.contenidoEstrella.length > 0) { - md += '### 🟢 MANTENER: Tu Contenido Estrella\n\n'; - md += 'Posts con más clicks - mantén este contenido actualizado:\n\n'; - md += '| Título | Clicks | Pos. Prom. |\n'; - md += '|--------|--------|------------|\n'; - data.contenidoEstrella.forEach(function(post) { - md += `| [${post.title}](${post.url}) | ${post.clicks} | ${post.posProm} |\n`; - }); - md += '\n**Acción**: Mantener actualizado y considerar contenido relacionado.\n\n'; - } - - // Infraposicionados Section - if (data.infraposicionados && data.infraposicionados.length > 0) { - md += '### 🔼 OPORTUNIDAD: Posts Infraposicionados\n\n'; - md += 'Estos posts reciben clicks pero aparecen muy abajo:\n\n'; - md += '| Título | Clicks | Pos. Prom. |\n'; - md += '|--------|--------|------------|\n'; - data.infraposicionados.forEach(function(post) { - md += `| ${post.title} | ${post.clicks} | ${post.posProm} |\n`; - }); - md += '\n**Acción**: Mejorar scoring o relevancia de estos APUs.\n\n'; - } - - md += '---\n\n'; - - // Summary stats - md += '## Resumen de Datos\n\n'; - md += `- **Términos únicos buscados**: ${data.totals.searches}\n`; - md += `- **Posts con clicks**: ${data.totals.clicks}\n`; - md += `- **Términos sin resultados**: ${data.totals.zeroResults}\n\n`; - - // Questions for analysis - md += '## Preguntas para Análisis con IA\n\n'; - md += '1. ¿Qué contenido debería crear primero basándome en las búsquedas sin resultados?\n'; - md += '2. ¿Por qué algunos términos tienen resultados pero CTR 0%? ¿Qué puedo mejorar?\n'; - md += '3. ¿Cómo optimizo los Quick Wins para llegar al top 3?\n'; - md += '4. ¿Qué patrones veo en mi contenido estrella que debería replicar?\n'; - md += '5. ¿Hay contenido en decadencia que debería actualizar urgentemente?\n\n'; - - // Footer - md += '---\n'; - md += '*Generado por ROI APU Search Dashboard v2*\n'; - md += '*Comparte este reporte con Claude para obtener recomendaciones detalladas.*\n'; - - return md; } /** * Show success message */ function showSuccessMessage(message) { - const container = document.getElementById('alertContainer'); + var container = document.getElementById('alertContainer'); if (!container) return; - - container.innerHTML = ` - - `; - - // Auto-hide after 3 seconds - setTimeout(function() { - container.innerHTML = ''; - }, 3000); + container.innerHTML = '
' + message + '
'; + setTimeout(function() { container.innerHTML = ''; }, 3000); } /** * Show error message */ function showErrorMessage(message) { - const container = document.getElementById('alertContainer'); + var container = document.getElementById('alertContainer'); if (!container) return; - - container.innerHTML = ` - - `; + container.innerHTML = '
' + message + '
'; } /** - * Initialize dashboard + * Escape HTML + */ + function escapeHtml(text) { + if (!text) return ''; + var div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + /** + * Format number + */ + function formatNumber(num) { + return parseInt(num, 10).toLocaleString('es-ES'); + } + + /** + * Load page via AJAX + */ + function loadPage(tableId, page) { + var config = { + 'top-searches': { + action: 'roi_apu_paginate_searches', + bodyId: 'top-searches-body', + paginationId: 'pagination-searches' + }, + 'top-clicks': { + action: 'roi_apu_paginate_clicks', + bodyId: 'top-clicks-body', + paginationId: 'pagination-clicks' + }, + 'zero-results': { + action: 'roi_apu_paginate_zero_results', + bodyId: 'zero-results-body', + paginationId: 'pagination-zero-results' + } + }; + + var cfg = config[tableId]; + if (!cfg) { + console.error('Invalid tableId:', tableId); + return; + } + + var tbody = document.getElementById(cfg.bodyId); + var paginationDiv = document.getElementById(cfg.paginationId); + + if (!tbody) { + console.error('tbody not found:', cfg.bodyId); + return; + } + + // Find the card container and add loading state (keeps size, shows overlay) + var card = tbody.closest('.card'); + if (card) { + card.classList.add('is-loading-table'); + } + + // Build form data + var formData = new FormData(); + formData.append('action', cfg.action); + formData.append('nonce', window.roiApuDashboardAjax.nonce); + formData.append('page', page); + formData.append('days', window.roiApuDashboardData ? window.roiApuDashboardData.period : 30); + + // AJAX request + fetch(window.roiApuDashboardAjax.ajaxUrl, { + method: 'POST', + body: formData, + credentials: 'same-origin' + }) + .then(function(response) { return response.json(); }) + .then(function(result) { + if (result.success && result.data && result.data.data) { + currentPages[tableId] = page; + + // Render rows based on table type + var html = ''; + var siteUrl = window.roiApuDashboardData ? window.roiApuDashboardData.siteUrl : ''; + + result.data.data.forEach(function(row) { + if (tableId === 'top-searches') { + var ctrClass = parseFloat(row.ctr) > 0 ? 'bg-success' : 'bg-secondary'; + html += '' + escapeHtml(row.q_term) + ''; + html += '' + formatNumber(row.busquedas) + ''; + html += '' + formatNumber(row.clicks) + ''; + html += '' + escapeHtml(row.ctr) + '%'; + html += '' + formatNumber(row.resultados) + ''; + } else if (tableId === 'top-clicks') { + var posClass = parseFloat(row.pos_prom) <= 3 ? 'bg-success' : (parseFloat(row.pos_prom) <= 5 ? 'bg-warning text-dark' : 'bg-secondary'); + var title = row.post_title.length > 80 ? row.post_title.substring(0, 77) + '...' : row.post_title; + html += '' + escapeHtml(title) + ''; + html += '' + formatNumber(row.clicks) + ''; + html += '' + escapeHtml(row.pos_prom) + ''; + html += '
'; + html += ''; + html += ''; + html += '
'; + } else if (tableId === 'zero-results') { + var date = new Date(row.ultima_busqueda); + var formattedDate = date.toLocaleDateString('es-ES'); + html += '' + escapeHtml(row.q_term) + ''; + html += '' + formatNumber(row.frecuencia) + ''; + html += '' + formattedDate + ''; + } + }); + + tbody.innerHTML = html; + + // Remove loading state + if (card) { + card.classList.remove('is-loading-table'); + } + + // Update pagination + if (paginationDiv) { + paginationDiv.innerHTML = renderPagination(page, result.data.pages, tableId); + bindPaginationEvents(paginationDiv, tableId); + } + } else { + if (card) { + card.classList.remove('is-loading-table'); + } + showErrorMessage('Error al cargar datos'); + console.error('AJAX error:', result); + } + }) + .catch(function(error) { + if (card) { + card.classList.remove('is-loading-table'); + } + console.error('Fetch error:', error); + showErrorMessage('Error de conexión'); + }); + } + + /** + * Render pagination HTML + */ + function renderPagination(currentPage, totalPages, tableId) { + if (totalPages <= 1) return ''; + + var html = ''; + return html; + } + + /** + * Bind click events to pagination links + */ + function bindPaginationEvents(container, tableId) { + var links = container.querySelectorAll('a[data-page]'); + links.forEach(function(link) { + link.onclick = function(e) { + e.preventDefault(); + e.stopPropagation(); + var page = parseInt(this.getAttribute('data-page'), 10); + if (page && page !== currentPages[tableId]) { + loadPage(tableId, page); + } + return false; + }; + }); + } + + /** + * Initialize all pagination + */ + function initPagination() { + // Bind events to existing pagination containers + var containers = [ + { id: 'pagination-searches', table: 'top-searches' }, + { id: 'pagination-clicks', table: 'top-clicks' }, + { id: 'pagination-zero-results', table: 'zero-results' } + ]; + + containers.forEach(function(cfg) { + var container = document.getElementById(cfg.id); + if (container) { + bindPaginationEvents(container, cfg.table); + } + }); + } + + /** + * Copy markdown to clipboard + */ + function copyMarkdown() { + var data = window.roiApuDashboardData; + if (!data) { + showErrorMessage('No hay datos disponibles'); + return; + } + + var md = '# Reporte Analytics - Buscador APUs\n\n'; + md += '**Período**: Últimos ' + data.period + ' días\n'; + md += '**Generado**: ' + data.generated + '\n\n'; + md += '## Métricas Clave\n\n'; + md += '| Métrica | Valor |\n|---------|-------|\n'; + md += '| Búsquedas | ' + data.kpis.totalBusquedas + ' |\n'; + md += '| CTR | ' + data.kpis.ctr + ' |\n'; + md += '| Sin Resultados | ' + data.kpis.sinResultados + ' |\n'; + md += '| Pos. Promedio | ' + data.kpis.posProm + ' |\n\n'; + md += '*Generado por ROI APU Search Dashboard*\n'; + + navigator.clipboard.writeText(md).then(function() { + showSuccessMessage('Copiado al portapapeles'); + }).catch(function() { + showErrorMessage('Error al copiar'); + }); + } + + /** + * Initialize */ function init() { initTooltips(); + initPagination(); - // Bind export button - const exportBtn = document.getElementById('btn-export-md'); + var exportBtn = document.getElementById('btn-export-md'); if (exportBtn) { - exportBtn.addEventListener('click', copyMarkdown); + exportBtn.onclick = copyMarkdown; } } - // Initialize when DOM is ready + // Run when ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } - // Expose functions globally for debugging + // Global access for debugging window.roiApuDashboard = { - copyMarkdown: copyMarkdown, - initTooltips: initTooltips + loadPage: loadPage, + currentPages: currentPages }; })(); diff --git a/admin/class-analytics-dashboard.php b/admin/class-analytics-dashboard.php index 4ca19a2..f741ea0 100644 --- a/admin/class-analytics-dashboard.php +++ b/admin/class-analytics-dashboard.php @@ -47,6 +47,11 @@ final class ROI_APU_Analytics_Dashboard // Private constructor for singleton } + /** + * Items per page for pagination + */ + private const ITEMS_PER_PAGE = 20; + /** * Initialize the dashboard */ @@ -54,6 +59,167 @@ final class ROI_APU_Analytics_Dashboard { add_action('admin_menu', [$this, 'register_menu']); add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']); + + // AJAX handlers for pagination + add_action('wp_ajax_roi_apu_paginate_searches', [$this, 'ajax_paginate_searches']); + add_action('wp_ajax_roi_apu_paginate_clicks', [$this, 'ajax_paginate_clicks']); + add_action('wp_ajax_roi_apu_paginate_zero_results', [$this, 'ajax_paginate_zero_results']); + } + + /** + * AJAX handler for Top Searches pagination + */ + public function ajax_paginate_searches(): void + { + check_ajax_referer('roi_apu_dashboard', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error(['message' => 'Unauthorized'], 403); + } + + $page = isset($_POST['page']) ? absint($_POST['page']) : 1; + $days = isset($_POST['days']) ? absint($_POST['days']) : 30; + $offset = ($page - 1) * self::ITEMS_PER_PAGE; + + $db = ROI_APU_Search_DB::get_instance(); + $repository = new ROI_APU_Metrics_Repository($db->get_pdo(), $db->get_prefix()); + + $data = $repository->getTopSearches($days, self::ITEMS_PER_PAGE, $offset); + $total = $repository->getTotalCounts($days)['total_searches']; + + wp_send_json_success([ + 'data' => $data, + 'total' => $total, + 'page' => $page, + 'pages' => ceil($total / self::ITEMS_PER_PAGE), + ]); + } + + /** + * AJAX handler for Top Clicks pagination + */ + public function ajax_paginate_clicks(): void + { + check_ajax_referer('roi_apu_dashboard', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error(['message' => 'Unauthorized'], 403); + } + + $page = isset($_POST['page']) ? absint($_POST['page']) : 1; + $days = isset($_POST['days']) ? absint($_POST['days']) : 30; + $offset = ($page - 1) * self::ITEMS_PER_PAGE; + + $db = ROI_APU_Search_DB::get_instance(); + $repository = new ROI_APU_Metrics_Repository($db->get_pdo(), $db->get_prefix()); + + $data = $repository->getTopClicks($days, self::ITEMS_PER_PAGE, $offset); + $total = $repository->getTotalCounts($days)['total_clicks']; + + wp_send_json_success([ + 'data' => $data, + 'total' => $total, + 'page' => $page, + 'pages' => ceil($total / self::ITEMS_PER_PAGE), + ]); + } + + /** + * AJAX handler for Zero Results pagination + */ + public function ajax_paginate_zero_results(): void + { + check_ajax_referer('roi_apu_dashboard', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_send_json_error(['message' => 'Unauthorized'], 403); + } + + $page = isset($_POST['page']) ? absint($_POST['page']) : 1; + $days = isset($_POST['days']) ? absint($_POST['days']) : 30; + $offset = ($page - 1) * self::ITEMS_PER_PAGE; + + $db = ROI_APU_Search_DB::get_instance(); + $repository = new ROI_APU_Metrics_Repository($db->get_pdo(), $db->get_prefix()); + + $data = $repository->getZeroResults($days, self::ITEMS_PER_PAGE, $offset); + $total = $repository->getTotalCounts($days)['total_zero_results']; + + wp_send_json_success([ + 'data' => $data, + 'total' => $total, + 'page' => $page, + 'pages' => ceil($total / self::ITEMS_PER_PAGE), + ]); + } + + /** + * Render pagination controls + * + * @param int $currentPage Current page number + * @param int $totalPages Total number of pages + * @param string $tableId ID of the table for JS targeting + * @return string HTML for pagination controls + */ + private function render_pagination(int $currentPage, int $totalPages, string $tableId): string + { + if ($totalPages <= 1) { + return ''; + } + + $html = ''; + + return $html; } /** @@ -117,6 +283,12 @@ final class ROI_APU_Analytics_Dashboard ROI_APU_SEARCH_VERSION, true ); + + // Localize script for AJAX pagination + wp_localize_script('roi-apu-dashboard', 'roiApuDashboardAjax', [ + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('roi_apu_dashboard'), + ]); } /** @@ -708,12 +880,12 @@ final class ROI_APU_Analytics_Dashboard - 20) : ?> - @@ -766,12 +938,12 @@ final class ROI_APU_Analytics_Dashboard - 20) : ?> - @@ -812,12 +984,12 @@ final class ROI_APU_Analytics_Dashboard - 20) : ?> -