Cuaderno Técnico: Plugin «Agenda Indaga» (v1.3.0), parte 3

Entrega 3: Los Shortcodes de Frontend (Formulario y Calendario Interactivo)

Valencia, 25/10/2026, G.B.
En las entregas anteriores vimos cómo levantar los cimientos del plugin en la base de datos y cómo estructurar el panel de administración en el backend. En este tercer bloque me centro en la parte visible para el usuario: los shortcodes que dan vida a la agenda en el frontend de la web.

Para conseguirlo, divido la funcionalidad en dos shortcodes independientes pero complementarios: indaga_agenda (entre corchetes) para gestionar el alta, edición y listado de citas, y calendario_agenda (entre corchetes) para renderizar el calendario mensual interactivo.

1. El Shortcode de Gestión y Formulario (indaga_agenda) (entre corchetes)

Este shortcode permite desplegar un formulario de alta y edición de eventos, acompañado de una tabla con los registros actuales de la instancia. Todo ello protegido mediante comprobaciones de sesión de usuario, validación de nonces y funciones de saneamiento de datos.

public function renderizar_shortcode_agenda( $atts ) {
    global $wpdb;
    $tabla = $wpdb->prefix . 'indaga_agenda';
    $atts = shortcode_atts( array( 'id' => 'default' ), $atts, 'indaga_agenda' );
    $grupo_id = sanitize_text_field( $atts['id'] );
    $user_id = get_current_user_id();
    $mensaje = '';

    // Procesamiento del formulario de guardado / edición
    if ( isset( $_POST['indaga_agenda_frontend_nonce'] ) && wp_verify_nonce( $_POST['indaga_agenda_frontend_nonce'], 'guardar_evento_frontend' ) ) {
        if ( !is_user_logged_in() ) {
            return '<p style="color:red;">Debes iniciar sesión para gestionar la agenda.</p>';
        }

        $evento_id = isset( $_POST['evento_id'] ) ? absint( $_POST['evento_id'] ) : 0;
        $titulo = sanitize_text_field( $_POST['titulo'] );
        $descripcion = sanitize_textarea_field( $_POST['descripcion'] );
        $fecha_inicio = sanitize_text_field( $_POST['fecha_inicio'] );
        $fecha_fin = sanitize_text_field( $_POST['fecha_fin'] );
        $categoria = sanitize_text_field( $_POST['categoria'] );

        if ( !empty( $titulo ) && !empty( $fecha_inicio ) ) {
            $datos = array(
                'user_id' => $user_id,
                'grupo_id' => $grupo_id,
                'titulo' => $titulo,
                'descripcion' => $descripcion,
                'fecha_inicio' => $fecha_inicio,
                'fecha_fin' => !empty( $fecha_fin ) ? $fecha_fin : $fecha_inicio,
                'categoria' => $categoria
            );

            if ( $evento_id > 0 ) {
                $wpdb->update( $tabla, $datos, array( 'id' => $evento_id ), array( '%d', '%s', '%s', '%s', '%s', '%s', '%s' ), array( '%d' ) );
                $mensaje = '<div style="background:#d4edda; color:#155724; padding:10px; margin-bottom:15px; border-radius:4px;">¡Evento actualizado con éxito!</div>';
            } else {
                $wpdb->insert( $tabla, $datos, array( '%d', '%s', '%s', '%s', '%s', '%s', '%s' ) );
                $mensaje = '<div style="background:#d4edda; color:#155724; padding:10px; margin-bottom:15px; border-radius:4px;">¡Evento creado con éxito!</div>';
            }
        }
    }
    // ... (código de renderizado de la vista y formulario)
}

Puntos clave del formulario:

  • Soporte multinstancia por atributo: A través del parámetro id="nombre_instancia" (por defecto default), delimito los registros que pertenecen a esa agenda concreta.
  • Doble funcionalidad (Alta / Edición): Si el sistema detecta un parámetro editar_id en la URL, carga automáticamente los datos del evento en los inputs para modificarlos sin duplicar registros.

2. El calendario interactivo (calendario_agenda) (entre corchetes)

Para mostrar los eventos en una vista de calendario mensual limpia y visual, transformo los registros de la base de datos a un array de JavaScript y construyo una cuadrícula dinámica.

public function renderizar_shortcode_calendario( $atts ) {
    global $wpdb;
    $tabla = $wpdb->prefix . 'indaga_agenda';
    $atts = shortcode_atts( array( 'id' => 'default' ), $atts, 'calendario_agenda' );
    $grupo_id = sanitize_text_field( $atts['id'] );

    $eventos = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $tabla WHERE grupo_id = %s ORDER BY fecha_inicio ASC", $grupo_id ) );
    
    $eventos_js = array();
    foreach ( $eventos as $ev ) {
        $eventos_js[] = array(
            'id' => $ev->id,
            'titulo' => $ev->titulo,
            'descripcion' => $ev->descripcion,
            'categoria' => $ev->categoria,
            'fecha_inicio' => $ev->fecha_inicio,
            'fecha_fin' => $ev->fecha_fin
        );
    }
    // ... (retorno del HTML y script de renderizado)
}

Características del motor visual en JavaScript:

  • Navegación fluida por meses: Botones de avance y retroceso (cambiarMes()) que recalculan los días de la semana y sitúan correctamente el primer día del mes.
  • Fichas modales emergentes: Al hacer clic sobre cualquier evento listado dentro de una casilla del calendario, se despliega una ventana flotante con los detalles completos (horarios, categoría y descripción).
  • Conexión directa con el alta: El botón superior «+ Añadir Evento» o hacer clic directamente sobre cualquier día del calendario desplaza la vista automáticamente hacia el formulario y precarga la fecha seleccionada.

Con esta tercera entrega queda completamente documentada la arquitectura del plugin Agenda Indaga (v1.3.0): desde los cimientos en la base de datos, pasando por el panel de administración multinstancia, hasta la interacción visual en el frontend.

Cuaderno Técnico: Plugin «Agenda Indaga» (v1.3.0), parte 2

Entrega 2: El Panel de Administración y la Gestión Multinstancia

En la primera entrega vimos los cimientos del plugin: la estructura principal con la clase IndagaAgenda, la seguridad inicial para bloquear accesos directos y la creación nativa de la tabla en la base de datos.

En este segundo bloque me centro en cómo está construido el panel de administración en el backend de WordPress (ubicado en Ajustes > Agenda Indaga), explicando el menú nativo, la estructura modular por pestañas y el control de las distintas instancias o grupos de eventos.

1. Registro del menú en el panel de administración

Para integrar la interfaz dentro del escritorio de WordPress sin cargar librerías adicionales, utilizo la API nativa de menús a través del gancho admin_menu:

public function registrar_menu_admin() {
    add_menu_page(
        'Agenda Indaga',              // Título de la página
        'Agenda Indaga',              // Título del menú
        'manage_options',             // Capacidad / Permiso requerido
        'indaga-agenda',              // Slug único del menú
        array( $this, 'renderizar_panel_admin' ), // Función de callback
        'dashicons-calendar-alt',     // Icono nativo de Dashicons
        6                             // Posición en la barra lateral
    );
}

Puntos clave:

  • manage_options: Garantizo que solo las cuentas con perfil de administración puedan acceder a este panel de control.
  • dashicons-calendar-alt: Aprovecho la librería de iconos nativa de WordPress (Dashicons), manteniendo el plugin sin dependencias externas.

2. Estructura modular por pestañas

Para que la experiencia de uso sea limpia e intuitiva, organizo el panel de administración en tres pestañas bien diferenciadas usando la navegación nativa de WordPress (nav-tab-wrapper):

  1. Gestión e Instancias: Control global de los eventos y de las instancias creadas.
  2. Instrucciones y Shortcodes: Guía rápida para saber cómo insertar los shortcodes.
  3. Ficha Técnica: Resumen de especificaciones del desarrollo.

El cambio entre pestañas se gestiona capturando el parámetro tab mediante la URL (p. ej., ?page=indaga-agenda&tab=instrucciones), sanitizándolo adecuadamente:

$tab = isset( $_GET['tab'] ) ? sanitize_text_field( $_GET['tab'] ) : 'gestion';
?>
<h2 class="nav-tab-wrapper">
    <a href="?page=indaga-agenda&tab=gestion" class="nav-tab <?php echo $tab === 'gestion' ? 'nav-tab-active' : ''; ?>">Gestión e Instancias</a>
    <a href="?page=indaga-agenda&tab=instrucciones" class="nav-tab <?php echo $tab === 'instrucciones' ? 'nav-tab-active' : ''; ?>">Instrucciones y Shortcodes</a>
    <a href="?page=indaga-agenda&tab=tecnica" class="nav-tab <?php echo $tab === 'tecnica' ? 'nav-tab-active' : ''; ?>">Ficha Técnica</a>
</h2>

3. Procesamiento seguro de acciones (eliminación con nonces)

Antes de pintar la vista o consultar los datos, verifico si se ha enviado alguna petición desde los formularios de administración (por ejemplo, para eliminar un evento suelto o una instancia completa).

La seguridad aquí descansa en dos pilares: comprobación de permisos y validación de Nonce para evitar ataques CSRF:

if ( isset( $_POST['indaga_agenda_nonce'] ) && wp_verify_nonce( $_POST['indaga_agenda_nonce'], 'indaga_agenda_admin_action' ) ) {
    
    // Eliminación de un evento individual
    if ( isset( $_POST['accion'] ) && $_POST['accion'] === 'eliminar_evento' && !empty( $_POST['evento_id'] ) ) {
        $evento_id = absint( $_POST['evento_id'] );
        $wpdb->delete( $tabla, array( 'id' => $evento_id ), array( '%d' ) );
        $mensaje = '<div class="updated notice"><p>Evento eliminado correctamente.</p></div>';
    
    // Eliminación de una instancia completa (y todos sus eventos asociados)
    } elseif ( isset( $_POST['accion'] ) && $_POST['accion'] === 'eliminar_instancia' && !empty( $_POST['grupo_id'] ) ) {
        $grupo_id = sanitize_text_field( $_POST['grupo_id'] );
        $wpdb->delete( $tabla, array( 'grupo_id' => $grupo_id ), array( '%s' ) );
        $mensaje = '<div class="updated notice"><p>Instancia completa y todos sus eventos asociados eliminados correctamente.</p></div>';
    }
}
  • wp_verify_nonce(): Garantiza que la petición se ha originado legítimamente dentro del panel y por un usuario autorizado.
  • absint() y sanitize_text_field(): Aseguran que los datos recibidos sean enteros positivos o cadenas limpias sin código malicioso.

4. Consulta agrupada para la gestión multinstancia

El valor diferencial de la versión 1.3.0 es el soporte multinstancia. Mediante una consulta SQL optimizada con agrupamiento (GROUP BY), obtengo la lista de todas las agendas o grupos existentes en la base de datos sin necesidad de tablas auxiliares:

$instancias = $wpdb->get_results( 
    "SELECT grupo_id, COUNT(*) as total_eventos, MIN(fecha_inicio) as primer_evento 
     FROM $tabla 
     GROUP BY grupo_id 
     ORDER BY grupo_id ASC" 
);

Con esto puedo renderizar una tabla clara que indica el nombre del grupo, cuántos eventos almacena y qué shortcode exacto debe copiarse para mostrar esa agenda concreta en el frontend:

<?php foreach ( $instancias as $inst ): ?>
    <tr>
        <td><strong><?php echo esc_html( $inst->grupo_id ); ?></strong></td>
        <td><?php echo esc_html( $inst->total_eventos ); ?> evento(s)</td>
        <td><code>

Acceso restringido: necesitas permisos de administrador para gestionar esta agenda. Iniciar sesión

</code></td> <td><code>

</code></td> <td> <form method="POST" style="display:inline;" onsubmit="return confirm('¿Estás seguro de eliminar esta instancia completa y todos sus eventos?');"> <?php wp_nonce_field( 'indaga_agenda_admin_action', 'indaga_agenda_nonce' ); ?> <input type="hidden" name="accion" value="eliminar_instancia"> <input type="hidden" name="grupo_id" value="<?php echo esc_attr( $inst->grupo_id ); ?>"> <button type="submit" class="button button-small button-link-delete">Eliminar Instancia</button> </form> </td> </tr> <?php endforeach; ?>

Con esto queda cubierto todo el funcionamiento del panel de administración y el control multinstancia desde el backend.

En la siguiente entrega abordaremos la tercera parte del cuaderno técnico: los Shortcodes de Frontend (indaga_agenda y calendario_agenda) (entre corchetes), analizando el formulario de alta de eventos y el calendario mensual interactivo programado en JavaScript nativo.

Hasta pronto!

Cuaderno Técnico: Plugin «Agenda Indaga» (v1.3.0), parte I

Valencia, 21/10/2026, G.B.
Empiezo una serie de cuadernos técnicos para explicar paso a paso los plugins de Indaga.net. Es una forma práctica de documentar el proceso, compartir el aprendizaje y, de paso, ver de un vistazo qué se puede mejorar o dónde falló algo en su día. Aquí va la primera parte del plugin Agenda Indaga.

Bloque 1: Cabecera, Seguridad y Estructura de la Clase Principal

Todo buen plugin de WordPress empieza con una cabecera limpia y un pequeño control de seguridad para evitar que nadie acceda al archivo directamente desde el navegador.

1. Cabecera del Plugin e Identificación

En las primeras líneas definimos los metadatos que lee WordPress en el listado de plugins. Destaca el uso del dominio de texto (indaga-agenda) y la especificación de autoría de Indaga.net:

<?php
/**
* Plugin Name: Agenda Indaga
* Plugin URI: https://indaga.net
* Description: Plugin nativo de agenda y eventos con soporte multinstancia avanzada...
* Version: 1.3.0
* Author: Guillermo Beltrán Pilato (Indaga.net)
* Author URI: https://indaga.net
* License: GPLv2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: indaga-agenda
*/

2. Bloqueo de Acceso Directo

La seguridad en WordPress empieza por proteger los archivos individuales frente a peticiones HTTP directas que no pasen por el núcleo del CMS. Si la constante global ABSPATH no está definida, abortamos la ejecución de inmediato:

if ( ! defined( 'ABSPATH' ) ) {
exit; // Seguridad: Evitar acceso directo
}

3. Patrón Singleton (IndagaAgenda)

Para evitar duplicidad de instancias y mantener el código ordenado y eficiente en memoria, implementamos el patrón Singleton. Esto garantiza que la clase solo se cargue una vez:

class IndagaAgenda {
private static $instance = null;

public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}

private function __construct() {
// Aquí engancharemos nuestros hooks de activación, menús y shortcodes
register_activation_hook( __FILE__, array( $this, 'activar_plugin' ) );
add_action( 'admin_menu', array( $this, 'registrar_menu_admin' ) );
add_shortcode( 'indaga_agenda', array( $this, 'renderizar_shortcode_agenda' ) );
add_shortcode( 'calendario_agenda', array( $this, 'renderizar_shortcode_calendario' ) );
}
}

// Inicializamos el plugin
IndagaAgenda::get_instance();

Bloque 2: Creación Nativa de la Base de Datos (activar_plugin)

Una de las premisas clave de los plugins en Indaga.net es cero dependencias externas: toda la información reside en nuestras propias tablas optimizadas dentro de la base de datos de WordPress.

Cuando el usuario activa el plugin por primera vez, se dispara el gancho (register_activation_hook) que ejecuta el siguiente método:

public function activar_plugin() {
global $wpdb;
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );

$tabla = $wpdb->prefix . 'indaga_agenda';
$charset_collate = $wpdb->get_charset_collate();

$sql = "CREATE TABLE $tabla (
id mediumint(9) NOT NULL AUTO_INCREMENT,
user_id bigint(20) NOT NULL,
grupo_id varchar(50) DEFAULT 'default' NOT NULL,
titulo varchar(255) NOT NULL,
descripcion text NOT NULL,
fecha_inicio datetime NOT NULL,
fecha_fin datetime NOT NULL,
categoria varchar(50) DEFAULT 'general' NOT NULL,
observaciones text DEFAULT '',
PRIMARY KEY (id),
KEY user_id (user_id),
KEY grupo_id (grupo_id),
KEY fecha_inicio (fecha_inicio)
) $charset_collate;";

dbDelta( $sql );
update_option( 'indaga_agenda_version', '1.3.0' );
}

¿Qué ocurre paso a paso en este método?

  1. global $wpdb;: Cargamos la clase global de WordPress para interactuar de forma segura con la base de datos.

  2. $wpdb->prefix: Añadimos el prefijo personalizado de las tablas de tu instalación de WordPress (por defecto solía ser wp_, resultando en wp_indaga_agenda), garantizando compatibilidad con cualquier entorno.

  3. dbDelta( $sql );: Es la función nativa recomendada de WordPress. Compara si la tabla ya existe o si necesita actualizar su esquema sin destruir los datos ya almacenados.

  4. Campos clave (grupo_id): Este campo es el corazón del soporte multinstancia avanzada, ya que nos permite filtrar y agrupar múltiples agendas independientes dentro de una misma tabla.

Continuaré…