From c46f1fd86200dd5c1cc53f84eeec269216c4b297 Mon Sep 17 00:00:00 2001 From: mgonzalez Date: Fri, 5 Jun 2026 16:54:02 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20analytics=20propio=20=E2=80=94=20histor?= =?UTF-8?q?ial=20diario,=20gr=C3=A1fica=2030=20d=C3=ADas,=20top=20p=C3=A1g?= =?UTF-8?q?inas=20en=20dashboard=20WP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 12 ++ VERSION | 2 +- .../pflores-cv-fields/pflores-cv-fields.php | 176 +++++++++++++++--- 3 files changed, 168 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8ddae1..0405f9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ Formato: `X.Y.Z` según `VERSIONADO.md` --- +## [1.2.0] — 2026-06-05 + +### feat: Analytics propio — historial diario, gráfica 30 días, top páginas + +- **Tabla `cv_cv_visits`** creada en BD dev (`.20`) y prod (`.32`): registra visitas únicas por día y URL. +- **Migración automática** del acumulado anterior (`persona/cvolio/lumina/apex_total_visits`) a la nueva tabla. +- **Registro de visitas** en `template_redirect` — deduplicado por IP+URI+hora con transient, solo para visitantes no autenticados. +- **Limpieza semanal** vía WP-Cron: elimina registros con más de 365 días. +- **Widget de dashboard** `📊 CV Analytics — Estadísticas` con: 4 tarjetas (Hoy / 7d / 30d / Total), gráfica de barras CSS últimos 30 días sin librerías externas, top 5 páginas con porcentaje, nota LOPDP al pie. + +--- + ## [1.1.0] — 2026-06-05 ### feat: LOPDP Ecuador completo diff --git a/VERSION b/VERSION index 9084fa2..26aaba0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.0 +1.2.0 diff --git a/backend/wordpress/wp-content/plugins/pflores-cv-fields/pflores-cv-fields.php b/backend/wordpress/wp-content/plugins/pflores-cv-fields/pflores-cv-fields.php index c0f4044..19dac7b 100644 --- a/backend/wordpress/wp-content/plugins/pflores-cv-fields/pflores-cv-fields.php +++ b/backend/wordpress/wp-content/plugins/pflores-cv-fields/pflores-cv-fields.php @@ -459,34 +459,168 @@ function pflores_lopd_banner(): void 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 ''; - foreach ($temas as $slug => $visitas) { - echo ''; - echo ''; + echo '
'; + foreach ($cards as [$label, $num, $color]) { + echo '
'; + echo '
' . esc_html((string) $num) . '
'; + echo '
' . esc_html($label) . '
'; + echo '
'; } - echo '
'; - echo ''; - echo '
' . esc_html($slug) . '' . esc_html((string) $visitas) . '
Total' . esc_html((string) $total) . '
'; - echo '

Visitas únicas por hora (LOPDP — datos anónimos)

'; + echo ''; + + // Gráfica de barras — últimos 30 días + if (! empty($dias30)) { + $maxV = max(array_column($dias30, 'v')) ?: 1; + echo '
'; + echo '

Últimos 30 días

'; + echo '
'; + foreach ($dias30 as $row) { + $h = max(2, (int) round(((int) $row['v'] / $maxV) * 60)); + $tip = esc_attr($row['fecha'] . ': ' . $row['v']); + echo '
'; + } + echo '
'; + } + + // Top 5 páginas + if (! empty($top)) { + echo '

Top páginas

'; + echo ''; + foreach ($top as $row) { + $pct = $total > 0 ? round(((int) $row['v'] / $total) * 100) : 0; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + echo '
' . esc_html($row['pagina']) . '' . esc_html((string) $row['v']) . '' . esc_html((string) $pct) . '%
'; + } + + echo '

Visitas únicas/hora · LOPDP — datos anónimos

'; } /* ─────────────────────────────────────────────