Files
roi-apu-search/roi-apu-search.php
FrankZamora 41fb658ca7 feat(analytics): Dashboard v2 con recomendaciones accionables y UX mejorada
- Agregar KPIs con tendencias vs período anterior (↑↓% comparativo)
- Implementar secciones de recomendaciones: Contenido a Crear, CTR 0%,
  Quick Wins, Contenido Estrella, Contenido en Decadencia
- Convertir listados a tablas con columnas separadas para mejor legibilidad
- Agregar botones Editar + Ver en todas las tablas de posts
- Ocultar secciones vacías dinámicamente (Búsquedas Sin Resultados)
- Relajar criterios Quick Wins: pos 2-15, CTR ≥2%, búsquedas ≥2
- Incluir distribución de clicks por posición con barras de progreso
- Agregar exportación a Markdown para análisis con IA

Archivos nuevos:
- admin/class-analytics-dashboard.php (UI del dashboard)
- admin/class-metrics-repository.php (queries de métricas)
- admin/assets/dashboard.css (estilos Bootstrap 5)
- admin/assets/dashboard.js (interactividad y export)
- sql/create-indices.sql (índices para optimización)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 20:15:56 -06:00

283 lines
8.6 KiB
PHP

<?php
/**
* Plugin Name: ROI APU Search
* Plugin URI: https://analisisdepreciosunitarios.com
* Description: Motor de busqueda ultra-rapido para Analisis de Precios Unitarios con PDO persistente y scoring hibrido.
* Version: 1.3.0
* Author: ROI Theme
* Author URI: https://analisisdepreciosunitarios.com
* Text Domain: roi-apu-search
* Domain Path: /languages
* Requires at least: 6.0
* Requires PHP: 8.0
*
* @package ROI_APU_Search
*/
declare(strict_types=1);
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
// Plugin constants
define('ROI_APU_SEARCH_VERSION', '1.3.0');
define('ROI_APU_SEARCH_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('ROI_APU_SEARCH_PLUGIN_URL', plugin_dir_url(__FILE__));
define('ROI_APU_SEARCH_PLUGIN_BASENAME', plugin_basename(__FILE__));
/**
* Main plugin class
*/
final class ROI_APU_Search_Plugin
{
private static ?self $instance = null;
/**
* Get singleton instance
*/
public static function get_instance(): self
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct()
{
$this->load_dependencies();
$this->init_hooks();
}
/**
* Load required files
*/
private function load_dependencies(): void
{
require_once ROI_APU_SEARCH_PLUGIN_DIR . 'includes/class-db-connection.php';
require_once ROI_APU_SEARCH_PLUGIN_DIR . 'includes/class-redis-cache.php';
require_once ROI_APU_SEARCH_PLUGIN_DIR . 'includes/class-search-engine.php';
require_once ROI_APU_SEARCH_PLUGIN_DIR . 'includes/class-shortcode.php';
require_once ROI_APU_SEARCH_PLUGIN_DIR . 'includes/class-analytics.php';
// Admin dashboard
if (is_admin()) {
require_once ROI_APU_SEARCH_PLUGIN_DIR . 'admin/class-metrics-repository.php';
require_once ROI_APU_SEARCH_PLUGIN_DIR . 'admin/class-analytics-dashboard.php';
}
}
/**
* Initialize WordPress hooks
*/
private function init_hooks(): void
{
// Register shortcode
add_action('init', [$this, 'register_shortcode']);
// Enqueue assets
add_action('wp_enqueue_scripts', [$this, 'enqueue_assets']);
// AJAX handlers (for non-SHORTINIT fallback)
add_action('wp_ajax_roi_apu_search', [$this, 'handle_ajax_search']);
add_action('wp_ajax_nopriv_roi_apu_search', [$this, 'handle_ajax_search']);
// Initialize admin dashboard
if (is_admin()) {
ROI_APU_Analytics_Dashboard::get_instance()->init();
}
}
/**
* Register the shortcode
*/
public function register_shortcode(): void
{
$shortcode = new ROI_APU_Search_Shortcode();
add_shortcode('roi_apu_search', [$shortcode, 'render']);
}
/**
* Enqueue frontend assets
*/
public function enqueue_assets(): void
{
// Only load if shortcode is used
global $post;
if (!$post || !has_shortcode($post->post_content, 'roi_apu_search')) {
return;
}
wp_enqueue_style(
'roi-apu-search',
ROI_APU_SEARCH_PLUGIN_URL . 'assets/css/search-ui.css',
[],
ROI_APU_SEARCH_VERSION
);
wp_enqueue_script(
'roi-apu-search',
ROI_APU_SEARCH_PLUGIN_URL . 'assets/js/search-handler.js',
[],
ROI_APU_SEARCH_VERSION,
true
);
// Obtener configuracion de AdSense del tema (si existe)
// Verifica que la funcion exista para evitar errores si se usa otro tema
$adsConfig = function_exists('roi_get_adsense_search_config')
? roi_get_adsense_search_config()
: ['enabled' => false];
// Localize script with AJAX URL, nonce, click tracking URL, and ads config
wp_localize_script('roi-apu-search', 'roiApuSearch', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'apiUrl' => ROI_APU_SEARCH_PLUGIN_URL . 'api/search-endpoint.php',
'clickUrl' => ROI_APU_SEARCH_PLUGIN_URL . 'api/click-endpoint.php',
'nonce' => wp_create_nonce('roi_apu_search'),
'ads' => $adsConfig,
]);
}
/**
* Handle AJAX search request (fallback if SHORTINIT doesn't work)
*/
public function handle_ajax_search(): void
{
// Verify nonce
if (!check_ajax_referer('roi_apu_search', 'nonce', false)) {
wp_send_json_error(['message' => 'Invalid security token'], 403);
}
// Store raw term before sanitization
$term_raw = isset($_POST['term']) ? (string) wp_unslash($_POST['term']) : '';
$term = sanitize_text_field($term_raw);
$page = isset($_POST['page']) ? absint($_POST['page']) : 1;
$per_page = isset($_POST['per_page']) ? absint($_POST['per_page']) : 10;
$categories = isset($_POST['categories']) ? sanitize_text_field(wp_unslash($_POST['categories'])) : '';
// Validate term
$min_len = 3;
$max_len = 250;
if (mb_strlen($term, 'UTF-8') < $min_len) {
wp_send_json_error(['message' => "Minimo {$min_len} caracteres"], 400);
}
if (mb_strlen($term, 'UTF-8') > $max_len) {
$term = mb_substr($term, 0, $max_len, 'UTF-8');
}
// Execute search
try {
$db = ROI_APU_Search_DB::get_instance();
$pdo = $db->get_pdo();
$prefix = $db->get_prefix();
$search = new ROI_APU_Search_Engine($pdo);
// Parse categories
$category_ids = [];
if (!empty($categories)) {
$category_ids = $this->parse_categories($categories);
}
$offset = ($page - 1) * $per_page;
$results = $search->run($term, $per_page, $offset, $category_ids);
// Build permalinks for each result (search engine returns empty permalink)
$site_url = rtrim(home_url(), '/');
$permalink_structure = get_option('permalink_structure', '/%postname%/');
foreach ($results['rows'] as &$row) {
if (empty($row['permalink'])) {
$postName = $row['post_name'] ?? '';
if (empty($postName)) {
$row['permalink'] = $site_url . '/?p=' . $row['ID'];
continue;
}
// Build according to permalink structure
if (strpos($permalink_structure, '%post_id%') !== false) {
$row['permalink'] = $site_url . '/' . $row['ID'] . '/';
} else {
$row['permalink'] = $site_url . '/' . $postName . '/';
}
}
}
unset($row);
// Log search for analytics
$analytics_cfg = ROI_APU_Search_Analytics::get_config();
$search_id = ROI_APU_Search_Analytics::logSearch(
$pdo,
$prefix,
$analytics_cfg,
$term_raw,
$term,
$results,
$page,
$per_page
);
// Add search_id to response for click tracking
$results['search_id'] = $search_id;
wp_send_json_success($results);
} catch (\Exception $e) {
wp_send_json_error(['message' => 'Error en la busqueda'], 500);
}
}
/**
* Parse categories string to array of IDs
*/
private function parse_categories(string $categories): array
{
$parts = array_filter(array_map('trim', explode(',', $categories)));
$ids = [];
foreach ($parts as $part) {
if (is_numeric($part)) {
$ids[] = (int) $part;
} else {
// Try to get category by slug
$term = get_term_by('slug', $part, 'category');
if ($term) {
$ids[] = (int) $term->term_id;
}
}
}
return array_unique($ids);
}
}
// Initialize plugin
add_action('plugins_loaded', function () {
ROI_APU_Search_Plugin::get_instance();
});
// Activation hook
register_activation_hook(__FILE__, function () {
// Check PHP version
if (version_compare(PHP_VERSION, '8.0', '<')) {
deactivate_plugins(plugin_basename(__FILE__));
wp_die('ROI APU Search requiere PHP 8.0 o superior.');
}
// Flush rewrite rules
flush_rewrite_rules();
});
// Deactivation hook
register_deactivation_hook(__FILE__, function () {
flush_rewrite_rules();
});