Herramientas Informaticas

Categoría: CodeIgniter 4.5 Página 1 de 2

🔐 Control de accesos a Saldos por Almacén — Código y guía práctica

Entrada fija

Resumen: implementación limpia y los bloques de código bien acomodados para que en la vista de saldos solo se muestren los productos/lotes de los almacenes a los que el usuario tiene permiso.


🧾 1) Controller — Preparar empresas y almacenes del usuario

Este bloque es el handler principal (método index()). Se encarga de:

  • Recuperar empresas asociadas al usuario
  • Recuperar almacenes activos asignados al usuario
  • Construir el builder via mdlGetSaldos y preparar respuesta para DataTables (draw/records/paginación)
// Controller: index()
public function index() {
    helper('auth');

    // 1) Obtener usuario
    $idUser = user()->id;

    // 2) Empresas del usuario (fallback a [0] si no tiene)
    $titulos["empresas"] = $this->empresa->mdlEmpresasPorUsuario($idUser);
    $empresasID = count($titulos["empresas"]) === 0 ? [0] : array_column($titulos["empresas"], "id");

    // 3) Almacenes (storages) asignados al usuario y activos (status = 'on')
    $storagesUser = $this->storagesPerUser
                    ->where("idUsuario", $idUser)
                    ->where("status", "on")
                    ->asArray()
                    ->findAll();

    $storagesUser = count($storagesUser) === 0 ? [0] : array_column($storagesUser, "idStorage");

    // 4) Si es petición AJAX (DataTables) devolvemos JSON paginado
    if ($this->request->isAJAX()) {
        $request = service('request');

        $draw = (int) $request->getGet('draw');
        $start = (int) $request->getGet('start');
        $length = (int) $request->getGet('length');
        $searchValue = $request->getGet('search')['value'] ?? '';
        $orderColumnIndex = (int) ($request->getGet('order')[0]['column'] ?? 0);
        $orderDir = $request->getGet('order')[0]['dir'] ?? 'asc';

        // Mapeo de columnas (orden)
        $fields = [
            'id' => 'a.id',
            'nombreAlmacen' => 'c.name',
            'lote' => 'a.lote',
            'codigoProducto' => 'a.codigoProducto',
            'descripcion' => 'a.descripcion',
            'fullname' => 'e.fullname'
        ];
        $orderField = $fields[$orderColumnIndex] ?? 'id';

        // Builder desde el modelo con filtros de empresas y almacenes
        $builder = $this->saldos->mdlGetSaldos($empresasID, $storagesUser);

        // Conteo total (sin filtros de búsqueda)
        $total = clone $builder;
        $recordsTotal = $total->countAllResults(false);

        // Filtro de búsqueda global
        if (!empty($searchValue)) {
            $builder->groupStart();
            foreach ($fields as $field) {
                $builder->orLike($field, $searchValue);
            }
            $builder->groupEnd();
        }

        // Conteo filtrado
        $filteredBuilder = clone $builder;
        $recordsFiltered = $filteredBuilder->countAllResults(false);

        // Obtener página
        $data = $builder->orderBy("a." . $orderField, $orderDir)
                ->get($length, $start)
                ->getResultArray();

        // Respuesta JSON para DataTables
        return $this->response->setJSON([
            'draw' => $draw,
            'recordsTotal' => $recordsTotal,
            'recordsFiltered' => $recordsFiltered,
            'data' => $data,
        ]);
    }

    // Vista normal (no-AJAX)
    $titulos["title"] = "Info Productos";
    $titulos["subtitle"] = "Extrae la información de los productos por el código de barras";
    return view('julio101290\\boilerplateinventory\\Views\\saldos', $titulos);
}

🧱 2) Modelo — Builder con filtros por empresa y almacén

Este método devuelve un Query Builder ya filtrado por $idEmpresas y $storagesUser. Úsalo tal cual en el controller.

public function mdlGetSaldos($idEmpresas, $storagesUser) {
    return $this->db->table('saldos a')
        ->select("
            a.id,
            a.idEmpresa,
            a.idAlmacen,
            a.idProducto,
            a.codigoProducto,
            a.lote,
            a.descripcion,
            a.cantidad,
            a.created_at,
            a.deleted_at,
            a.updated_at,
            b.nombre AS nombreEmpresa,
            c.name AS nombreAlmacen,
            COALESCE(e.fullname, 'Sin asignar') AS fullname
        ")
        // JOINs para mostrar nombres legibles
        ->join('empresas b', 'a.idEmpresa = b.id')
        ->join('storages c', 'a.idAlmacen = c.id')
        // LEFT JOINs para evitar romper si no hay relación
        ->join('productsemployes pe', 'pe.idProduct = a.id', 'left')
        ->join('employes e', 'e.id = pe.idEmploye', 'left')
        // filtros de permisos: solo empresas/almacenes permitidos
        ->whereIn('a.idEmpresa', $idEmpresas)
        ->whereIn('a.idAlmacen', $storagesUser)
        ->orderBy('a.id', 'DESC');
}

🛠️ 3) Notas técnicas y buenas prácticas aplicadas

  • Back-end es la fuente de verdad: los arrays de IDs ($empresasID y $storagesUser) se construyen en el servidor a partir de user()->id. Nunca confíes en listas enviadas por cliente.
  • Fallback seguro: usar [0] cuando no hay empresas/almacenes evita que whereIn reciba un array vacío y genere errores SQL. En ese caso la consulta devuelve resultados vacíos.
  • LEFT JOINs: mantuvimos LEFT JOIN en relaciones opcionales para que la consulta no falle si no hay datos relacionados (por ejemplo, empleado no asignado).
  • Clonar builder: clonar el builder para conteo (countAllResults(false)) mantiene el flujo de DataTables sin re-ejecutar joins innecesarios.

🔍 4) Sugerencias de índices SQL (para rendimiento)

Recomiendo agregar índices (si no existen) para acelerar filtros y joins:

-- Índices recomendados
CREATE INDEX idx_saldos_idEmpresa ON saldos (idEmpresa);
CREATE INDEX idx_saldos_idAlmacen ON saldos (idAlmacen);
CREATE INDEX idx_saldos_codigoProducto ON saldos (codigoProducto);
CREATE INDEX idx_saldos_lote ON saldos (lote);
-- Índices en tablas relacionadas
CREATE INDEX idx_storages_id ON storages (id);
CREATE INDEX idx_empresas_id ON empresas (id);

Si usas PostgreSQL, considera índices compuestos o índices GIN si aplicarás búsquedas textuales complejas.


🧪 5) Pruebas recomendadas (QA) — pasos concretos

  1. Usuario sin almacenes: Inicia sesión con un usuario sin almacenes asignados. La tabla debe venir vacía. Ver el mensaje UX (ver sección UX abajo).
  2. Usuario con 1 almacén: Inicia sesión con acceso a un solo almacén; la vista debe mostrar únicamente los saldos de ese almacén.
  3. Usuario con múltiples almacenes: Comprueba que aparecen filas de cualquiera de esos almacenes, y que no aparecen filas de almacenes no asignados.
  4. Busqueda global: Ejecuta una búsqueda por codigoProducto, lote o descripcion y valida que los resultados respetan el filtro por almacén.
  5. Paginación y orden: Revisa recordsTotal y recordsFiltered cuando aplicas orden y búsqueda; deben reflejar correctamente la cantidad total y la cantidad filtrada.
  6. Seguridad: Intenta manipular parámetros GET/POST desde el cliente (por ejemplo, forzar otro idAlmacen) y verifica que no se muestran saldos si el usuario no tiene permiso.

💬 6) Mensajes UX sugeridos

Si el usuario no tiene almacenes asignados, muestra un mensaje amigable y accionable en la UI (evita pantalla en blanco):

<div class="alert alert-info">
  <strong>Sin almacén asignado</strong><br>
  No tienes almacenes asignados. Contacta al administrador para que te asigne los permisos necesarios.
</div>

🔐 7) Seguridad y consideraciones adicionales

  • Recalcula permisos siempre en servidor: no aceptes listas desde cliente.
  • Validar status: al desactivar un permiso (status != ‘on’), asegúrate que en la siguiente petición el usuario deje de ver los datos correspondientes.
  • Auditoría: opcionalmente loguea consultas sensibles (quién vio qué y cuándo) para trazabilidad.

📦 8) Release & commit (referencia)

Los cambios fueron incluidos en el release v1.2.3 y el commit con la implementación es este:

Release v1.2.3Commit d8ad77ad

Repositorio: https://github.com/julio101290/boilerplateInventory


📋 9) Checklist de despliegue

  • ✅ Probar en staging con usuarios de distintos permisos
  • ✅ Verificar índices y tiempos de respuesta
  • ✅ Añadir pruebas unitarias/integración que verifiquen permisos
  • ✅ Revisar logs de auditoría tras deploy

🚀 Corrección completa del control de lotes en inventario: evita duplicados, valida subcategorías y mejora la experiencia de usuario

Entrada fija

Resumen rápido: En este artículo revisamos y documentamos una corrección integral para un problema común en sistemas de inventario: la generación de lotes duplicados al agregar productos rápidamente vía AJAX. Incluye la solución frontend con un contador en memoria, la validación backend de subcategoría, manejo de errores con Toast, ejemplos de código listos para copiar y todo explicado paso a paso. Además se referencia el commit donde se subieron los cambios.


🧭 Introducción — ¿qué problema resolvimos y por qué importa?

En aplicaciones web que manejan inventario, es frecuente tener un flujo donde el usuario busca un producto y presiona Agregar varias veces. Si ese evento dispara una petición AJAX al servidor para obtener el último lote disponible y la respuesta se procesa en paralelo, existe una ventana de inconsistencia: varias respuestas pueden leer el mismo estado inicial y generar el mismo consecutivo, produciendo lotes duplicados (por ejemplo LMPLMLAPTOP000001 repetido varias veces). Esto provoca problemas reales: registros duplicados en inventario, conflictos de seguimiento, problemas legales en trazabilidad, y mala experiencia de usuario.

La meta fue sencilla pero crítica: evitar duplicados y hacer el flujo robusto sin reescribir toda la arquitectura. Priorizamos una solución práctica, compatible con jQuery y CodeIgniter/PHP, que puedas aplicar hoy en tu proyecto.


🔎 Diagnóstico técnico — por qué ocurrieron los duplicados

El error tiene tres factores principales:

  1. Asincronía: las peticiones AJAX regresan en tiempos distintos.
  2. Dependencia del DOM: el código calculaba el siguiente consecutivo consultando elementos del DOM que todavía no reflejaban las inserciones hechas por otras respuestas.
  3. Falta de validación del backend: el backend asumía que existían datos (por ejemplo, la subcategoría), lo que podía llevar a lotes mal formados o a errores silenciosos.

La combinación de esos puntos genera la race condition: varias respuestas ven el mismo último consecutivo y todas calculan el mismo siguiente valor.


🛠️ Solución aplicada — enfoque general

Optamos por una solución híbrida y pragmática:

  • Frontend: mantener un contador en memoria (lotesContador) que almacene, por cada loteBase, el último consecutivo usado en la sesión. Al recibir la respuesta del backend, sincronizamos ese contador con el consecutivo que trae el servidor y, si corresponde, lo incrementamos localmente para el siguiente lote. Esto evita depender del estado del DOM y evita conflictos cuando el usuario hace clicks rápidos.
  • Backend: agregar validaciones tempranas para condiciones críticas (por ejemplo: que el producto tenga subcategoría). Si falta información, el servidor devuelve una respuesta JSON con error: true y un message descriptivo.
  • UX: usar toasts para informar errores (icono de error), y evitar mensajes ambiguos o iconos de éxito cuando ocurrió un fallo.

💡 ¿Por qué esta solución es práctica?

Porque:

  • No requiere reestructurar la base de datos ni añadir infraestructura adicional.
  • Es fácil de integrar en proyectos existentes con jQuery + backend PHP/CodeIgniter.
  • Reduce la ventana de inconsistencias en la interfaz del usuario y evita la mayoría de los duplicados en escenarios de click rápido.
  • Permite seguir usando al backend como fuente de verdad cuando hay cambios externos: si el backend reporta un consecutivo mayor al local, el frontend se sincroniza.

🔧 Código: backend (CodeIgniter) — validar subcategoría y devolver JSON de error

Este fragmento se inserta en el controller que calcula el lote (método calculateLot() o similar). La idea es validar que exista idSubCategoria en la fila del producto y que la subcategoría realmente exista en la tabla de subcategorías. Si falla, devolvemos un JSON con error.

// Validar que tenga subcategoría
if (empty($productData["idSubCategoria"])) {
    return $this->response->setJSON([
        "error" => true,
        "message" => "El producto no tiene subcategoría asignada"
    ]);
}

// Buscar subcategoría
$subCategoryData = $this->subCategory->select("*")
        ->where("id", $productData["idSubCategoria"])
        ->first();

// Validar que exista
if (!$subCategoryData) {
    return $this->response->setJSON([
        "error" => true,
        "message" => "Subcategoría no encontrada"
    ]);
}

$keyCategory = $subCategoryData["descripcion"];
// ... continuar con la construcción de $baseLot, búsqueda del último lote y generación del consecutivo ...

Con esto, si alguien intenta calcular un lote para un producto sin subcategoría, el frontend recibirá un JSON con error y se podrá mostrar un mensaje al usuario en lugar de producirse un lote inválido o un fallo más profundo.


🖥️ Código: frontend (jQuery) — patrón robusto para decidir loteFinal

A continuación está el patrón que implementamos en el success del AJAX. Debes declarar var lotesContador = {}; en un scope compartido (por ejemplo, al inicio de tu archivo JS o dentro de tu $(function(){...})), y luego usar esta lógica al recibir la respuesta del servidor para calcular el loteFinal.

/* Declarar al inicio del script */
var lotesContador = {};

/* Dentro del success del AJAX */
if (respuesta.error) {
    Toast.fire({
        icon: 'error',
        title: respuesta.message
    });
    return;
}

var loteCalculado = (respuesta && respuesta.lot) ? respuesta.lot : (lote || "UNKNOWN000001");

if (!loteCalculado) {
    console.error("No se pudo determinar el lote calculado");
    return;
}

var loteBase = loteCalculado.slice(0, -6);  // asume 6 dígitos de consecutivo
var consecutivoBackend = parseInt(loteCalculado.slice(-6), 10);
if (isNaN(consecutivoBackend)) consecutivoBackend = 0;

var loteFinal;

if (!lotesContador.hasOwnProperty(loteBase)) {
    // Primera vez: inicializar con lo que trae el backend
    lotesContador[loteBase] = consecutivoBackend;
    loteFinal = loteCalculado;
} else {
    // Ya hay un contador local
    if (consecutivoBackend > lotesContador[loteBase]) {
        // Backend avanzó por fuera de esta sesión: sincronizamos
        lotesContador[loteBase] = consecutivoBackend;
        loteFinal = loteCalculado;
    } else {
        // Usamos el siguiente consecutivo en memoria
        lotesContador[loteBase] = lotesContador[loteBase] + 1;
        var nuevo = String(lotesContador[loteBase]).padStart(6, "0");
        loteFinal = loteBase + nuevo;
    }
}

/* Llamar a la función que agrega el renglón con loteFinal */
agregarRenglon( idProduct, codeProduct, loteFinal, description, salePrice,
    porcentTax, porcentIVARetenido, porcentISRRetenido,
    claveUnidadSAT, unidad, claveProductoSAT );

Este flujo asegura que:

  • Si el backend ya avanzó el consecutivo en otra sesión, el frontend se sincroniza.
  • Si no, el frontend incrementa localmente el consecutivo para evitar duplicados cuando se hagan clicks rápidos.

📋 Función agregarRenglon — ejemplo práctico

Aquí tienes una versión robusta de tu función que construye el HTML del renglón y normaliza valores numéricos para evitar NaN o inputs inválidos:

function agregarRenglon(idProduct, codeProduct, lote, description, salePrice,
    porcentTax, porcentIVARetenido, porcentISRRetenido,
    claveUnidadSAT, unidad, claveProductoSAT) {

    salePrice = Number(salePrice) || 0;
    porcentTax = Number(porcentTax) || 0;
    porcentIVARetenido = Number(porcentIVARetenido) || 0;
    porcentISRRetenido = Number(porcentISRRetenido) || 0;

    var tax = (porcentTax > 0) ? ((porcentTax * 0.01) * salePrice) : 0;
    var IVARetenido = (porcentIVARetenido > 0) ? ((porcentIVARetenido * 0.01) * salePrice) : 0;
    var ISRRetenido = (porcentISRRetenido > 0) ? ((porcentISRRetenido * 0.01) * salePrice) : 0;
    var neto = (((porcentTax * 0.01) + 1) * salePrice) - (IVARetenido + ISRRetenido);

    var renglon = "<div class='form-group row nuevoProduct'>";
    renglon += "<div class='col-1'>";
    renglon += "<button type='button' class='btn btn-danger quitProduct'><span class='far fa-trash-alt'></span></button>";
    renglon += " <button type='button' data-toggle='modal' data-target='#modelMoreInfoRow' class='btn btn-primary btnInfo'><span class='fa fa-fw fa-pencil-alt'></span></button> ";
    renglon += "<input type='hidden' class='idProductR' name='idProductR' value='" + (idProduct || "") + "'>";
    renglon += "</div>";
    renglon += "<div class='col-1'>";
    renglon += "<input type='hidden' class='claveProductoSATR' name='claveProductoSATR' value='" + (claveProductoSAT || "") + "'>";
    renglon += "<input type='hidden' class='claveUnidadSatR' name='claveUnidadSatR' value='" + (claveUnidadSAT || "") + "'>";
    renglon += "<input type='hidden' class='unidad' name='unidad' value='" + (unidad || "") + "'>";
    renglon += "<input type='text' class='form-control codeProduct' name='codeProduct' value='" + (codeProduct || "") + "'> </div>";
    renglon += "<div class='col-1'> <input type='text' class='form-control lote' name='lote' value='" + (lote || "") + "' required> </div>";
    renglon += "<div class='col-6'> <input type='text' class='form-control description' name='description' value='" + (description || "") + "' required> </div>";
    renglon += "<div class='col-1'> <input type='number' class='form-control cant' name='cant' value='1' required>";
    renglon += "<input type='hidden' class='porcentIVARetenido' name='porcentIVARetenido' value='" + porcentIVARetenido + "'>";
    renglon += "<input type='hidden' class='porcentISRRetenido' name='porcentISRRetenido' value='" + porcentISRRetenido + "'>";
    renglon += "<input type='hidden' class='porcentTax' name='porcentTax' value='" + porcentTax + "'></div>";
    renglon += "<div class='col-1'> <input type='number' class='form-control price' name='price' value='" + salePrice + "' required>";
    renglon += "<input type='hidden' class='IVARetenido' name='IVARetenido' value='" + IVARetenido + "'>";
    renglon += "<input type='hidden' class='ISRRetenido' name='ISRRetenido' value='" + ISRRetenido + "'>";
    renglon += "<input type='hidden' class='tax' name='tax' value='" + tax + "'> </div>";
    renglon += "<div class='col-1'> <input readonly type='number' class='form-control total' name='total' value='" + salePrice + "'>";
    renglon += "<input type='hidden' class='neto' name='neto' value='" + neto + "'> </div>";
    renglon += "</div>";

    $('.rowProducts').append(renglon);

    if (typeof listProducts === "function") listProducts();
}

✅ Manejo de errores y UX — toasts y mensajes claros

Una buena UX evita frustración. Cuando el backend devuelve error: true, mostramos un toast con icono de error y el mensaje específico:

if (respuesta.error) {
    Toast.fire({
        icon: 'error',
        title: respuesta.message
    });
    return;
}

Observa dos puntos importantes:

  • No uses icon: 'success' para errores.
  • Mantén los mensajes del backend claros y útiles para el usuario (por ejemplo: “El producto no tiene subcategoría asignada”).

🧪 Pruebas que debes ejecutar (y por qué)

Antes de subir a producción, ejecuta una batería de pruebas que validen los casos reales:

  1. Clicks rápidos: simula spam click y verifica que los lotes generados en el DOM sean secuenciales y no repetidos.
  2. Producto sin subcategoría: intenta agregar un producto con idSubCategoria vacío o inexistente y verifica que aparezca el toast con el mensaje correcto y que no se agregue el renglón.
  3. Multiusuario: abre dos sesiones diferentes (o dos navegadores) y agrega productos con la misma loteBase para comprobar la sincronización con el backend.
  4. Eliminar renglón: borra renglones y agrega nuevos; el contador en memoria no se decrementa (esto evita reuso accidental de consecutivos), verifica que los nuevos lotes aumenten correctamente.
  5. Prueba de estrés: con un pequeño script o con herramientas como k6 o ab, simula múltiples peticiones al endpoint getLastLot y verifica que el backend responda correctamente y que el frontend se comporte como esperado.

⚠️ Consideraciones y limitaciones de la solución

La solución de contador en memoria es práctica y resuelve el problema en la mayoría de los casos, pero existen condiciones donde se requiere mayor robustez:

  • Escalabilidad y clusters distribuidos: si tu aplicación corre en múltiples servidores y hay concurrente masiva, el contador en memoria por sesión no garantiza unicidad global. En ese caso, considera un servicio centralizado (Redis, tabla con contador atómico) para emitir consecutivos atómicos.
  • Transacciones críticas: si la generación del lote debe ser 100% atómica con la inserción del registro en inventario, la lógica debe moverse al backend con locking o transacción que reserve el siguiente número y lo persista en la misma operación.
  • Reutilización de números: en esta solución NO se reutilizan consecutivos al borrar renglones. Reusar números es peligroso porque puede llevar a duplicados e inconsistencias en auditoría. Si requieres reuso, debe implementarse cuidadosamente en el backend y con auditoría.

🔁 Alternativas y mejoras (sin la sección “Siguiente paso”)

Aquí describimos varias alternativas que puedes implementar según tu nivel de exigencia:

1) Redis / contador centralizado

Usar INCR en Redis para obtener el siguiente consecutivo de forma atómica. Ideal para arquitecturas distribuidas. Ventajas: simple y rápido. Inconveniente: requiere infra adicional.

2) Tabla en BD con fila de contador y SELECT FOR UPDATE

Usar una tabla con el contador y reservar el número con locking a nivel de transacción. Ventajas: no necesitas nueva infra; Inconveniente: puede ser más lento bajo alta concurrencia y requiere diseño cuidadoso para evitar contención.

3) Generación optimista y reconciliación

Generas un lote provisional en frontend y al persistir en backend verificas si está disponible. Si no, el backend retorna el lote correcto y el frontend actualiza el renglón. Esto requiere un flujo UX para reconciliación (indicar “lote pendiente” y luego “confirmado”).


📦 Registro de cambios — commit en tu repositorio

Los cambios relacionados con esta corrección se subieron al repositorio en el commit referenciado a continuación. Revisa el diff para ver exactamente qué archivos y líneas se modificaron.

Commit con los cambios: :contentReference[oaicite:0]{index=0}


📌 Checklist completo para despliegue (rápido)

  • ✅ Aplicar validación de idSubCategoria en backend.
  • ✅ Devolver error:true y message si falta subcategoría.
  • ✅ Implementar lotesContador en frontend y sincronizar con backend.
  • ✅ Mostrar Toast con icon: 'error' cuando haya problemas.
  • ✅ Ejecutar pruebas de clicks rápidos y multiusuario.
  • ✅ Instrumentar logs en backend para cada lote generado (auditoría).
  • ✅ No decrementar el contador local al borrar renglones.
  • ✅ Revisar la necesidad de un contador global (Redis/BD) según volumen de concurrencia.

🔍 Ejemplos de mensajes útiles para el backend

  • El producto no tiene subcategoría asignada — cuando idSubCategoria está vacío.
  • Subcategoría no encontrada — cuando el ID no existe en la tabla de subcategorías.
  • No se pudo obtener el último lote — cuando la consulta a la tabla de saldos falla.

Mensajes claros hacen más fácil el debugging y mejoran la experiencia del desarrollador y del usuario final.


🧾 Buenas prácticas y recomendaciones finales

  1. Fail fast: valida temprano en backend y devuelve errores claros.
  2. Fuente de verdad: el backend siempre es la fuente de verdad; el frontend optimiza UX y reduce latencia.
  3. Auditoría: registra cada vez que se asigna un lote para trazar operaciones en caso de discrepancias.
  4. Pruebas automáticas: escribe pruebas e2e que cubran clicks rápidos y multiusuario.
  5. Documentación: documenta en el README del repo el comportamiento del contador en memoria y las condiciones que requieren un contador centralizado.

🔚 Conclusión

Resolver la duplicación de lotes en un sistema de inventario es crítico para mantener la integridad de los datos y la confianza del usuario. La estrategia que aplicamos —validación temprana en backend y un contador en memoria sincronizado en frontend— ofrece una solución práctica, rápida de implementar y efectiva para la gran mayoría de escenarios. Para entornos de alta concurrencia o arquitecturas distribuidas, existe la opción de aumentar la robustez con contadores atómicos centralizados (Redis, BD con locking).

Si quieres que convierta esta guía en un artículo formateado para tu tema de WordPress (por ejemplo con estilos específicos o bloques personalizados), o que genere una versión en Markdown para tu repo, lo preparo. Solo pégalo en un bloque HTML personalizado y debería quedar correcto en Gutenberg.

Artículo generado para facilitar la integración de correcciones en el módulo de inventario. Copia y pega en tu editor de WordPress (bloque HTML personalizado) y listo. ✅

🚀 Nueva Funcionalidad: Asignación de Productos Internos a Empleados con Control ON/OFF (CodeIgniter 4 + DataTables)

Entrada fija

Hoy quiero compartir una mejora importante implementada en uno de mis módulos de mantenimiento interno: la asignación dinámica de productos internos a empleados, completamente integrada con CodeIgniter 4, DataTables Server-Side y una arquitectura escalable basada en controladores limpios y métodos reutilizables.

Este cambio simplifica el control operativo dentro del sistema y habilita una administración más clara y eficiente entre productos y personal. Aquí te cuento cómo quedó y por qué la solución es tan efectiva.


🎯 ¿Cuál era el objetivo?

Empresas que manejan productos internos suelen tener un problema común:
¿Qué empleados tienen asignado qué producto?

Antes la gestión se hacía de forma manual, lo cual era lento, propenso a errores y nada práctico.

La meta era:

  • ✔ Mostrar todos los empleados
  • ✔ Señalar si tienen o no asignado un producto (ON/OFF)
  • ✔ Permitir activar o desactivar la asignación con un clic
  • ✔ Mantener todo sincronizado con DataTables server-side
  • ✔ Evitar duplicados y mantener la base limpia

🧩 La solución implementada

1. Nueva tabla pivote: productsEmployes

Registra las asignaciones entre productos y empleados.

id | idProduct | idEmploye | created_at | updated_at | deleted_at

Una tabla limpia, auditada y optimizada para relaciones N:N.


2. Endpoint server-side para DataTables

El método ctrDatatableProductsEmployes() hace lo siguiente:

  • 🔹 Recibe el idProduct
  • 🔹 Obtiene automáticamente el idEmpresa del producto
  • 🔹 Lista empleados que pertenecen a esa empresa
  • 🔹 Calcula si cada empleado tiene asignado el producto (ON/OFF)
  • 🔹 Soporta búsqueda, orden y paginación
  • 🔹 Devuelve JSON compatible con DataTables

💡 Resultado: un DataTable rápido, preciso y escalable.


3. Endpoint para alternar asignación ON/OFF

Con el método toggleEmployeProduct() se controla la asignación:

  • ✔ Si la asignación no existe → se inserta
  • ✔ Si ya existe → se elimina
  • ✔ Respuesta JSON simple y rápida

Todo esto sin recargar página, con una UX fluida y sin duplicados.


4. Interfaz visual con botones ON/OFF

En la vista se implementaron botones:

  • 🟢 ON en verde
  • 🔴 OFF en rojo

Cada clic es enviado por AJAX y la tabla se actualiza automáticamente.


🔥 ¿Qué beneficios aporta?

  • ✔ Velocidad operativa: Asignaciones con un solo clic.
  • ✔ Cero duplicados: El sistema valida antes de insertar o eliminar.
  • ✔ Escalable: Lista para ampliarse sin cambiar la base.
  • ✔ UX intuitiva: Interfaz clara y visual.
  • ✔ Integración CI4: Arquitectura limpia y mantenible.

🌟 Conclusión

Este módulo transforma la manera de gestionar productos internos en las empresas. Lo más importante: queda construido sobre una arquitectura clara, modular y fácil de extender, ideal para cualquier sistema administrativo o ERP desarrollado en PHP/CodeIgniter.

Si deseas ver el código completo, la arquitectura o adaptarlo a tu proyecto, puedo ayudarte a integrarlo o extenderlo.

Cómo Sobrescribir o Extender Clases en CodeIgniter 4.5: Caso Práctico con DataTables

Entrada fija

En muchos proyectos de desarrollo web con CodeIgniter 4, nos encontramos con la necesidad de modificar o extender clases de terceros sin alterar el código fuente original. Esto es especialmente común al trabajar con bibliotecas como Hermawan\DataTables, ampliamente utilizada para integrar DataTables con Eloquent y Query Builder.

Uno de los escenarios más comunes es sobrescribir la clase DataTableColumnDefs, ya sea para agregar soporte adicional (como compatibilidad con PostgreSQL) o personalizar cómo se gestionan las columnas y filtros de búsqueda.

En este artículo te mostraré cómo sobrescribir o extender esta clase correctamente en CodeIgniter 4.5, sin comprometer la mantenibilidad de tu proyecto.


🔍 ¿Por qué deberías sobrescribir una clase?

Modificar directamente los archivos de una librería externa puede parecer una solución rápida, pero es una mala práctica. Estas son algunas razones para evitarlo:

  • ❌ Pierdes la capacidad de actualizar el paquete con Composer.
  • ❌ Rompes el principio de separación de responsabilidades.
  • ✅ Extender o sobrescribir desde tu propia aplicación es más limpio y seguro.

🧱 Ejemplo: DataTableColumnDefs

La clase Hermawan\DataTables\DataTableColumnDefs es responsable de definir las columnas que serán procesadas por la tabla, sus alias, si son ordenables, buscables, etc.

Supongamos que quieres modificar el método getSearchable() para agregar compatibilidad con PostgreSQL (por ejemplo, usando CAST a texto y comillas dobles).


✅ Opción 1: Extender la clase original

Paso 1: Crea una nueva clase en app/Libraries

<?php

namespace App\Libraries;

use Hermawan\DataTables\DataTableColumnDefs;

class MyDataTableColumnDefs extends DataTableColumnDefs
{
    public function getSearchable()
    {
        // Aquí tu versión modificada del método original
        return parent::getSearchable(); // Puedes personalizarla como desees
    }
}

Paso 2: Usa tu clase en lugar de la original

use App\Libraries\MyDataTableColumnDefs;

$dt = new MyDataTableColumnDefs($builder);

🔁 Puedes sobrescribir cualquier otro método, como add, remove, o initFromBuilder.


✅ Opción 2: Reemplazar completamente la clase original

⚠️ Solo se recomienda si estás seguro de que no actualizarás el paquete regularmente.

Paso 1: Copia la clase original a app/Libraries/DataTableColumnDefs.php

Paso 2: Cambia el namespace

use App\Libraries\MyDataTableColumnDefs;

$dt = new MyDataTableColumnDefs($builder);

Paso 3: Modifica lo que necesites

Paso 4: Usa tu versión

use App\Libraries\MyDataTableColumnDefs;

$dt = new MyDataTableColumnDefs($builder);

✅ Opción 3: Sobrescribir vía Composer Autoload

Esta es una opción elegante y avanzada si necesitas que tu clase se cargue en lugar de la original sin cambiar el nombre.

Paso 1: Edita composer.json

"autoload": {
    "psr-4": {
        "Hermawan\\DataTables\\": "app/Overrides/DataTables/"
    }
}

Paso 2: Crea el archivo sobrescrito

app/Overrides/DataTables/DataTableColumnDefs.php

Paso 3: Copia el contenido original, modifica lo necesario

Paso 4: Ejecuta:

composer dump-autoload

✨ ¡Listo! CodeIgniter usará tu versión personalizada sin modificar el paquete original.


🎯 Conclusión

Sobrescribir o extender clases en CodeIgniter 4.5 te da la flexibilidad de adaptar librerías a tus necesidades sin sacrificar actualizaciones futuras o buenas prácticas de desarrollo. Ya sea para añadir soporte a PostgreSQL, cambiar la lógica de filtrado o mejorar la compatibilidad con tus modelos, hacerlo correctamente asegura que tu aplicación sea más escalable y mantenible.

¿Qué Base de Datos Elegir? Comparativa de MariaDB, PostgreSQL y SQL Server

Entrada fija
  • Si buscas una solución gratuita, potente, extensible y con una gran comunidad, ideal para una amplia variedad de aplicaciones (web, móviles, empresariales) y si no dependes fuertemente del ecosistema Microsoft: PostgreSQL es una excelente opción. Su robustez y características avanzadas lo hacen muy versátil.
  • Si necesitas una solución gratuita, con buen rendimiento y alta compatibilidad con MySQL, especialmente si ya tienes experiencia con MySQL o planeas una migración sencilla: MariaDB es una alternativa sólida.
  • Si tu infraestructura ya está fuertemente basada en productos Microsoft, necesitas una integración perfecta con ellos, un amplio conjunto de herramientas empresariales y soporte comercial robusto, y el presupuesto no es la principal limitante: SQL Server sería la opción más adecuada.

En resumen:

  • PostgreSQL: Versátil, potente, de código abierto, ideal para diversas aplicaciones.
  • MariaDB: Gratuita, buen rendimiento, compatible con MySQL.
  • SQL Server: Óptimo para entornos Microsoft, con amplias funcionalidades comerciales.

Para tomar la mejor decisión, te recomiendo considerar los siguientes factores específicos para tu situación en Los Mochis:

  • Requisitos de tu aplicación: ¿Qué tipo de datos manejarás? ¿Qué tipo de consultas realizarás? ¿Necesitas características específicas como geodatos?
  • Tamaño y escalabilidad esperada: ¿Cuánto crecerá tu base de datos y tu aplicación?
  • Experiencia de tu equipo: ¿Con qué base de datos están más familiarizados tus desarrolladores y administradores?
  • Presupuesto: ¿Puedes asumir los costos de licencia de SQL Server?
  • Infraestructura existente: ¿Qué sistemas operativos utilizas? ¿Necesitas una integración estrecha con otras herramientas?
  • Soporte local: ¿Hay experiencia y soporte técnico disponible en Los Mochis para alguna de estas bases de datos en particular?

Considerando que trabajarás con CodeIgniter 4, tanto MariaDB como PostgreSQL son excelentes opciones y muy populares en la comunidad de PHP y CodeIgniter. SQL Server también puede funcionar bien, pero podría tener algunas consideraciones adicionales.

MariaDB con CodeIgniter 4:

  • Ventajas:
    • Compatibilidad Directa: CodeIgniter 4 tiene un excelente soporte para MySQL, y dado que MariaDB es altamente compatible, la configuración y el uso serán muy sencillos.
    • Rendimiento: Puede ofrecer un buen rendimiento para aplicaciones web desarrolladas con CodeIgniter 4.
    • Facilidad de Uso: Muchos desarrolladores PHP están familiarizados con MySQL/MariaDB, lo que podría facilitar el desarrollo y la administración.
    • Código Abierto y Gratuito: Sin costos de licencia, lo cual es atractivo para muchos proyectos.
    • Comunidad: Amplia comunidad de usuarios de PHP y MySQL/MariaDB.
  • Desventajas:
    • Menos Características Avanzadas: Comparado con PostgreSQL, podría carecer de algunas características más avanzadas si tu aplicación en el futuro las necesitara (tipos de datos más complejos, extensiones como PostGIS, etc.).

PostgreSQL con CodeIgniter 4:

  • Ventajas:
    • Características Potentes: Ofrece características avanzadas que podrían ser útiles para aplicaciones más complejas (tipos de datos JSON, arrays, funciones avanzadas, etc.).
    • Integridad de Datos: Conocido por su robustez y cumplimiento de los estándares SQL, lo que puede contribuir a una mayor integridad de los datos.
    • Extensiones: La capacidad de extender su funcionalidad con extensiones como PostGIS para datos geoespaciales es una gran ventaja si tu aplicación lo requiere.
    • Rendimiento Sólido: Funciona muy bien con aplicaciones web y puede manejar grandes volúmenes de datos y consultas complejas de manera eficiente.
    • Comunidad: Una comunidad fuerte y activa dentro del mundo de PHP y PostgreSQL.
  • Desventajas:
    • Curva de Aprendizaje Ligeramente Mayor: Si no estás familiarizado con PostgreSQL, podría haber una pequeña curva de aprendizaje en comparación con MySQL/MariaDB.
    • Configuración Inicial: La configuración inicial podría tener algunos pasos ligeramente diferentes en comparación con MySQL/MariaDB.

SQL Server con CodeIgniter 4:

  • Ventajas:
    • Potencia y Funcionalidades: Ofrece un conjunto robusto de características empresariales.
    • Integración (si usas Windows Server): Si tu servidor corre en Windows Server, la integración podría ser más sencilla en algunos aspectos.
  • Desventajas:
    • Costo de Licencia: El costo de las licencias puede ser un factor limitante.
    • Configuración Adicional: La configuración para que CodeIgniter 4 se conecte a SQL Server podría requerir algunos pasos adicionales y la instalación de drivers específicos.
    • Menor Popularidad en el Mundo PHP: Aunque funciona, no es tan comúnmente utilizado con PHP como MariaDB o PostgreSQL, por lo que la comunidad y los ejemplos específicos para CodeIgniter 4 podrían ser menores.

¿Cuál es la mejor opción para CodeIgniter 4 ?

Considerando que se usara CodeIgniter 4, tanto MariaDB como PostgreSQL son excelentes opciones.

  • Si buscas simplicidad, familiaridad (si vienes de MySQL), buen rendimiento para aplicaciones web típicas y una configuración sencilla con CodeIgniter 4, MariaDB es una opción muy sólida y popular.
  • Si anticipas que tu aplicación podría necesitar características más avanzadas en el futuro, valoras la integridad de los datos, o quieres tener la flexibilidad de usar extensiones potentes como PostGIS, PostgreSQL es una opción fantástica y cada vez más popular en la comunidad PHP.

Mi recomendación general para CodeIgniter 4 sería:

  • Si no tienes necesidades muy específicas y buscas una solución probada y sencilla, elige MariaDB. Es muy probable que cumpla con todos tus requisitos para la mayoría de las aplicaciones web.
  • Si prevés que tu aplicación crecerá en complejidad o necesitará características más avanzadas, o si simplemente prefieres la potencia y las características de PostgreSQL, entonces esta sería una excelente elección. CodeIgniter 4 tiene un buen soporte para PostgreSQL.

SQL Server podría ser una buena opción si ya tienes una fuerte inversión en el ecosistema Microsoft y esa es la base de datos estándar en tu entorno. Sin embargo, para un proyecto nuevo con CodeIgniter 4, MariaDB o PostgreSQL suelen ser opciones más comunes y con una integración más directa en el mundo del desarrollo PHP.

En resumen, para CodeIgniter 4, te recomendaría inclinarte por MariaDB por su simplicidad y compatibilidad directa, o por PostgreSQL si anticipas necesidades más avanzadas en el futuro. Ambas son excelentes bases de datos y funcionarán muy bien con el framework. ¡La elección final dependerá de tus requisitos específicos!

CodeIgniter 4 Boilerplate Ubicaciones ( Carta Porte)

Entrada fija
Latest Stable Version
Total Downloads
Latest Unstable Version
License

   

image

CodeIgniter 4 Boilerplate Ubicaciones ( Carta Porte)

Biblioteca a ubicaciones, requerida para carta portuaria (Carta Porte) CFDI 4.0

Requerimientos

  • PhpCfdi\SatCatalogos
  • julio101290/boilerplatelog
  • hermawan/codeigniter4-datatables

Instalación

Ejecutar Comandos

composer require phpcfdi/sat-catalogos

composer require hermawan/codeigniter4-datatables
	
composer require julio101290/boilerplatelog

composer require julio101290/boilerplatecompanies

composer require julio101290/boilerplatestorages

composer require julio101290/boilerplatetypesmovement

composer require julio101290/boilerplatequotes

composer require julio101290/boilerplatecomprobanterd

composer require julio101290/boilerplatesells

composer require julio101290/boilerplatelocations

Ejecutar Comandos de migración de las tablas y sembrado de los datos

php spark boilerplatecompanies:installcompaniescrud

php spark boilerplatelog:installlog

php spark boilerplatestorages:installstorages

php spark boilerplatetypesmovement:installtypesmovement

php spark boilerplatequotes:installquotes

php spark boilerplatecomprobanterd:installcomprobanterd

php spark boilerplatequotes:installsells

php spark boilerplatelocations:installlocations

Ejemplo para crear el Menú

image

Listo

image

Usage

You can find how it works with the read code routes, controller and views etc. Finnally… Happy Coding!

Changelog

Please see CHANGELOG for more information what has changed recently.

Contributing

Contributions are very welcome.

License

This package is free software distributed under the terms of the MIT license.

CodeIgniter 4 Boilerplate CFDI Descarga Masiva

Entrada fija
Latest Stable Version
Total Downloads
Latest Unstable Version
License

   

image

CodeIgniter 4 Boilerplate CFDI Descarga Masiva

Biblioteca para la Descarga Masiva de Facturas Electrónicas CFDI del SAT, solo requiere Certificado Fiel, Clave y Contraseña.

Requerimientos

  • PhpCfdi\SatCatalogos
  • julio101290/boilerplatelog
  • hermawan/codeigniter4-datatables
  • phpcfdi/cfditopdf
  • phpcfdi/cfdi-to-json”
  • phpcfdi/xml-cancelacion

Instalación

Ejecutar Comandos

composer require phpcfdi/sat-catalogos

composer require hermawan/codeigniter4-datatables

composer require julio101290/boilerplatelog

composer require julio101290/boilerplatecompanies

composer require julio101290/boilerplatestorages

composer require julio101290/boilerplatetypesmovement

composer require julio101290/boilerplatequotes

composer require julio101290/boilerplatecfdidescargamasiva

Ejecutar comando de migración y sembrado

php spark boilerplatecompanies:installcompaniescrud

php spark boilerplatelog:installlog

php spark boilerplatestorages:installstorages

php spark boilerplatetypesmovement:installtypesmovement

php spark boilerplatequotes:installquotes

Creando el menu, Ejemplo

image

Listo

image

Usage

You can find how it works with the read code routes, controller and views etc. Finnally… Happy Coding!

Changelog

Please see CHANGELOG for more information what has changed recently.

Contributing

Contributions are very welcome.

License

This package is free software distributed under the terms of the MIT license.

CodeIgniter 4 Boilerplate Arqueo de Caja

Entrada fija
Latest Stable Version
Total Downloads
Latest Unstable Version
License

   

CodeIgniter 4 Boilerplate Arqueo de caja

CodeIgniter4 Boilerplate Arqueo de caja, este modulo servira para abrir y cerrar las cajas en una fecha determinada.

El funcionamiento es que al abrir la caja todas las ventas realizadas se ingresaran a ese arqueo, el cual va a generar un reporte de cuales ventas son a credito y cuales a contado.

Al registrar el arqueo de caja capturamos cuando hay en caja actualmente y cuanto hay en el reconteo de efectivo

Requerimientos

  • PhpCfdi\SatCatalogos
  • julio101290/boilerplatelog
  • hermawan/codeigniter4-datatables

Instalación

Ejecutar los siguientes comandos

composer require phpcfdi/sat-catalogos

composer require hermawan/codeigniter4-datatables

composer require julio101290/boilerplatelog

composer require julio101290/boilerplatecompanies

composer require julio101290/boilerplatestorages

composer require julio101290/boilerplatetypesmovement

composer require julio101290/boilerplatequotes

composer require julio101290/boilerplatecashtonnage

Ejecutar los comandos para la migración de tablas y sembrado de datos

php spark boilerplatecompanies:installcompaniescrud

php spark boilerplatelog:installlog

php spark boilerplatestorages:installstorages

php spark boilerplatetypesmovement:installtypesmovement

php spark boilerplatequotes:installquotes

php spark boilerplatecashtonnage:installcashtonnage

Creamos el menú, Como se ve en la imagen

image

Listo

image
image
image

Usage

You can find how it works with the read code routes, controller and views etc. Finnally… Happy Coding!

Changelog

Please see CHANGELOG for more information what has changed recently.

Contributing

Contributions are very welcome.

License

This package is free software distributed under the terms of the MIT license.

CodeIgniter 4 Boilerplate Catalogo de Vehiculos

Entrada fija
Latest Stable Version
Total Downloads
Latest Unstable Version
License

   

CodeIgniter 4 Boilerplate Catalogo de Vehiculos

CodeIgniter4 Boilerplatevehicles CRUD MVC tipo captura Vehículo y vehículos para facturas y Carta Mexicana Porte y módulo de taller

Requerimientos

  • PhpCfdi\SatCatalogos
  • julio101290/boilerplatelog
  • hermawan/codeigniter4-datatables

Instalación

Ejecutar Comandos

composer require phpcfdi/sat-catalogos

composer require hermawan/codeigniter4-datatables

composer require julio101290/boilerplatelog

composer require julio101290/boilerplatecompanies

composer require julio101290/boilerplatestorages

composer require julio101290/boilerplatetypesmovement

composer require julio101290/boilerplatevehicles

Ejecutar comandos para la migración y sembrado

php spark boilerplatecompanies:installcompaniescrud

php spark boilerplatelog:installlog

php spark boilerplatestorages:installstorages

php spark boilerplatetypesmovement:installtypesmovement

php spark boilerplatevehicles:installvehicles

Creamos el menú para el tipo de vehiculo, Ejemplo

image

Creamos el menú para el vehiculo, ejemplo

image

Listo

image
image

Usage

You can find how it works with the read code routes, controller and views etc. Finnally… Happy Coding!

Changelog

Please see CHANGELOG for more information what has changed recently.

Contributing

Contributions are very welcome.

License

This package is free software distributed under the terms of the MIT license.

CodeIgniter 4 Boilerplate Almacenes

Entrada fija
Latest Stable Version
Total Downloads
Latest Unstable Version
License

   

CodeIgniter 4 Boilerplate Almacenes

CodeIgniter4 Boilerplatecompanies CRUD MVC contiene almacenamientos de captura para inventario, con los campos Compañía, Código, Nombre, Operación inicial, Tipo, sucursal, etc.

Requerimientos

  • PhpCfdi\SatCatalogos
  • julio101290/boilerplatelog
  • hermawan/codeigniter4-datatables

Instalación

Ejecutar Comandos

composer require phpcfdi/sat-catalogos

composer require hermawan/codeigniter4-datatables

composer require julio101290/boilerplatelog

composer require julio101290/boilerplatecompanies

composer require julio101290/boilerplatestorages

Ejecutar Comandos para los archivos de migración y sembrado

php spark boilerplatecompanies:installcompaniescrud

php spark boilerplatelog:installlog

php spark boilerplatestorages:installstorages

Creando el Menu

image

Listo

image
image

Usage

You can find how it works with the read code routes, controller and views etc. Finnally… Happy Coding!

Changelog

Please see CHANGELOG for more information what has changed recently.

Contributing

Contributions are very welcome.

License

This package is free software distributed under the terms of the MIT license.

Página 1 de 2

Creado con WordPress & Tema de Anders Norén