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