Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/jpbarbatic/webapp/llms.txt

Use this file to discover all available pages before exploring further.

El panel de administración separa completamente la lógica PHP de la presentación HTML. Los fragmentos de interfaz que se repiten en varios módulos —la paginación de tablas, el modal de confirmación de borrado, los filtros de búsqueda y la barra de navegación— se implementan como archivos de plantilla independientes que los controladores y las vistas incluyen mediante require o include. Esta arquitectura evita duplicar marcado HTML y garantiza que cualquier ajuste visual o de comportamiento se propague a todos los módulos desde un único punto.

Paginación — html/paginacion.html.php

El componente de paginación renderiza una barra de navegación por páginas usando las clases pagination de Bootstrap 5. Solo se muestra cuando el número total de páginas es mayor que uno. Código fuente completo:
<?php $num_paginas = ceil($total / $items_pagina); ?>
<?php if ($num_paginas > 1): ?>
    <ul class="pagination pagination-sm">
        <li class="page-item<?= $p == 1 ? ' disabled' : '' ?>">
            <a class="page-link" href="<?= strstr($vista, '/', true) ?>/?p=<?= $p - 1 ?>" aria-label="Previous">
                <span aria-hidden="true">&laquo;</span>
            </a>
        </li>
        <?php for ($i = 1; $i <= $num_paginas; $i++): ?>
            <li class="page-item<?= $p == $i ? ' active' : '' ?>">
                <a class="page-link" href="<?= strstr($vista, '/', true) ?>/?p=<?= $i ?>"><?= $i ?></a>
            </li>
        <?php endfor; ?>
        <li class="page-item<?= $p == $num_paginas ? ' disabled' : '' ?>">
            <a class="page-link" href="<?= strstr($vista, '/', true) ?>/?p=<?= $p + 1 ?>" aria-label="Next">
                <span aria-hidden="true">&raquo;</span>
            </a>
        </li>
    </ul>
<?php endif; ?>
Variables que debe tener el ámbito de inclusión:
VariableTipoDescripción
$totalintNúmero total de registros que devolvería la consulta sin LIMIT. Normalmente proviene de extract($res) tras una llamada a db_select().
$pintNúmero de página actual. Proviene de extract(paginacion()).
$items_paginaintRegistros por página (valor fijo 10). Proviene de extract(paginacion()).
$vistastringRuta relativa del módulo actual (p. ej. 'productos/listado'). La función strstr($vista, '/', true) extrae el segmento antes de la primera barra para construir la URL base del módulo (productos/).
Comportamiento:
  • Calcula $num_paginas con ceil($total / $items_pagina).
  • Si $num_paginas <= 1 no renderiza nada.
  • El botón « queda deshabilitado (disabled) cuando $p == 1.
  • El botón » queda deshabilitado cuando $p == $num_paginas.
  • El número de página activa recibe la clase active.
  • Cada enlace construye la URL como {modulo}/?p={N}, preservando la navegación dentro del módulo actual.
Ejemplo completo de uso en un controlador:
// 1. Obtener parámetros de paginación desde $_GET
extract(paginacion());
// Variables disponibles: $p, $items_pagina, $offset, $orden, $orden_dir

// 2. Ejecutar la consulta paginada
$sql    = 'SELECT * FROM productos ORDER BY id LIMIT ? OFFSET ?';
$params = [$items_pagina, $offset];
$res    = db_select($db, $sql, $params, $items_pagina, $offset);
extract($res); // crea $total y $datos

// 3. Pasar todo a la plantilla (paginacion.html.php lo incluirá la vista)
require('../../html/paginacion.html.php');

Filtros de productos — html/productos/filtro.html.php

La barra de filtros es un formulario colapsable (Bootstrap Collapse) que permite buscar productos por nombre o identificador numérico y opcionalmente filtrar por categoría. Se incluye dentro de html/productos/listado.html.php. Código fuente completo:
<a class="btn btn-primary btn-sm"
   data-bs-toggle="collapse"
   href="#collapseExample"
   role="button"
   aria-expanded="false"
   aria-controls="collapseExample">
    Filtro
</a>

<div class="collapse<?= isset($_GET['filtro']) ? ' show' : '' ?> mt-3" id="collapseExample">
    <form action="">
        <div class="row">
            <div class="col-auto">
                <label class="col-form-label">Nombre/Id</label>
            </div>
            <div class="col-auto">
                <input class="form-control form-control-sm"
                       name="filtro[nombre]"
                       value="<?= isset($_GET['filtro']['nombre']) ? $_GET['filtro']['nombre'] : '' ?>">
            </div>
            <div class="col-auto">
                <select class="form-select form-select-sm" name="filtro[categoria]">
                    <option value=''>--Elige una categoría--</option>
                    <?= html_opciones(
                        $categorias,
                        isset($_GET['filtro']['categoria']) ? $_GET['filtro']['categoria'] : '',
                        'id',
                        'nombre'
                    ) ?>
                </select>
            </div>
            <div class="col-auto">
                <input class="btn btn-primary btn-sm" type="submit" value="Buscar">
            </div>
        </div>
    </form>
</div>
Campos del formulario:
CampoNombre HTTPDescripción
Texto librefiltro[nombre]Búsqueda por nombre de producto. El controlador del listado también soporta la sintaxis id:N para buscar directamente por identificador numérico.
Selector de categoríafiltro[categoria]Desplegable con todas las categorías disponibles, generado con html_opciones(). Seleccionar --Elige una categoría-- (valor vacío) elimina el filtro de categoría.
Funcionamiento:
  • El formulario usa method implícito GET (action=""), por lo que los filtros se envían como parámetros de la URL, lo que permite compartir o marcar como favorita una búsqueda concreta.
  • El panel colapsable se expande automáticamente si existe algún parámetro filtro en $_GET (isset($_GET['filtro'])), de modo que al volver a la página con un filtro activo el usuario vea los controles desplegados.
  • Requiere que la variable $categorias (array de filas de la tabla categorias) esté disponible en el ámbito de inclusión; el controlador de listado de productos la carga antes de incluir la vista.

El modal de borrado es un diálogo Bootstrap 5 con fondo rojo que solicita confirmación explícita antes de eliminar un registro. Se incluye dentro de cada plantilla de listado que soporte borrado, y el script que lo inicializa se incluye en la plantilla de listado correspondiente. Código fuente completo del modal:
<div class="modal fade" id="deleteModal" tabindex="-1"
     aria-labelledby="deleteModalLabel" aria-hidden="true">
    <div class="modal-dialog modal-dialog-centered">
        <form id="deleteForm" method="POST"
              action="<?= strstr($vista, '/', true) ?>/borrar.php">
            <div class="modal-content">
                <div class="modal-header bg-danger text-white">
                    <h5 class="modal-title" id="deleteModalLabel">
                        ⚠️ Confirmar eliminación
                    </h5>
                    <button type="button" class="btn-close btn-close-white"
                            data-bs-dismiss="modal" aria-label="Cerrar">
                    </button>
                </div>
                <div class="modal-body">
                    <p>¿Estás seguro de que deseas eliminar este registro?
                       <strong>Esta acción es irreversible.</strong>
                    </p>
                    <input type="hidden" name="id" id="modalRecordId">
                    <input type="hidden" name="csrf_token"
                           value="<?= htmlspecialchars($_SESSION['csrf_token'] ?? '') ?>">
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary"
                            data-bs-dismiss="modal">Cancelar</button>
                    <button type="submit" class="btn btn-danger" id="btnConfirmDelete">
                        <span class="spinner-border spinner-border-sm d-none"
                              id="btnSpinner"></span>
                        Sí, eliminar
                    </button>
                </div>
            </div>
        </form>
    </div>
</div>
Elementos clave del modal:
ElementoID / AtributoPropósito
Contenedor del modalid="deleteModal"Target de los botones de eliminación en la tabla (data-bs-target="#deleteModal").
Formularioid="deleteForm"Envía POST a {modulo}/borrar.php. La URL del módulo se construye con strstr($vista, '/', true).
Campo oculto de IDid="modalRecordId", name="id"Se rellena vía JavaScript con el ID del registro a borrar justo antes de que el modal se muestre.
Token CSRFname="csrf_token"Protege contra ataques CSRF. Toma su valor de $_SESSION['csrf_token'].
Botón de confirmaciónid="btnConfirmDelete"El script lo deshabilita tras el primer clic para evitar envíos dobles.
Spinner de cargaid="btnSpinner"Oculto por defecto (clase d-none). El script lo muestra al enviar el formulario.
Botón disparador en la tabla de listado:
<button type="button"
        class="btn btn-danger btn-sm"
        data-bs-toggle="modal"
        data-bs-target="#deleteModal"
        data-id="<?= (int)$producto['id'] ?>">
    <i class="bi bi-trash"></i> Eliminar
</button>
El atributo data-id transporta el identificador numérico del registro. Bootstrap lo pasa como event.relatedTarget al evento show.bs.modal, donde el JavaScript lo lee y lo escribe en #modalRecordId. Script de inicialización del modal (patrón compartido entre módulos): Cada módulo que usa el modal incluye su propio archivo listado.script.php con el siguiente bloque JavaScript. El patrón es idéntico en categorias, productos y usuarios:
document.addEventListener('DOMContentLoaded', () => {
  const deleteModal    = document.getElementById('deleteModal');
  const modalRecordId  = document.getElementById('modalRecordId');
  const deleteForm     = document.getElementById('deleteForm');
  const btnConfirm     = document.getElementById('btnConfirmDelete');
  const btnSpinner     = document.getElementById('btnSpinner');

  // 1. Cuando el modal se abre, captura el ID del botón que lo activó
  deleteModal.addEventListener('show.bs.modal', (event) => {
    const button   = event.relatedTarget; // Botón clickeado en la tabla
    const recordId = button.getAttribute('data-id');
    modalRecordId.value = recordId;       // Inyecta el ID en el campo oculto
  });

  // 2. Al enviar el formulario, mostrar loading y evitar doble clic
  deleteForm.addEventListener('submit', () => {
    btnConfirm.disabled = true;
    btnSpinner.classList.remove('d-none');
  });
});
Flujo completo de una eliminación:
1

Clic en «Eliminar»

El usuario hace clic en el botón de la tabla. Bootstrap dispara el evento show.bs.modal antes de mostrar el diálogo.
2

Inyección del ID

El listener de show.bs.modal lee event.relatedTarget.getAttribute('data-id') y asigna ese valor al campo oculto #modalRecordId.
3

Confirmación

El usuario lee el mensaje de advertencia y hace clic en «Sí, eliminar». El botón se deshabilita y aparece el spinner para indicar que la operación está en curso.
4

POST a borrar.php

El formulario envía id y csrf_token por POST al endpoint {modulo}/borrar.php, que valida el token, ejecuta el DELETE en la base de datos y redirige de vuelta al listado con un mensaje de confirmación en sesión.

Scripts compartidos — html/comun.script.php

El archivo html/comun.script.php es incluido por html/plantilla.html.php en todas las páginas del panel, justo antes de cargar el script específico del módulo activo. En la versión actual del proyecto el archivo está vacío, reservado para futuros scripts de utilidad transversal (p. ej. inicialización global de tooltips Bootstrap, interceptores de formularios o helpers de notificación). La plantilla lo incluye de forma incondicional:
<script src="assets/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<?php include __DIR__ . '/comun.script.php'; ?>
<?php if (file_exists(__DIR__ . '/' . $vista . '.script.php')) {
    include __DIR__ . '/' . $vista . '.script.php';
} ?>
Este orden garantiza que Bootstrap esté disponible cuando se ejecute cualquier script de módulo, y que comun.script.php pueda definir funciones o variables globales accesibles desde los scripts de módulo que se cargan a continuación.

Barra de navegación — html/nav.html.php

La barra de navegación es una <nav> Bootstrap fija en la parte superior (fixed-top bg-dark). Se incluye automáticamente en todas las páginas a través de html/plantilla.html.php. Código fuente completo:
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="#"><?= NOMBRE_WEB ?></a>
        <button class="navbar-toggler" type="button"
                data-bs-toggle="collapse"
                data-bs-target="#navbarCollapse"
                aria-controls="navbarCollapse"
                aria-expanded="false"
                aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarCollapse">
            <ul class="navbar-nav me-auto mb-2 mb-md-0">
                <li class="nav-item">
                    <a class="nav-link<?= str_starts_with($vista, 'dashboard') ? ' active' : '' ?>"
                       aria-current="page" href="#">Dashboard</a>
                </li>
                <?php if (es_visible('usuarios')): ?>
                <li class="nav-item">
                    <a class="nav-link<?= str_starts_with($vista, 'usuarios') ? ' active' : '' ?>"
                       href="usuarios/">Usuarios</a>
                </li>
                <?php endif; ?>
                <li class="nav-item">
                    <a class="nav-link<?= str_starts_with($vista, 'roles') ? ' active' : '' ?>"
                       href="roles/">Roles</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link<?= str_starts_with($vista, 'productos') ? ' active' : '' ?>"
                       href="productos/">Productos</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link<?= str_starts_with($vista, 'categorias') ? ' active' : '' ?>"
                       href="categorias/">Categorías</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="logout.php">Salir</a>
                </li>
            </ul>
        </div>
    </div>
</nav>
Variables y constantes utilizadas:
IdentificadorTipoDescripción
NOMBRE_WEBConstanteNombre de la aplicación definido en config.php. Se muestra como la marca de la navbar.
$vistaVariableRuta relativa del módulo activo (p. ej. 'productos/listado'). Se compara con str_starts_with() para añadir la clase active al enlace del módulo que está en uso.
Visibilidad de los enlaces de navegación:
EnlaceURLCondición de visibilidad
Dashboard#Siempre visible.
Usuariosusuarios/Solo si es_visible('usuarios') devuelve un resultado no vacío (el rol tiene al menos un permiso con prefijo usuarios.).
Rolesroles/Siempre visible.
Productosproductos/Siempre visible.
Categoríascategorias/Siempre visible.
Salirlogout.phpSiempre visible.
Solo el enlace Usuarios está protegido por es_visible() en el nivel de la barra de navegación. Los demás módulos son visibles para cualquier usuario autenticado en la interfaz, aunque el acceso real a sus controladores sigue estando protegido por el guard de backend.php mediante la variable $permiso.

El modal de borrado es completamente reutilizable. El archivo html/confirmacion.borrado.html.php es idéntico para todos los módulos CRUD: categorias, productos y usuarios lo incluyen sin modificación alguna al final de su respectivo listado.html.php. La URL de destino del formulario se resuelve dinámicamente con strstr($vista, '/', true), por lo que el mismo HTML apunta a categorias/borrar.php, productos/borrar.php o usuarios/borrar.php según el módulo que lo incluya. Para añadir el modal a un nuevo módulo basta con incluir el archivo de plantilla, añadir el atributo data-id a los botones de la tabla y copiar el bloque JavaScript de inicialización en el listado.script.php del nuevo módulo.

Build docs developers (and LLMs) love