// /home/Autopartes/pos/static/js/inventory.js // Inventory management UI: CRUD, purchases, adjustments, transfers, physical count, alerts (function () { 'use strict'; const API = '/pos/api/inventory'; const token = localStorage.getItem('pos_token'); if (!token) { window.location.href = '/pos/login'; return; } const headers = { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' }; let currentPage = 1; let currentSearch = ''; let draftCountId = null; // --- API helper --- async function apiFetch(url, opts) { const resp = await fetch(url, Object.assign({ headers: headers }, opts || {})); if (resp.status === 401) { localStorage.removeItem('pos_token'); window.location.href = '/pos/login'; return null; } return resp.json(); } // --- Tab switching --- document.querySelectorAll('.tab').forEach(function (tab) { tab.addEventListener('click', function () { document.querySelectorAll('.tab').forEach(function (t) { t.classList.remove('active'); }); document.querySelectorAll('.tab-content').forEach(function (c) { c.classList.remove('active'); }); tab.classList.add('active'); document.getElementById('tab-' + tab.dataset.tab).classList.add('active'); if (tab.dataset.tab === 'alerts') loadAlerts(); }); }); // --- Products --- async function loadItems(page, search) { currentPage = page || 1; currentSearch = search !== undefined ? search : currentSearch; var params = new URLSearchParams({ page: currentPage, per_page: 50 }); if (currentSearch) params.set('q', currentSearch); var data = await apiFetch(API + '/items?' + params.toString()); if (!data) return; var tbody = document.getElementById('productTableBody'); var items = data.data || []; if (!items.length) { tbody.innerHTML = 'Sin productos'; return; } tbody.innerHTML = items.map(function (it) { return '' + '' + esc(it.barcode) + '' + '' + esc(it.part_number) + '' + '' + esc(it.name) + '' + '' + esc(it.brand) + '' + '' + it.stock + '' + '$' + fmt(it.cost) + '' + '$' + fmt(it.price_1) + '' + '$' + fmt(it.price_2) + '' + '$' + fmt(it.price_3) + '' + '' + esc(it.location) + '' + ' ' + '' + ''; }).join(''); // Pagination var pg = data.pagination || {}; var pgEl = document.getElementById('productPagination'); if (pg.total_pages > 1) { pgEl.innerHTML = '' + '' + pg.page + ' / ' + pg.total_pages + ' (' + pg.total + ' items)' + ''; } else { pgEl.innerHTML = '' + (pg.total || 0) + ' productos'; } } // Search var searchInput = document.getElementById('productSearch'); var searchTimeout; searchInput.addEventListener('input', function () { clearTimeout(searchTimeout); searchTimeout = setTimeout(function () { loadItems(1, searchInput.value.trim()); }, 350); }); // --- Create item --- async function createItem() { var data = { part_number: document.getElementById('newPartNumber').value.trim(), name: document.getElementById('newName').value.trim(), brand: document.getElementById('newBrand').value.trim(), barcode: document.getElementById('newBarcode').value.trim() || undefined, cost: parseFloat(document.getElementById('newCost').value) || 0, price_1: parseFloat(document.getElementById('newPrice1').value) || 0, price_2: parseFloat(document.getElementById('newPrice2').value) || 0, price_3: parseFloat(document.getElementById('newPrice3').value) || 0, min_stock: parseInt(document.getElementById('newMinStock').value) || 0, initial_stock: parseInt(document.getElementById('newInitialStock').value) || 0, location: document.getElementById('newLocation').value.trim() }; if (!data.part_number || !data.name) { document.getElementById('createResult').innerHTML = 'Numero de parte y nombre son obligatorios'; return; } var result = await apiFetch(API + '/items', { method: 'POST', body: JSON.stringify(data) }); if (result && result.id) { document.getElementById('createResult').innerHTML = 'Creado ID ' + result.id + ' | Barcode: ' + result.barcode + ''; loadItems(currentPage); } else { document.getElementById('createResult').innerHTML = '' + (result ? result.error || 'Error' : 'Error de red') + ''; } } // --- Purchase --- async function recordPurchase() { var data = { inventory_id: parseInt(document.getElementById('purchaseItemId').value), quantity: parseInt(document.getElementById('purchaseQty').value), unit_cost: parseFloat(document.getElementById('purchaseCost').value), supplier_invoice: document.getElementById('purchaseInvoice').value.trim(), notes: document.getElementById('purchaseNotes').value.trim() }; if (!data.inventory_id || !data.quantity || !data.unit_cost) { document.getElementById('purchaseResult').innerHTML = 'Complete todos los campos'; return; } var result = await apiFetch(API + '/purchase', { method: 'POST', body: JSON.stringify(data) }); document.getElementById('purchaseResult').innerHTML = result && result.operation_id ? 'Compra registrada (op #' + result.operation_id + ')' : '' + (result ? result.error || 'Error' : 'Error de red') + ''; } // --- Adjustment --- async function recordAdjustment() { var data = { inventory_id: parseInt(document.getElementById('adjustItemId').value), quantity: parseInt(document.getElementById('adjustQty').value), reason: document.getElementById('adjustReason').value.trim() }; if (!data.inventory_id || data.quantity === undefined || !data.reason) { document.getElementById('adjustResult').innerHTML = 'Complete todos los campos (razon obligatoria)'; return; } var result = await apiFetch(API + '/adjustment', { method: 'POST', body: JSON.stringify(data) }); document.getElementById('adjustResult').innerHTML = result && result.operation_id ? 'Ajuste registrado (op #' + result.operation_id + ')' : '' + (result ? result.error || 'Error' : 'Error de red') + ''; } // --- Transfer --- async function recordTransfer() { var data = { inventory_id: parseInt(document.getElementById('transferItemId').value), from_branch_id: parseInt(document.getElementById('transferFrom').value), to_branch_id: parseInt(document.getElementById('transferTo').value), quantity: parseInt(document.getElementById('transferQty').value), notes: document.getElementById('transferNotes').value.trim() }; if (!data.inventory_id || !data.from_branch_id || !data.to_branch_id || !data.quantity) { document.getElementById('transferResult').innerHTML = 'Complete todos los campos'; return; } var result = await apiFetch(API + '/transfer', { method: 'POST', body: JSON.stringify(data) }); document.getElementById('transferResult').innerHTML = result && result.out_operation_id ? 'Transferencia registrada' : '' + (result ? result.error || 'Error' : 'Error de red') + ''; } // --- Physical Count (two-phase) --- function addCountLine() { var container = document.getElementById('countLines'); var row = document.createElement('div'); row.className = 'count-row'; row.innerHTML = '' + '' + ''; container.appendChild(row); } async function startPhysicalCount() { var rows = document.querySelectorAll('.count-row'); var items = []; rows.forEach(function (row) { var invId = parseInt(row.querySelector('.count-inv-id').value); var qty = parseInt(row.querySelector('.count-qty').value); if (invId && !isNaN(qty)) items.push({ inventory_id: invId, counted_quantity: qty }); }); if (!items.length) { alert('Agregue al menos una linea'); return; } var result = await apiFetch(API + '/physical-count/start', { method: 'POST', body: JSON.stringify({ items: items }) }); if (!result || !result.count_id) { document.getElementById('countResults').innerHTML = '' + (result ? result.error || 'Error' : 'Error de red') + ''; return; } draftCountId = result.count_id; var html = '

Borrador #' + result.count_id + ' — ' + result.message + '

'; html += ''; (result.results || []).forEach(function (r) { var color = r.difference === 0 ? '#16a34a' : (r.difference < 0 ? '#dc2626' : '#ca8a04'); html += ''; }); html += '
IDEsperadoContadoDiferencia
' + r.inventory_id + '' + r.expected + '' + r.counted + '' + (r.difference > 0 ? '+' : '') + r.difference + '
'; html += ''; html += ' '; document.getElementById('countResults').innerHTML = html; } async function approvePhysicalCount() { if (!draftCountId) { alert('No hay borrador activo'); return; } var result = await apiFetch(API + '/physical-count/approve', { method: 'POST', body: JSON.stringify({ count_id: draftCountId }) }); if (result && result.status === 'approved') { document.getElementById('countResults').innerHTML = '' + result.message + ''; draftCountId = null; } else { document.getElementById('countResults').innerHTML += '
' + (result ? result.error || 'Error' : 'Error de red') + ''; } } function cancelDraft() { draftCountId = null; document.getElementById('countResults').innerHTML = 'Borrador cancelado'; } // --- Alerts --- async function loadAlerts() { var data = await apiFetch(API + '/alerts'); if (!data) return; var el = document.getElementById('alertsList'); var alerts = data.data || []; if (!alerts.length) { el.innerHTML = '

Sin alertas activas

'; return; } el.innerHTML = alerts.map(function (a) { var cls = a.severity === 'critical' ? 'alert-critical' : (a.severity === 'warning' ? 'alert-warning' : 'alert-info'); var icon = a.type === 'zero' ? 'AGOTADO' : (a.type === 'low' ? 'BAJO' : 'EXCESO'); return '
' + '
[' + icon + '] ' + esc(a.part_number) + ' — ' + esc(a.name) + ' | Stock: ' + a.stock + (a.min_stock ? ' (min: ' + a.min_stock + ')' : '') + (a.max_stock ? ' (max: ' + a.max_stock + ')' : '') + '
' + 'Sucursal ' + a.branch_id + '
'; }).join(''); } // --- History modal --- async function viewHistory(itemId) { var data = await apiFetch(API + '/items/' + itemId + '/history'); if (!data) return; var history = data.data || []; var html = ''; if (!history.length) { html = '

Sin movimientos

'; } else { html = ''; history.forEach(function (h) { var qtyColor = h.quantity > 0 ? '#16a34a' : '#dc2626'; html += ''; }); html += '
FechaTipoCantidadCostoEmpleadoNotas
' + h.date + '' + h.type + '' + (h.quantity > 0 ? '+' : '') + h.quantity + '' + (h.cost ? '$' + fmt(h.cost) : '-') + '' + esc(h.employee) + '' + esc(h.notes) + '
'; } document.getElementById('historyContent').innerHTML = html; document.getElementById('historyModal').classList.add('show'); } function closeHistoryModal() { document.getElementById('historyModal').classList.remove('show'); } // --- Create modal --- function showCreateModal() { document.getElementById('createModal').classList.add('show'); } function closeCreateModal() { document.getElementById('createModal').classList.remove('show'); } // --- Barcode label --- function printBarcode(barcode, partNumber, name) { var w = window.open('', '_blank', 'width=400,height=250'); w.document.write('Etiqueta'); w.document.write('

' + barcode + '

'); w.document.write('

' + partNumber + '

'); w.document.write('

' + name + '

'); w.document.write(''); w.document.close(); w.print(); } // --- Helpers --- function fmt(n) { return (parseFloat(n) || 0).toFixed(2); } function esc(s) { if (!s) return ''; var d = document.createElement('div'); d.textContent = s; return d.innerHTML; } // --- Expose globals --- window._loadItems = function (p) { loadItems(p); }; window.viewHistory = viewHistory; window.closeHistoryModal = closeHistoryModal; window.showCreateModal = showCreateModal; window.closeCreateModal = closeCreateModal; window.createItem = createItem; window.recordPurchase = recordPurchase; window.recordAdjustment = recordAdjustment; window.recordTransfer = recordTransfer; window.addCountLine = addCountLine; window.startPhysicalCount = startPhysicalCount; window.approvePhysicalCount = approvePhysicalCount; window.cancelDraft = cancelDraft; window.loadAlerts = loadAlerts; window.printBarcode = printBarcode; // --- Init --- loadItems(1); })();