feat(dashboard): reemplazar Load More por paginación con overlay de carga
- Añadir AJAX handlers para paginación en las 3 tablas (búsquedas, clicks, sin resultados) - Implementar controles de paginación estilo sitio principal (Inicio, números, Ver más, Fin) - Añadir overlay de carga que mantiene el tamaño del card (sin saltos visuales) - Estilos de paginación: botones con padding 8px 16px, border-radius 6px, activo naranja #FF8600 - Spinner CSS puro centrado durante la carga - Deshabilitar pointer-events mientras carga para evitar doble clic 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.');
|
||||
Array.prototype.slice.call(tooltipTriggerList).forEach(function(el) {
|
||||
new bootstrap.Tooltip(el);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`;
|
||||
});
|
||||
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 = `
|
||||
<div class="admin-notice notice-success mb-3" role="alert">
|
||||
<i class="bi bi-check-circle me-2"></i>
|
||||
<strong>${message}</strong>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Auto-hide after 3 seconds
|
||||
setTimeout(function() {
|
||||
container.innerHTML = '';
|
||||
}, 3000);
|
||||
container.innerHTML = '<div class="admin-notice notice-success mb-3"><i class="bi bi-check-circle me-2"></i><strong>' + message + '</strong></div>';
|
||||
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 = `
|
||||
<div class="admin-notice notice-error mb-3" role="alert">
|
||||
<i class="bi bi-exclamation-circle me-2"></i>
|
||||
<strong>${message}</strong>
|
||||
</div>
|
||||
`;
|
||||
container.innerHTML = '<div class="admin-notice notice-error mb-3"><i class="bi bi-exclamation-circle me-2"></i><strong>' + message + '</strong></div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 += '<tr><td><strong>' + escapeHtml(row.q_term) + '</strong></td>';
|
||||
html += '<td class="text-center">' + formatNumber(row.busquedas) + '</td>';
|
||||
html += '<td class="text-center">' + formatNumber(row.clicks) + '</td>';
|
||||
html += '<td class="text-center"><span class="badge ' + ctrClass + '">' + escapeHtml(row.ctr) + '%</span></td>';
|
||||
html += '<td class="text-center">' + formatNumber(row.resultados) + '</td></tr>';
|
||||
} 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 += '<tr><td><strong>' + escapeHtml(title) + '</strong></td>';
|
||||
html += '<td class="text-center">' + formatNumber(row.clicks) + '</td>';
|
||||
html += '<td class="text-center"><span class="badge ' + posClass + '">' + escapeHtml(row.pos_prom) + '</span></td>';
|
||||
html += '<td class="text-center"><div class="btn-group btn-group-sm">';
|
||||
html += '<a href="/wp-admin/post.php?post=' + row.post_id + '&action=edit" target="_blank" class="btn btn-outline-primary"><i class="bi bi-pencil"></i></a>';
|
||||
html += '<a href="' + siteUrl + '/' + row.post_name + '/" target="_blank" class="btn btn-outline-secondary"><i class="bi bi-box-arrow-up-right"></i></a>';
|
||||
html += '</div></td></tr>';
|
||||
} else if (tableId === 'zero-results') {
|
||||
var date = new Date(row.ultima_busqueda);
|
||||
var formattedDate = date.toLocaleDateString('es-ES');
|
||||
html += '<tr><td><strong>' + escapeHtml(row.q_term) + '</strong></td>';
|
||||
html += '<td class="text-center"><span class="badge bg-danger">' + formatNumber(row.frecuencia) + '</span></td>';
|
||||
html += '<td class="text-center"><small class="text-muted">' + formattedDate + '</small></td></tr>';
|
||||
}
|
||||
});
|
||||
|
||||
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 = '<nav class="mt-3"><ul class="pagination justify-content-center mb-0 flex-wrap" data-table="' + tableId + '">';
|
||||
|
||||
// Inicio
|
||||
var firstDisabled = currentPage <= 1 ? ' disabled' : '';
|
||||
html += '<li class="page-item' + firstDisabled + '"><a class="page-link" href="javascript:void(0)" data-page="1">Inicio</a></li>';
|
||||
|
||||
// Page numbers
|
||||
var startPage = Math.max(1, currentPage - 2);
|
||||
var endPage = Math.min(totalPages, currentPage + 2);
|
||||
if (currentPage <= 2) endPage = Math.min(totalPages, 5);
|
||||
if (currentPage >= totalPages - 1) startPage = Math.max(1, totalPages - 4);
|
||||
|
||||
for (var i = startPage; i <= endPage; i++) {
|
||||
var active = i === currentPage ? ' active' : '';
|
||||
html += '<li class="page-item' + active + '"><a class="page-link" href="javascript:void(0)" data-page="' + i + '">' + i + '</a></li>';
|
||||
}
|
||||
|
||||
// Ver más
|
||||
if (endPage < totalPages) {
|
||||
var nextPage = Math.min(currentPage + 5, totalPages);
|
||||
html += '<li class="page-item"><a class="page-link" href="javascript:void(0)" data-page="' + nextPage + '">Ver más</a></li>';
|
||||
}
|
||||
|
||||
// Fin
|
||||
var lastDisabled = currentPage >= totalPages ? ' disabled' : '';
|
||||
html += '<li class="page-item' + lastDisabled + '"><a class="page-link" href="javascript:void(0)" data-page="' + totalPages + '">Fin</a></li>';
|
||||
|
||||
html += '</ul></nav>';
|
||||
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
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
@@ -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 = '<nav aria-label="Paginación" class="mt-3">';
|
||||
$html .= '<ul class="pagination justify-content-center mb-0 flex-wrap" data-table="' . esc_attr($tableId) . '">';
|
||||
|
||||
// "Inicio" button
|
||||
$firstDisabled = $currentPage <= 1 ? 'disabled' : '';
|
||||
$html .= sprintf(
|
||||
'<li class="page-item %s"><a class="page-link" href="javascript:void(0)" data-page="1">Inicio</a></li>',
|
||||
$firstDisabled
|
||||
);
|
||||
|
||||
// Page numbers (show max 5 pages around current)
|
||||
$startPage = max(1, $currentPage - 2);
|
||||
$endPage = min($totalPages, $currentPage + 2);
|
||||
|
||||
// Adjust if near start or end
|
||||
if ($currentPage <= 2) {
|
||||
$endPage = min($totalPages, 5);
|
||||
}
|
||||
if ($currentPage >= $totalPages - 1) {
|
||||
$startPage = max(1, $totalPages - 4);
|
||||
}
|
||||
|
||||
// Page numbers
|
||||
for ($i = $startPage; $i <= $endPage; $i++) {
|
||||
$active = $i === $currentPage ? 'active' : '';
|
||||
$html .= sprintf(
|
||||
'<li class="page-item %s"><a class="page-link" href="javascript:void(0)" data-page="%d">%d</a></li>',
|
||||
$active,
|
||||
$i,
|
||||
$i
|
||||
);
|
||||
}
|
||||
|
||||
// "Ver más" if there are more pages
|
||||
if ($endPage < $totalPages) {
|
||||
$nextPage = min($currentPage + 5, $totalPages);
|
||||
$html .= sprintf(
|
||||
'<li class="page-item"><a class="page-link" href="javascript:void(0)" data-page="%d">Ver más</a></li>',
|
||||
$nextPage
|
||||
);
|
||||
}
|
||||
|
||||
// "Fin" button
|
||||
$lastDisabled = $currentPage >= $totalPages ? 'disabled' : '';
|
||||
$html .= sprintf(
|
||||
'<li class="page-item %s"><a class="page-link" href="javascript:void(0)" data-page="%d">Fin</a></li>',
|
||||
$lastDisabled,
|
||||
$totalPages
|
||||
);
|
||||
|
||||
$html .= '</ul></nav>';
|
||||
|
||||
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
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($total_counts['total_searches'] > 20) : ?>
|
||||
<div class="card-footer text-center">
|
||||
<button class="btn btn-sm btn-outline-secondary" id="load-more-searches" data-offset="20">
|
||||
<i class="bi bi-plus-circle me-1"></i>
|
||||
<?php esc_html_e('Cargar más', 'roi-apu-search'); ?>
|
||||
</button>
|
||||
<?php
|
||||
$total_search_pages = (int) ceil($total_counts['total_searches'] / self::ITEMS_PER_PAGE);
|
||||
if ($total_search_pages > 1) :
|
||||
?>
|
||||
<div class="card-footer" id="pagination-searches">
|
||||
<?php echo $this->render_pagination(1, $total_search_pages, 'top-searches'); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -766,12 +938,12 @@ final class ROI_APU_Analytics_Dashboard
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($total_counts['total_clicks'] > 20) : ?>
|
||||
<div class="card-footer text-center">
|
||||
<button class="btn btn-sm btn-outline-secondary" id="load-more-clicks" data-offset="20">
|
||||
<i class="bi bi-plus-circle me-1"></i>
|
||||
<?php esc_html_e('Cargar más', 'roi-apu-search'); ?>
|
||||
</button>
|
||||
<?php
|
||||
$total_click_pages = (int) ceil($total_counts['total_clicks'] / self::ITEMS_PER_PAGE);
|
||||
if ($total_click_pages > 1) :
|
||||
?>
|
||||
<div class="card-footer" id="pagination-clicks">
|
||||
<?php echo $this->render_pagination(1, $total_click_pages, 'top-clicks'); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -812,12 +984,12 @@ final class ROI_APU_Analytics_Dashboard
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($total_counts['total_zero_results'] > 20) : ?>
|
||||
<div class="card-footer text-center">
|
||||
<button class="btn btn-sm btn-outline-secondary" id="load-more-zero" data-offset="20">
|
||||
<i class="bi bi-plus-circle me-1"></i>
|
||||
<?php esc_html_e('Cargar más', 'roi-apu-search'); ?>
|
||||
</button>
|
||||
<?php
|
||||
$total_zero_pages = (int) ceil($total_counts['total_zero_results'] / self::ITEMS_PER_PAGE);
|
||||
if ($total_zero_pages > 1) :
|
||||
?>
|
||||
<div class="card-footer" id="pagination-zero-results">
|
||||
<?php echo $this->render_pagination(1, $total_zero_pages, 'zero-results'); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user