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
mdlGetSaldosy 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 (
$empresasIDy$storagesUser) se construyen en el servidor a partir deuser()->id. Nunca confíes en listas enviadas por cliente. - Fallback seguro: usar
[0]cuando no hay empresas/almacenes evita quewhereInreciba 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
- 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).
- 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.
- Usuario con múltiples almacenes: Comprueba que aparecen filas de cualquiera de esos almacenes, y que no aparecen filas de almacenes no asignados.
- Busqueda global: Ejecuta una búsqueda por
codigoProducto,loteodescripciony valida que los resultados respetan el filtro por almacén. - Paginación y orden: Revisa
recordsTotalyrecordsFilteredcuando aplicas orden y búsqueda; deben reflejar correctamente la cantidad total y la cantidad filtrada. - 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.3 • Commit 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
Deja un comentario