feat: analytics propio — historial diario, gráfica 30 días, top páginas en dashboard WP
This commit is contained in:
@@ -459,34 +459,168 @@ function pflores_lopd_banner(): void
|
||||
<?php
|
||||
}
|
||||
|
||||
// C) Widget de visitas en el dashboard
|
||||
add_action('wp_dashboard_setup', 'pflores_visitas_widget');
|
||||
function pflores_visitas_widget(): void
|
||||
// C1) Crear tabla si no existe (admin_init)
|
||||
add_action('admin_init', function () {
|
||||
if (get_option('pflores_visits_table_v1')) {
|
||||
return;
|
||||
}
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'cv_visits';
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
dbDelta("CREATE TABLE IF NOT EXISTS `{$table}` (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
fecha DATE NOT NULL,
|
||||
pagina VARCHAR(255) NOT NULL DEFAULT '/',
|
||||
visitas INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
UNIQUE KEY fecha_pagina (fecha, pagina)
|
||||
) {$wpdb->get_charset_collate()};");
|
||||
$old = array_sum(array_map(
|
||||
fn ($k) => (int) get_option("{$k}_total_visits", 0),
|
||||
['persona', 'cvolio', 'lumina', 'apex']
|
||||
));
|
||||
if ($old > 0) {
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"INSERT IGNORE INTO `{$table}` (fecha,pagina,visitas) VALUES (%s,'/',%d)",
|
||||
'2026-01-01',
|
||||
$old
|
||||
));
|
||||
}
|
||||
update_option('pflores_visits_table_v1', true);
|
||||
});
|
||||
|
||||
// C2) Registrar visita (template_redirect)
|
||||
add_action('template_redirect', function () {
|
||||
if (is_admin() || wp_doing_ajax() || wp_doing_cron()) {
|
||||
return;
|
||||
}
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'GET') {
|
||||
return;
|
||||
}
|
||||
if (is_user_logged_in() && current_user_can('manage_options')) {
|
||||
return;
|
||||
}
|
||||
if (! is_front_page() && ! is_home() && ! is_singular()) {
|
||||
return;
|
||||
}
|
||||
if (! get_option('pflores_visits_table_v1')) {
|
||||
return;
|
||||
}
|
||||
$uri = strtok(sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'] ?? '/')), '?');
|
||||
$key = 'pflores2_' . md5($uri . '|' . pflores_client_ip() . '|' . gmdate('Y-m-d-H'));
|
||||
if (get_transient($key)) {
|
||||
return;
|
||||
}
|
||||
set_transient($key, '1', HOUR_IN_SECONDS);
|
||||
global $wpdb;
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"INSERT INTO `{$wpdb->prefix}cv_visits` (fecha,pagina,visitas) VALUES (CURDATE(),%s,1)
|
||||
ON DUPLICATE KEY UPDATE visitas=visitas+1",
|
||||
$uri
|
||||
));
|
||||
});
|
||||
|
||||
function pflores_client_ip(): string
|
||||
{
|
||||
foreach (['HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'] as $k) {
|
||||
if (! empty($_SERVER[$k])) {
|
||||
return trim(explode(',', (string) $_SERVER[$k])[0]);
|
||||
}
|
||||
}
|
||||
return '0.0.0.0';
|
||||
}
|
||||
|
||||
// C3) Limpieza semanal (cron)
|
||||
add_action('init', function () {
|
||||
if (! wp_next_scheduled('pflores_visits_cleanup')) {
|
||||
wp_schedule_event(time(), 'weekly', 'pflores_visits_cleanup');
|
||||
}
|
||||
});
|
||||
add_action('pflores_visits_cleanup', function () {
|
||||
global $wpdb;
|
||||
$wpdb->query("DELETE FROM `{$wpdb->prefix}cv_visits`
|
||||
WHERE fecha < DATE_SUB(CURDATE(), INTERVAL 365 DAY)");
|
||||
});
|
||||
|
||||
// C4) Widget de Analytics en el dashboard
|
||||
add_action('wp_dashboard_setup', 'pflores_analytics_widget');
|
||||
function pflores_analytics_widget(): void
|
||||
{
|
||||
wp_add_dashboard_widget(
|
||||
'pflores_visitas_totales',
|
||||
'CV Patricia Flores — Estadísticas de visitas',
|
||||
'pflores_render_visitas_widget'
|
||||
'pflores_analytics',
|
||||
'📊 CV Analytics — Estadísticas',
|
||||
'pflores_render_analytics_widget'
|
||||
);
|
||||
}
|
||||
function pflores_render_visitas_widget(): void
|
||||
|
||||
function pflores_render_analytics_widget(): void
|
||||
{
|
||||
$temas = [
|
||||
'persona-cv' => (int) get_option('persona_total_visits', 0),
|
||||
'cvolio' => (int) get_option('cvolio_total_visits', 0),
|
||||
'lumina' => (int) get_option('lumina_total_visits', 0),
|
||||
'apex-cv' => (int) get_option('apex_total_visits', 0),
|
||||
global $wpdb;
|
||||
$t = $wpdb->prefix . 'cv_visits';
|
||||
|
||||
$hoy = (int) $wpdb->get_var("SELECT COALESCE(SUM(visitas),0) FROM `{$t}` WHERE fecha=CURDATE()");
|
||||
$sem = (int) $wpdb->get_var("SELECT COALESCE(SUM(visitas),0) FROM `{$t}` WHERE fecha>=DATE_SUB(CURDATE(),INTERVAL 7 DAY)");
|
||||
$mes = (int) $wpdb->get_var("SELECT COALESCE(SUM(visitas),0) FROM `{$t}` WHERE fecha>=DATE_SUB(CURDATE(),INTERVAL 30 DAY)");
|
||||
$total = (int) $wpdb->get_var("SELECT COALESCE(SUM(visitas),0) FROM `{$t}`");
|
||||
|
||||
$dias30 = $wpdb->get_results(
|
||||
"SELECT fecha, SUM(visitas) AS v FROM `{$t}`
|
||||
WHERE fecha>=DATE_SUB(CURDATE(),INTERVAL 30 DAY)
|
||||
GROUP BY fecha ORDER BY fecha ASC",
|
||||
ARRAY_A
|
||||
) ?: [];
|
||||
|
||||
$top = $wpdb->get_results(
|
||||
"SELECT pagina, SUM(visitas) AS v FROM `{$t}`
|
||||
GROUP BY pagina ORDER BY SUM(visitas) DESC LIMIT 5",
|
||||
ARRAY_A
|
||||
) ?: [];
|
||||
|
||||
// Tarjetas resumen
|
||||
$cards = [
|
||||
['Hoy', $hoy, '#1a7a8a'],
|
||||
['7 días', $sem, '#1a7a8a'],
|
||||
['30 días', $mes, '#1a7a8a'],
|
||||
['Total', $total, '#c0392b'],
|
||||
];
|
||||
$total = array_sum($temas);
|
||||
echo '<table style="width:100%;border-collapse:collapse;font-size:.9rem">';
|
||||
foreach ($temas as $slug => $visitas) {
|
||||
echo '<tr><td style="padding:4px 8px">' . esc_html($slug) . '</td>';
|
||||
echo '<td style="padding:4px 8px;text-align:right;font-weight:600">' . esc_html((string) $visitas) . '</td></tr>';
|
||||
echo '<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:12px">';
|
||||
foreach ($cards as [$label, $num, $color]) {
|
||||
echo '<div style="background:#f8f9fa;border-top:3px solid ' . esc_attr($color) . ';border-radius:4px;padding:8px;text-align:center">';
|
||||
echo '<div style="font-size:1.4rem;font-weight:700;color:' . esc_attr($color) . '">' . esc_html((string) $num) . '</div>';
|
||||
echo '<div style="font-size:.72rem;color:#555;margin-top:2px">' . esc_html($label) . '</div>';
|
||||
echo '</div>';
|
||||
}
|
||||
echo '<tr style="border-top:1px solid #ccc"><td style="padding:6px 8px;font-weight:700">Total</td>';
|
||||
echo '<td style="padding:6px 8px;text-align:right;font-weight:700">' . esc_html((string) $total) . '</td></tr>';
|
||||
echo '</table>';
|
||||
echo '<p style="margin-top:8px;font-size:.78rem;color:#666">Visitas únicas por hora (LOPDP — datos anónimos)</p>';
|
||||
echo '</div>';
|
||||
|
||||
// Gráfica de barras — últimos 30 días
|
||||
if (! empty($dias30)) {
|
||||
$maxV = max(array_column($dias30, 'v')) ?: 1;
|
||||
echo '<div style="margin-bottom:10px">';
|
||||
echo '<p style="font-size:.72rem;color:#888;margin-bottom:4px;text-transform:uppercase;letter-spacing:.06em">Últimos 30 días</p>';
|
||||
echo '<div style="display:flex;align-items:flex-end;height:65px;gap:2px;overflow:hidden">';
|
||||
foreach ($dias30 as $row) {
|
||||
$h = max(2, (int) round(((int) $row['v'] / $maxV) * 60));
|
||||
$tip = esc_attr($row['fecha'] . ': ' . $row['v']);
|
||||
echo '<div title="' . $tip . '" style="flex:1;height:' . $h . 'px;background:#1B3A5C;border-radius:2px 2px 0 0;min-width:2px;cursor:default"></div>';
|
||||
}
|
||||
echo '</div></div>';
|
||||
}
|
||||
|
||||
// Top 5 páginas
|
||||
if (! empty($top)) {
|
||||
echo '<p style="font-size:.72rem;color:#888;margin-bottom:4px;text-transform:uppercase;letter-spacing:.06em">Top páginas</p>';
|
||||
echo '<table style="width:100%;border-collapse:collapse;font-size:.82rem">';
|
||||
foreach ($top as $row) {
|
||||
$pct = $total > 0 ? round(((int) $row['v'] / $total) * 100) : 0;
|
||||
echo '<tr>';
|
||||
echo '<td style="padding:3px 4px;color:#333">' . esc_html($row['pagina']) . '</td>';
|
||||
echo '<td style="padding:3px 4px;text-align:right;font-weight:600;color:#1B3A5C">' . esc_html((string) $row['v']) . '</td>';
|
||||
echo '<td style="padding:3px 4px;text-align:right;color:#888;font-size:.75rem">' . esc_html((string) $pct) . '%</td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
echo '</table>';
|
||||
}
|
||||
|
||||
echo '<p style="margin-top:8px;font-size:.72rem;color:#999">Visitas únicas/hora · LOPDP — datos anónimos</p>';
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user