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:
FrankZamora
2025-12-03 22:46:05 -06:00
parent e92d2018b1
commit 5e7626725d
3 changed files with 548 additions and 199 deletions

View File

@@ -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>