feat: analytics propio — historial diario, gráfica 30 días, top páginas en dashboard WP
This commit is contained in:
@@ -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
|
## [1.1.0] — 2026-06-05
|
||||||
|
|
||||||
### feat: LOPDP Ecuador completo
|
### feat: LOPDP Ecuador completo
|
||||||
|
|||||||
@@ -459,34 +459,168 @@ function pflores_lopd_banner(): void
|
|||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
// C) Widget de visitas en el dashboard
|
// C1) Crear tabla si no existe (admin_init)
|
||||||
add_action('wp_dashboard_setup', 'pflores_visitas_widget');
|
add_action('admin_init', function () {
|
||||||
function pflores_visitas_widget(): void
|
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(
|
wp_add_dashboard_widget(
|
||||||
'pflores_visitas_totales',
|
'pflores_analytics',
|
||||||
'CV Patricia Flores — Estadísticas de visitas',
|
'📊 CV Analytics — Estadísticas',
|
||||||
'pflores_render_visitas_widget'
|
'pflores_render_analytics_widget'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function pflores_render_visitas_widget(): void
|
|
||||||
|
function pflores_render_analytics_widget(): void
|
||||||
{
|
{
|
||||||
$temas = [
|
global $wpdb;
|
||||||
'persona-cv' => (int) get_option('persona_total_visits', 0),
|
$t = $wpdb->prefix . 'cv_visits';
|
||||||
'cvolio' => (int) get_option('cvolio_total_visits', 0),
|
|
||||||
'lumina' => (int) get_option('lumina_total_visits', 0),
|
$hoy = (int) $wpdb->get_var("SELECT COALESCE(SUM(visitas),0) FROM `{$t}` WHERE fecha=CURDATE()");
|
||||||
'apex-cv' => (int) get_option('apex_total_visits', 0),
|
$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 '<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:12px">';
|
||||||
echo '<table style="width:100%;border-collapse:collapse;font-size:.9rem">';
|
foreach ($cards as [$label, $num, $color]) {
|
||||||
foreach ($temas as $slug => $visitas) {
|
echo '<div style="background:#f8f9fa;border-top:3px solid ' . esc_attr($color) . ';border-radius:4px;padding:8px;text-align:center">';
|
||||||
echo '<tr><td style="padding:4px 8px">' . esc_html($slug) . '</td>';
|
echo '<div style="font-size:1.4rem;font-weight:700;color:' . esc_attr($color) . '">' . esc_html((string) $num) . '</div>';
|
||||||
echo '<td style="padding:4px 8px;text-align:right;font-weight:600">' . esc_html((string) $visitas) . '</td></tr>';
|
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 '</div>';
|
||||||
echo '<td style="padding:6px 8px;text-align:right;font-weight:700">' . esc_html((string) $total) . '</td></tr>';
|
|
||||||
echo '</table>';
|
// Gráfica de barras — últimos 30 días
|
||||||
echo '<p style="margin-top:8px;font-size:.78rem;color:#666">Visitas únicas por hora (LOPDP — datos anónimos)</p>';
|
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