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