table_name = $wpdb->prefix . 'afa_verificame'; // 1. Intento estándar de crear tabla en el admin if ( is_admin() ) { add_action('admin_init', array($this, 'create_or_update_table')); } // 2. Registro del Shortcode [verificame] add_shortcode('verificame', array($this, 'shortcode_callback')); // 3. Carga de Assets (CSS/FA) solo si es necesario add_action('wp_enqueue_scripts', array($this, 'enqueue_assets')); // 4. Inyección de JS en el footer add_action('wp_footer', array($this, 'print_js')); } /** * Carga estilos y librerías externas condicionalmente. */ public function enqueue_assets() { if ( is_admin() ) return; global $post; // Cargar solo si el post contiene el shortcode if ( is_a($post, 'WP_Post') && has_shortcode($post->post_content, 'verificame') ) { // Font Awesome 6.5 wp_enqueue_style( 'font-awesome-6', 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css', array(), '6.5.1' ); // CSS Inline personalizado $custom_css = $this->get_inline_css(); wp_add_inline_style('font-awesome-6', $custom_css); } } /** * Retorna el CSS completo para el módulo. */ private function get_inline_css() { return ' /* =========================================== AID FOR AIDS – VERIFÍCAME CONTRATISTAS =========================================== */ .afa-validacion-shell { padding: 40px 15px; background: #f4f7fb; } .afa-validacion-wrapper { max-width: 960px; margin: 0 auto; background: #ffffff; border-radius: 24px; box-shadow: 0 18px 45px rgba(0, 45, 90, 0.18); overflow: hidden; font-family: "Raleway", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } /* Encabezado */ .afa-validacion-header { background: linear-gradient(135deg, #003666, #0082cf); padding: 24px 30px; color: #ffffff; display: flex; align-items: center; gap: 16px; } .afa-validacion-titles h2 { margin: 0; font-size: 26px; font-weight: 700; letter-spacing: 0.03em; } .afa-validacion-titles p { margin: 4px 0 0; font-size: 13px; opacity: 0.9; } /* Cuerpo */ .afa-validacion-body { padding: 26px 28px 30px; } .afa-grid { display: grid; gap: 24px; transition: all 0.3s ease; } .afa-grid.admin-open { grid-template-columns: 1fr 1.3fr; } .afa-grid.admin-closed { grid-template-columns: 1fr; position: relative; } @media (max-width: 900px) { .afa-grid.admin-open { grid-template-columns: 1fr; } } /* Toggle Admin */ .afa-admin-toggle { display: flex; justify-content: flex-end; margin-bottom: 12px; } .afa-admin-toggle-btn { font-size: 11px; text-transform: uppercase; letter-spacing: 0.12em; border-radius: 999px; padding: 7px 14px; background: #003259; color: #ffffff; border: none; cursor: pointer; display: inline-flex; align-items: center; gap: 8px; transition: background 0.2s; } .afa-admin-toggle-btn:hover { background: #004a85; } /* Animación Panel Admin */ .afa-admin-column { transition: transform .25s ease, opacity .25s ease; } .afa-grid.admin-closed .afa-admin-column { position: absolute; top: 0; right: 0; width: 100%; max-width: 400px; transform: translateX(120%); opacity: 0; pointer-events: none; } /* Tarjetas */ .afa-card { background: #f8fbff; border-radius: 18px; border: 1px solid #e1e9f3; padding: 22px 20px 20px; } .afa-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; } .afa-card-header h3 { margin: 0; font-size: 16px; font-weight: 700; color: #003259; text-transform: uppercase; letter-spacing: 0.06em; display: flex; align-items: center; gap: 8px; } .afa-card-header h3 i { color: #0082cf; } .afa-card-header span { font-size: 11px; text-transform: uppercase; letter-spacing: 0.12em; color: #8ba5c4; } /* Inputs y Formularios */ .afa-field-group { margin-bottom: 16px; } .afa-label { display: block; font-size: 13px; font-weight: 600; color: #003259; margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.07em; } .afa-input, .afa-select, .afa-file-input { width: 100%; padding: 12px 13px; border-radius: 10px; border: 1px solid #c9d6ea; font-size: 14px; background: #ffffff; box-sizing: border-box; transition: border-color .16s ease, box-shadow .16s ease, transform .07s ease; } .afa-input:focus, .afa-select:focus, .afa-file-input:focus { border-color: #0082cf; box-shadow: 0 0 0 3px rgba(0,130,207,0.1); transform: translateY(-1px); outline: none; } .afa-input[disabled], .afa-select[disabled] { background: #eef2f7; color: #8898aa; cursor: not-allowed; } /* Botones */ .afa-actions { display: flex; gap: 14px; flex-wrap: wrap; align-items: center; } .afa-btn { border: none; border-radius: 999px; padding: 10px 22px; font-size: 14px; font-weight: 700; text-transform: uppercase; cursor: pointer; display: inline-flex; align-items: center; gap: 8px; transition: all .2s ease; text-decoration: none; } .afa-btn i { font-size: 0.9em; } .afa-btn-primary { background: linear-gradient(135deg, #025993, #0082cf); color: #fff; box-shadow: 0 6px 16px rgba(0,90,150,0.35); } .afa-btn-primary:hover { transform: translateY(-2px); box-shadow: 0 12px 28px rgba(0,80,140,0.45); } .afa-btn-secondary { background: #ffffff; border: 1px solid #d5dfef; color: #003259; } .afa-btn-secondary:hover { background: #f7faff; border-color: #b8cadd; } .afa-btn-danger { background: linear-gradient(135deg, #d32f2f, #e53935); color: #ffffff; box-shadow: 0 6px 16px rgba(200,60,60,0.35); } .afa-btn-danger:hover { transform: translateY(-1px); box-shadow: 0 12px 28px rgba(180,40,40,0.45); } .afa-btn-utility { font-size: 12px; padding: 6px 12px; color: #0082cf; background: rgba(0, 130, 207, 0.08); border: none; } .afa-btn-utility:hover { background: rgba(0, 130, 207, 0.15); } /* Alertas */ .afa-alert { margin-top: 12px; padding: 12px 14px; border-radius: 12px; font-size: 14px; font-weight: 600; display: flex; gap: 10px; align-items: center; } .afa-alert-success { background: #e0f7f0; color: #1b5e20; border: 1px solid #8bd7a0; } .afa-alert-error { background: #ffebee; color: #c62828; border: 1px solid #ef9a9a; } /* Tabla de Datos */ .afa-table-wrapper { margin-top: 20px; max-height: 400px; overflow-y: auto; border: 1px solid #e1e9f3; border-radius: 12px; box-shadow: inset 0 2px 4px rgba(0,0,0,0.02); } .afa-data-table { width: 100%; border-collapse: collapse; font-size: 13px; text-align: left; } .afa-data-table thead th { background: #f4f7fb; color: #003259; padding: 12px; position: sticky; top: 0; z-index: 2; font-weight: 700; border-bottom: 2px solid #e1e9f3; white-space: nowrap; } .afa-data-table tbody td { padding: 10px 12px; border-bottom: 1px solid #eee; color: #444; vertical-align: middle; } /* Resultados */ .afa-verif-result { margin-top: 22px; padding: 26px 24px; border-radius: 22px; border: 1px solid #b7e7c7; background: #ffffff; display: flex; gap: 20px; align-items: center; box-shadow: 0 10px 28px rgba(0, 80, 140, 0.08); } .afa-verif-result-success { border-color: #a5d6a7; background: #f1f8e9; } .afa-verif-result-error { border-color: #ef9a9a; background: #ffebee; } .afa-verif-photo { width: 110px; height: 110px; border-radius: 50%; overflow: hidden; background: #ffffff; flex-shrink: 0; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 12px rgba(0,0,0,0.1); } .afa-verif-photo img { width: 100%; height: 100%; object-fit: cover; } .afa-verif-info { line-height:1.5; } .afa-verif-name { font-size: 22px; font-weight: 800; color: #003259; margin-bottom: 4px; display: flex; align-items: center; gap: 8px; } /* Responsive */ @media (max-width: 600px) { .afa-validacion-header { padding: 18px; } .afa-validacion-titles h2 { font-size: 20px; } .afa-validacion-body { padding: 20px 15px; } .afa-verif-result { flex-direction: column; text-align: center; } .afa-verif-name { justify-content: center; } } '; } /** * Imprime el JavaScript necesario para la interactividad del frontend. */ public function print_js() { if ( is_admin() ) return; global $post; if ( is_a($post, 'WP_Post') && has_shortcode($post->post_content, 'verificame') ) { ?> get_var("SHOW TABLES LIKE '{$this->table_name}'"); if ( ! $check_table ) { $this->create_or_update_table(); } // Cargar funciones de usuario si no están disponibles if ( ! function_exists('wp_get_current_user') ) { require_once ABSPATH . 'wp-includes/pluggable.php'; } $es_admin = current_user_can('manage_options'); $consulta = null; $msg_csv = null; $msg_delete = null; $registros = array(); // 1. Procesar Consulta Pública if ( ! empty($_POST['afa_check_nonce']) && wp_verify_nonce($_POST['afa_check_nonce'], 'afa_check_contratista') ) { $consulta = $this->procesar_consulta(); } // 2. Procesar Carga CSV (Solo Admin) if ( $es_admin && ! empty($_POST['afa_csv_nonce']) && wp_verify_nonce($_POST['afa_csv_nonce'], 'afa_csv_upload') ) { $msg_csv = $this->procesar_csv(); } // 3. Procesar Eliminación (Solo Admin) if ( $es_admin && ! empty($_POST['afa_del_nonce']) && wp_verify_nonce($_POST['afa_del_nonce'], 'afa_delete_contratista') ) { $msg_delete = $this->procesar_eliminar(); } $bloquear_form = ($consulta !== null); // 4. Obtener Registros Recientes if ($es_admin) { $registros = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$this->table_name} ORDER BY created_at DESC LIMIT %d", 50 ) ); } $data = array( 'es_admin' => $es_admin, 'consulta' => $consulta, 'msg_csv' => $msg_csv, 'msg_delete' => $msg_delete, 'bloquear_form' => $bloquear_form, 'registros' => $registros, ); return $this->render_view($data); } /* ========================================================= * VISTA (HTML) * ======================================================= */ private function render_view($data) { extract($data); ob_start(); ?>

Verifícame

Usa nuestra herramienta de consulta oficial para confirmar si una persona es un contratista autorizado por nuestra organización.

Uso público
required >
Foto contratista
descripcion_tipo($consulta['data']['tipo_id']) ); ?> •
Contratista verificado
No encontrado
Por favor verifica la información digitada o contacta al administrador.
get_charset_collate(); // IMPORTANTE: dbDelta requiere dos espacios entre palabras clave KEY y la definición $sql = "CREATE TABLE {$this->table_name} ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, tipo_id VARCHAR(20) NOT NULL, numero_id VARCHAR(50) NOT NULL, nombre VARCHAR(190) NULL, cargo VARCHAR(190) NULL, foto VARCHAR(255) NULL, estado VARCHAR(20) NOT NULL DEFAULT 'activo', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY tipo_numero_unico (tipo_id, numero_id) ) $charset_collate;"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta($sql); } /* ========================================================= * HELPERS Y UTILIDADES * ======================================================= */ /** * Busca automáticamente la foto en el servidor. * Ruta esperada: /wp-content/themes/TU_TEMA_ACTIVO/web/verificame/fotos/ */ private function buscar_foto_automaticamente($numero_id) { // Si este código se mueve a un plugin, cambiar get_stylesheet_directory por plugin_dir_path $dir = get_stylesheet_directory() . '/web/verificame/fotos/'; $url_base = get_stylesheet_directory_uri() . '/web/verificame/fotos/'; $id = sanitize_file_name($numero_id); $exts = array('jpg', 'jpeg', 'png'); foreach ($exts as $ext) { $file_path = $dir . $id . '.' . $ext; if (file_exists($file_path)) { return $url_base . $id . '.' . $ext; } } return ''; } private function descripcion_tipo($tipo) { $map = array( 'PPT' => 'Permiso de Protección Temporal (PPT)', 'CE' => 'Cédula de Extranjería (CE)', 'CC' => 'Cédula de Ciudadanía (CC)', 'PA' => 'Pasaporte (PA)', ); return $map[$tipo] ?? $tipo; } /* ========================================================= * LÓGICA DE NEGOCIO * ======================================================= */ private function procesar_consulta() { if (empty($_POST['tipo_id_consulta']) || empty($_POST['numero_id_consulta'])) { return array('type' => 'error', 'text' => 'Debes seleccionar el tipo y número de identificación.'); } global $wpdb; $tipo = sanitize_text_field($_POST['tipo_id_consulta']); $numero = sanitize_text_field($_POST['numero_id_consulta']); $row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$this->table_name} WHERE tipo_id = %s AND numero_id = %s AND estado = 'activo' LIMIT 1", $tipo, $numero ) ); if ($row) { // Búsqueda automática de foto $foto_url = $this->buscar_foto_automaticamente($row->numero_id); return array( 'type' => 'success', 'text' => 'Contratista verificado.', 'data' => array( 'tipo_id' => $row->tipo_id, 'numero_id' => $row->numero_id, 'nombre' => $row->nombre, 'cargo' => $row->cargo, 'foto' => $foto_url, ), ); } return array( 'type' => 'error', 'text' => 'El contratista no se encuentra en base de datos.' ); } /** * Procesa la subida e importación del CSV. * Maneja BOM UTF-8 y delimitadores dinámicos. */ private function procesar_csv() { if (empty($_FILES['csv_contratistas'])) { return array('type' => 'error', 'text' => 'No se detectó archivo.'); } $file = $_FILES['csv_contratistas']; if ($file['error'] !== UPLOAD_ERR_OK) { return array('type' => 'error', 'text' => 'Error al subir el archivo.'); } // Validación MIME básica if (class_exists('finfo')) { $finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($file['tmp_name']); $allowed_mimes = array('text/csv', 'text/plain', 'application/csv', 'application/vnd.ms-excel'); if (!in_array($mime, $allowed_mimes)) { return array('type' => 'error', 'text' => 'Formato inválido. Se requiere CSV.'); } } global $wpdb; $tmp = $file['tmp_name']; $handle = fopen($tmp, 'r'); if ( ! $handle ) { return array('type' => 'error', 'text' => 'No se pudo leer el archivo.'); } // 1. Intentar leer con punto y coma $headers = fgetcsv($handle, 0, ';'); // 2. Si falla, intentar con coma if ($headers === false || (is_array($headers) && count($headers) === 1 && strpos($headers[0], ';') !== false)) { rewind($handle); $headers = fgetcsv($handle, 0, ','); } if ($headers === false || !is_array($headers)) { fclose($handle); return array('type' => 'error', 'text' => 'El CSV parece estar vacío o corrupto.'); } // 3. LIMPIEZA DE ENCABEZADOS (Elimina BOM invisible) $headers = array_map(function($header) { $header = preg_replace('/^\xEF\xBB\xBF/', '', $header); return trim(strtolower($header)); }, $headers); $idx_tipo = array_search('tipo_id', $headers); $idx_numero = array_search('numero_id', $headers); $idx_nombre = array_search('nombre', $headers); $idx_cargo = array_search('cargo', $headers); if ($idx_tipo === false || $idx_numero === false) { fclose($handle); $debug_info = "Columnas leídas: " . implode(', ', $headers); return array('type' => 'error', 'text' => "Faltan columnas requeridas (tipo_id, numero_id). Debug: " . $debug_info); } $insertados = 0; $actualizados = 0; $validos = array('PPT','CE','CC','PA'); $wpdb->query('START TRANSACTION'); try { while (($row = fgetcsv($handle, 0, ';')) !== false) { if (count($row) === 1) { $row = str_getcsv($row[0], ','); } if (empty(array_filter($row))) continue; $tipo = isset($row[$idx_tipo]) ? trim($row[$idx_tipo]) : ''; $numero = isset($row[$idx_numero]) ? trim($row[$idx_numero]) : ''; if (!$tipo || !$numero) { continue; } if (!in_array($tipo, $validos, true)) { continue; } $nombre = ($idx_nombre !== false && isset($row[$idx_nombre])) ? sanitize_text_field(trim($row[$idx_nombre])) : ''; $cargo = ($idx_cargo !== false && isset($row[$idx_cargo])) ? sanitize_text_field(trim($row[$idx_cargo])) : ''; $foto = ''; $existe = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$this->table_name} WHERE tipo_id=%s AND numero_id=%s", $tipo, $numero ) ); if ($existe) { $wpdb->update( $this->table_name, array('nombre' => $nombre, 'cargo' => $cargo, 'foto' => $foto, 'estado' => 'activo'), array('id' => $existe), array('%s','%s','%s','%s'), array('%d') ); $actualizados++; } else { $wpdb->insert( $this->table_name, array('tipo_id' => $tipo, 'numero_id' => $numero, 'nombre' => $nombre, 'cargo' => $cargo, 'foto' => $foto, 'estado' => 'activo'), array('%s','%s','%s','%s','%s','%s') ); $insertados++; } } $wpdb->query('COMMIT'); } catch (Exception $e) { $wpdb->query('ROLLBACK'); fclose($handle); return array('type' => 'error', 'text' => 'Error crítico: ' . $e->getMessage()); } fclose($handle); return array( 'type' => 'success', 'text' => "Proceso finalizado. Nuevos: {$insertados}. Actualizados: {$actualizados}." ); } private function procesar_eliminar() { if (empty($_POST['tipo_id_delete']) || empty($_POST['numero_id_delete'])) { return array('type' => 'error', 'text' => 'Datos incompletos para eliminar.'); } global $wpdb; $tipo = sanitize_text_field($_POST['tipo_id_delete']); $numero = sanitize_text_field($_POST['numero_id_delete']); $deleted = $wpdb->delete( $this->table_name, array('tipo_id' => $tipo, 'numero_id' => $numero), array('%s','%s') ); if ($deleted) { return array('type' => 'success', 'text' => 'Registro eliminado.'); } return array('type' => 'error', 'text' => 'No se encontró el registro.'); } } // Inicialización del módulo new AFA_Verificame_Module();