// /home/Autopartes/pos/static/js/inventory.js // Inventory management UI — rewritten to match design-system HTML structure // Panels: panel-stock, panel-entradas, panel-salidas, panel-traspasos, panel-ajustes, panel-conteos, panel-alertas (function () { 'use strict'; var API = '/pos/api/inventory'; function getToken() { return localStorage.getItem('pos_token') || ''; } if (!getToken()) { window.location.href = '/pos/login'; return; } function authHeaders() { return { 'Authorization': 'Bearer ' + getToken(), 'Content-Type': 'application/json' }; } var currentPage = 1; var currentSearch = ''; var draftCountId = null; var inventoryVS = null; var compatSource = 'both'; // default, loaded from config var inventorySearchController = null; var user = window.POS_USER || {}; var userRole = (user.role || '').toLowerCase(); var userPerms = user.permissions || []; var canEditPrices = userRole === 'owner' || userRole === 'admin' || userPerms.indexOf('config.edit_prices') !== -1; var canCreateItem = userRole === 'owner' || userRole === 'admin' || userRole === 'counter' || userRole === 'cashier' || userRole === 'warehouse' || userPerms.indexOf('inventory.create') !== -1; var canEditItem = userRole === 'owner' || userRole === 'admin' || userRole === 'warehouse' || userPerms.indexOf('inventory.edit') !== -1; var canImportItems = userRole === 'owner' || userRole === 'admin' || userRole === 'warehouse' || userPerms.indexOf('inventory.edit') !== -1; // Hide toolbar actions the user is not allowed to use (function applyInventoryPermissions() { var headerNew = document.getElementById('btnHeaderNewProduct'); var stockNew = document.getElementById('btnStockNewProduct'); var headerImport = document.getElementById('btnHeaderImport'); if (headerNew) headerNew.style.display = canCreateItem ? '' : 'none'; if (stockNew) stockNew.style.display = canCreateItem ? '' : 'none'; if (headerImport) headerImport.style.display = canImportItems ? '' : 'none'; })(); // Load compatibility source setting (function loadCompatSource() { fetch('/pos/api/config/vehicle-compat-source', { headers: { 'Authorization': 'Bearer ' + getToken() } }) .then(function(r) { return r.json(); }) .then(function(d) { if (d.source) compatSource = d.source; }).catch(function() {}); })(); // --- API helper --- function apiFetch(url, opts) { return fetch(url, Object.assign({ headers: authHeaders() }, opts || {})) .then(function (resp) { if (resp.status === 401) { localStorage.removeItem('pos_token'); window.location.href = '/pos/login'; return null; } return resp.json(); }); } // --- 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; } function formatDateTime(isoStr) { if (!isoStr) return '-'; var d = new Date(isoStr); if (isNaN(d.getTime())) return esc(isoStr); return d.toLocaleString('es-MX', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); } // --- Dashboard summary badges --- function loadSummary() { var skeletonHtml = '
'; ['inv-total-skus','inv-total-value','inv-low-stock','inv-no-movement'].forEach(function(id) { var el = document.getElementById(id); if (el) el.innerHTML = skeletonHtml; }); apiFetch(API + '/summary').then(function(data) { if (!data) return; var totalSkusEl = document.getElementById('inv-total-skus'); var totalValueEl = document.getElementById('inv-total-value'); var lowStockEl = document.getElementById('inv-low-stock'); var noMovementEl = document.getElementById('inv-no-movement'); if (totalSkusEl) totalSkusEl.textContent = (data.total_skus || 0).toLocaleString('es-MX'); if (totalValueEl) totalValueEl.textContent = '$' + (data.total_value || 0).toLocaleString('es-MX', {minimumFractionDigits: 2, maximumFractionDigits: 2}); if (lowStockEl) lowStockEl.textContent = (data.low_stock || 0).toLocaleString('es-MX'); if (noMovementEl) noMovementEl.textContent = (data.no_movement || 0).toLocaleString('es-MX'); }).catch(function(err) { console.error('Inventory summary load failed:', err); }); } loadSummary(); // --- Global tier discounts --- var globalDiscounts = { 2: 15, 3: 25 }; function loadTierDiscounts() { apiFetch(API + '/tier-discounts').then(function(data) { if (data && data.data) { data.data.forEach(function(d) { globalDiscounts[d.tier_id] = d.discount_pct; }); } var discEl = document.getElementById('tierDiscountBadge'); if (discEl) { discEl.textContent = 'Taller -' + globalDiscounts[2] + '% · Mayoreo -' + globalDiscounts[3] + '%'; } }); } loadTierDiscounts(); function showTierDiscountModal() { document.getElementById('tierDisc2').value = globalDiscounts[2]; document.getElementById('tierDisc3').value = globalDiscounts[3]; document.getElementById('tierDiscountModal').classList.add('is-open'); } function closeTierDiscountModal() { document.getElementById('tierDiscountModal').classList.remove('is-open'); } function saveTierDiscounts() { var d2 = parseFloat(document.getElementById('tierDisc2').value) || 0; var d3 = parseFloat(document.getElementById('tierDisc3').value) || 0; fetch(API + '/tier-discounts', { method: 'PUT', headers: { 'Authorization': 'Bearer ' + getToken(), 'Content-Type': 'application/json' }, body: JSON.stringify({ discount_pct_2: d2, discount_pct_3: d3 }) }).then(function(r) { return r.json(); }) .then(function(res) { showToast(res.message || 'Guardado', 'ok'); globalDiscounts[2] = d2; globalDiscounts[3] = d3; var discEl = document.getElementById('tierDiscountBadge'); if (discEl) { discEl.textContent = 'Taller -' + d2 + '% · Mayoreo -' + d3 + '%'; } closeTierDiscountModal(); }).catch(function() { showToast('Error al guardar descuentos', 'error'); }); } // Register Cmd+K items if (typeof registerCmdKItem === 'function') { registerCmdKItem({ group: 'Inventario', label: 'Ver stock', href: '/pos/inventory#stock', icon: '📦' }); registerCmdKItem({ group: 'Inventario', label: 'Alertas de stock', href: '/pos/inventory#alertas', icon: '⚠️' }); registerCmdKItem({ group: 'Inventario', label: 'Entradas de mercancía', href: '/pos/inventory#entradas', icon: '📥' }); registerCmdKItem({ group: 'Inventario', label: 'Salidas / Ventas', href: '/pos/inventory#salidas', icon: '📤' }); registerCmdKItem({ group: 'Inventario', label: 'Traspasos', href: '/pos/inventory#traspasos', icon: '🚚' }); registerCmdKItem({ group: 'Inventario', label: 'Ajustes', href: '/pos/inventory#ajustes', icon: '⚙️' }); registerCmdKItem({ group: 'Inventario', label: 'Conteos físicos', href: '/pos/inventory#conteos', icon: '🔢' }); } // Handle hash-based tab switching (e.g. /pos/inventory#alertas) (function handleHashTab() { var hash = window.location.hash.replace('#', ''); if (hash && ['stock', 'entradas', 'salidas', 'traspasos', 'ajustes', 'conteos', 'alertas'].indexOf(hash) !== -1) { setTimeout(function() { switchTab(hash); }, 100); } })(); // ===================================================================== // TAB SWITCHING — uses design-system switchTab() already in the HTML. // We hook into it to trigger data loads when tabs are activated. // ===================================================================== var _origSwitchTab = window.switchTab; window.switchTab = function (name) { if (typeof _origSwitchTab === 'function') _origSwitchTab(name); if (name === 'alertas') loadAlerts(); if (name === 'stock') loadItems(currentPage); }; // ===================================================================== // STOCK / PRODUCTS (panel-stock) // ===================================================================== var selectedItems = new Set(); function renderInventoryRow(it) { var isChecked = selectedItems.has(it.id) ? 'checked' : ''; return '' + '' + '' + it.id + '' + '' + 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) + '' + '' + ' ' + (canEditItem ? ' ' : '') + (canCreateItem ? ' ' : '') + (userPerms.indexOf('marketplace.manage') !== -1 || userRole === 'owner' || userRole === 'admin' ? ' ' : '') + ' ' + (canEditItem ? '' : '') + ''; } window.toggleItemSelection = function(id) { if (selectedItems.has(id)) { selectedItems.delete(id); } else { selectedItems.add(id); } updateSelectionUI(); }; window.toggleSelectAllItems = function() { var cb = document.getElementById('selectAllItems'); var allChecked = cb.checked; // We need to get all visible items from inventoryVS if (inventoryVS && inventoryVS.data) { inventoryVS.data.forEach(function(it) { if (allChecked) selectedItems.add(it.id); else selectedItems.delete(it.id); }); inventoryVS.refresh(); } updateSelectionUI(); }; function updateSelectionUI() { var count = selectedItems.size; var btn = document.getElementById('btnPublishML'); var badge = document.getElementById('meliSelectedCountBadge'); if (btn) btn.style.display = count > 0 ? 'inline-flex' : 'none'; if (badge) badge.textContent = count; // Update select-all checkbox state var selectAll = document.getElementById('selectAllItems'); if (selectAll && inventoryVS && inventoryVS.data) { var visibleIds = inventoryVS.data.map(function(it) { return it.id; }); var allSelected = visibleIds.length > 0 && visibleIds.every(function(id) { return selectedItems.has(id); }); selectAll.checked = allSelected; } } 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 tbody = document.getElementById('productTableBody'); if (tbody) tbody.innerHTML = renderSkeletonRows(12, 8); if (inventorySearchController) { inventorySearchController.abort(); } inventorySearchController = new AbortController(); apiFetch(API + '/items?' + params.toString(), { signal: inventorySearchController.signal }) .then(function (data) { inventorySearchController = null; if (!data) return; var items = data.data || []; if (!items.length) { tbody.innerHTML = '' + renderEmptyState({ icon: '', title: 'Sin productos', subtitle: currentSearch ? 'No se encontraron resultados para "' + esc(currentSearch) + '". Intenta con otro término.' : 'El inventario está vacío. Crea tu primer producto para empezar.', action: currentSearch ? '' : '' }) + ''; document.getElementById('productPagination').innerHTML = ''; return; } if (!inventoryVS) { inventoryVS = new VirtualScroll({ container: tbody, rowHeight: 48, buffer: 3, renderRow: renderInventoryRow, emptyHtml: '' + renderEmptyState({ title: 'Sin productos', subtitle: 'El inventario está vacío.' }) + '' }); } inventoryVS.setData(items); // Make columns resizable if (typeof makeTableResizable === 'function') { makeTableResizable('#stockTable'); } // Pagination var pg = data.pagination || {}; var pgEl = document.getElementById('productPagination'); if (pg.total_pages > 1) { pgEl.innerHTML = ''; } else { pgEl.innerHTML = '' + (pg.total || 0) + ' productos'; } }).catch(function (err) { if (err.name === 'AbortError') return; console.error('Inventory load error:', err); }); } // Search var searchInput = document.getElementById('productSearch'); var searchTimeout; if (searchInput) { searchInput.addEventListener('input', function () { clearTimeout(searchTimeout); searchTimeout = setTimeout(function () { loadItems(1, searchInput.value.trim()); }, 350); }); } // ===================================================================== // CREATE ITEM (createModal) // ===================================================================== function loadCategories(selectedId) { var sel = document.getElementById('newCategory'); if (!sel) return; apiFetch(API + '/categories/all').then(function(data) { if (!data || !data.categories) return; var cats = data.categories; var top = cats.filter(function(c) { return !c.parent_id; }); var subs = cats.filter(function(c) { return c.parent_id; }); sel.innerHTML = ''; top.forEach(function(c) { sel.innerHTML += '' + ''; subs.filter(function(s) { return s.parent_id === c.id; }).forEach(function(s) { sel.innerHTML += ''; }); sel.innerHTML += ''; }); }); } window.loadCategories = loadCategories; function onCategoryChange(categoryId) { // Kept for backwards compatibility; selector is now flat. } window.onCategoryChange = onCategoryChange; function resetCreateModal() { document.getElementById('editItemId').value = ''; document.getElementById('createModalTitle').textContent = 'Nuevo Producto'; document.getElementById('createModalBtn').textContent = 'Crear Producto'; var ids = ['newPartNumber','newName','newBrand','newBarcode','newSku2','newSku3','newUnit','newCost','newPrice1','newMinStock','newInitialStock','newMaxStock','newTaxRate','newLocation','newDescription']; ids.forEach(function(id) { var el = document.getElementById(id); if (el) el.value = ''; }); document.getElementById('newCategory').innerHTML = ''; document.getElementById('newIsActive').value = 'true'; document.getElementById('initialStockField').style.display = ''; document.querySelectorAll('#createModal .price-field input').forEach(function(el) { el.disabled = false; }); } function showCreateModal() { resetCreateModal(); document.getElementById('createModal').classList.add('is-open'); loadCategories(); // Attach AI classification on part number blur var pnInput = document.getElementById('newPartNumber'); if (pnInput && !pnInput._classifyBound) { pnInput._classifyBound = true; pnInput.addEventListener('blur', function () { var pn = this.value.trim(); if (pn.length < 3) return; var nameInput = document.getElementById('newName'); // Only auto-classify if name is still empty if (nameInput && nameInput.value.trim()) return; classifyPartNumber(pn); }); } } function showEditItemModal(itemId) { resetCreateModal(); document.getElementById('editItemId').value = itemId; document.getElementById('createModalTitle').textContent = 'Editar Producto'; document.getElementById('createModalBtn').textContent = 'Guardar Cambios'; document.getElementById('initialStockField').style.display = 'none'; document.getElementById('createModal').classList.add('is-open'); apiFetch(API + '/items/' + itemId).then(function(it) { if (!it) return; document.getElementById('newPartNumber').value = it.part_number || ''; document.getElementById('newName').value = it.name || ''; document.getElementById('newBrand').value = it.brand || ''; document.getElementById('newBarcode').value = it.barcode || ''; document.getElementById('newUnit').value = it.unit || ''; document.getElementById('newCost').value = it.cost != null ? it.cost : ''; document.getElementById('newPrice1').value = it.price_1 != null ? it.price_1 : ''; document.getElementById('newMinStock').value = it.min_stock != null ? it.min_stock : ''; document.getElementById('newMaxStock').value = it.max_stock != null ? it.max_stock : ''; document.getElementById('newTaxRate').value = it.tax_rate != null ? it.tax_rate : ''; document.getElementById('newLocation').value = it.location || ''; document.getElementById('newDescription').value = it.description || ''; document.getElementById('newIsActive').value = (it.is_active === false ? 'false' : 'true'); document.getElementById('newSku2').value = (it.sku_aliases && it.sku_aliases[0]) ? it.sku_aliases[0].sku : ''; document.getElementById('newSku3').value = (it.sku_aliases && it.sku_aliases[1]) ? it.sku_aliases[1].sku : ''; if (!canEditPrices) { document.querySelectorAll('#createModal .price-field input').forEach(function(el) { el.disabled = true; }); } loadCategories(it.category_id); }).catch(function(e) { alert('Error al cargar producto: ' + e.message); closeCreateModal(); }); } window.showEditItemModal = showEditItemModal; function classifyPartNumber(partNumber) { var resultEl = document.getElementById('createResult'); resultEl.innerHTML = 'Consultando IA...'; apiFetch(API + '/classify/' + encodeURIComponent(partNumber)).then(function (data) { if (!data) return; if (data.name) { document.getElementById('newName').value = data.name; } if (data.brand) { document.getElementById('newBrand').value = data.brand; } // Show suggestion label var parts = []; if (data.name) parts.push(data.name); if (data.brand) parts.push(data.brand); if (data.vehicle) parts.push(data.vehicle); if (data.category) parts.push(data.category); if (parts.length > 0) { resultEl.innerHTML = 'Sugerido por IA: ' + esc(parts.join(' | ')) + ''; } else { resultEl.innerHTML = 'IA no pudo identificar este numero de parte'; } }).catch(function () { resultEl.innerHTML = ''; }); } function closeCreateModal() { document.getElementById('createModal').classList.remove('is-open'); document.getElementById('createResult').innerHTML = ''; resetCreateModal(); } function createItem() { var editId = document.getElementById('editItemId').value; 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, unit: document.getElementById('newUnit').value.trim() || undefined, cost: parseFloat(document.getElementById('newCost').value) || 0, price_1: parseFloat(document.getElementById('newPrice1').value) || 0, min_stock: parseInt(document.getElementById('newMinStock').value) || 0, max_stock: parseInt(document.getElementById('newMaxStock').value) || 0, tax_rate: parseFloat(document.getElementById('newTaxRate').value) || 0, location: document.getElementById('newLocation').value.trim(), description: document.getElementById('newDescription').value.trim(), is_active: document.getElementById('newIsActive').value === 'true', sku_aliases: [] }; var sku2 = document.getElementById('newSku2').value.trim(); var sku3 = document.getElementById('newSku3').value.trim(); var categoryId = document.getElementById('newCategory').value; if (categoryId) data.category_id = parseInt(categoryId); if (sku2) data.sku_aliases.push({sku: sku2, label: 'Alternativo 1'}); if (sku3) data.sku_aliases.push({sku: sku3, label: 'Alternativo 2'}); if (!data.part_number || !data.name) { document.getElementById('createResult').innerHTML = 'Numero de parte y nombre son obligatorios'; return; } var url = API + '/items'; var method = 'POST'; if (editId) { url = API + '/items/' + editId; method = 'PUT'; // Stock inicial solo en creación } else { data.initial_stock = parseInt(document.getElementById('newInitialStock').value) || 0; } apiFetch(url, { method: method, body: JSON.stringify(data) }).then(function (result) { if (result && (result.id || result.message)) { var msg = editId ? 'Producto actualizado' : ('Creado ID ' + result.id + ' | Barcode: ' + result.barcode); if (!editId && result.vehicle_compatibilities_added > 0) { msg += ' | ' + result.vehicle_compatibilities_added + ' vehiculo(s) asignado(s) por IA'; } document.getElementById('createResult').innerHTML = '' + msg + ''; loadItems(currentPage); closeCreateModal(); if (window.loadInventoryStats) window.loadInventoryStats(); } else { document.getElementById('createResult').innerHTML = '' + (result ? result.error || 'Error' : 'Error de red') + ''; } }); } function submitBulkImport() { var fileInput = document.getElementById('bulkImportFile'); var resultEl = document.getElementById('bulkImportResult'); var mode = document.getElementById('bulkImportMode').value; var strategy = document.getElementById('bulkImportStrategy').value; if (!fileInput.files || !fileInput.files[0]) { resultEl.style.display = 'block'; resultEl.innerHTML = 'Selecciona un archivo CSV o Excel.'; return; } var file = fileInput.files[0]; var formData = new FormData(); formData.append('file', file); resultEl.style.display = 'block'; resultEl.innerHTML = 'Importando...'; fetch(API + '/items/bulk-import', { method: 'POST', headers: { 'Authorization': 'Bearer ' + getToken(), 'X-Import-Mode': mode, 'X-Import-Strategy': strategy, 'X-Import-Stock': (document.getElementById('bulkImportSkipStock') || {}).checked ? 'skip' : 'add' }, body: formData }).then(function(resp) { return resp.json(); }).then(function(data) { if (data.error) { resultEl.innerHTML = '' + esc(data.error) + ''; return; } var html = '
Importacion completada: ' + data.created + ' producto(s) creado(s)'; if (data.skipped > 0) html += ', ' + data.skipped + ' saltado(s)'; html += '
'; if (data.warnings && data.warnings.length) { html += '
'; html += 'Advertencias (' + data.warnings.length + '):
'; } resultEl.innerHTML = html; loadItems(currentPage); if (window.loadInventoryStats) window.loadInventoryStats(); }).catch(function(err) { resultEl.innerHTML = 'Error de red: ' + esc(err.message) + ''; }); } window.submitBulkImport = submitBulkImport; function downloadBulkImportTemplate() { var headers = [ 'numero_de_parte','nombre','marca','precio','cantidad','costo', 'sku_secundario','descripcion','categoria','fabricante','modelo','anio','motor','codigo_motor' ]; var example = [ 'EJ-001','Filtro de aceite','ACDelco','150.00','10','90.00', 'EJ001-ALT','Filtro para sedan','Filtros','Nissan','Sentra','2020','1.8','MR18DE' ]; var csv = [headers.join(','), example.join(',')].join('\n'); var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'plantilla_inventario.csv'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } window.downloadBulkImportTemplate = downloadBulkImportTemplate; // --- Full inventory export (server-side CSV) --- function loadExportBranches() { var sel = document.getElementById('exportBranch'); if (!sel) return; fetch('/pos/api/config/branches', { headers: { 'Authorization': 'Bearer ' + getToken() } }).then(function(resp) { return resp.ok ? resp.json() : {}; }) .then(function(data) { var branches = data.data || data.branches || []; branches.forEach(function(b) { var opt = document.createElement('option'); opt.value = b.id; opt.textContent = b.name; sel.appendChild(opt); }); }).catch(function() { /* selector queda solo con "todas" */ }); } loadExportBranches(); function exportFullInventory() { var btn = document.getElementById('btnExportFull'); var branchSel = document.getElementById('exportBranch'); var branchId = branchSel ? branchSel.value : ''; var url = API + '/export' + (branchId ? ('?branch_id=' + encodeURIComponent(branchId)) : ''); if (btn) { btn.disabled = true; btn.textContent = '⏳ Generando CSV...'; } fetch(url, { headers: { 'Authorization': 'Bearer ' + getToken() } }).then(function(resp) { if (!resp.ok) { return resp.json().catch(function() { return {}; }).then(function(err) { throw new Error(err.error || ('Error ' + resp.status)); }); } var disposition = resp.headers.get('Content-Disposition') || ''; var match = disposition.match(/filename="?([^";]+)"?/); var filename = match ? match[1] : 'inventario_completo.csv'; return resp.blob().then(function(blob) { return { blob: blob, filename: filename }; }); }).then(function(result) { var url = URL.createObjectURL(result.blob); var a = document.createElement('a'); a.href = url; a.download = result.filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); document.getElementById('exportModal').classList.remove('is-open'); showToast('Inventario completo descargado', 'ok'); }).catch(function(err) { showToast('Error al exportar: ' + esc(err.message), 'error'); }).finally(function() { if (btn) { btn.disabled = false; btn.textContent = '📦 Inventario completo (CSV)'; } }); } window.exportFullInventory = exportFullInventory; function exportCurrentView() { document.getElementById('exportModal').classList.remove('is-open'); if (typeof window.exportVisibleTableCSV === 'function') { window.exportVisibleTableCSV('inventario'); } } window.exportCurrentView = exportCurrentView; // ===================================================================== // PURCHASE / ENTRADA (purchaseModal) // ===================================================================== let purchaseSearchTimeout = null; let purchaseSelectedItem = null; function showPurchaseModal() { document.getElementById('purchaseModal').classList.add('is-open'); setTimeout(function() { var el = document.getElementById('purchaseItemSearch'); if (el) el.focus(); }, 100); } function showPurchaseModalForItem(itemId) { // Pre-fill by fetching item details apiFetch(API + '/items?page=1&per_page=1').then(function() { // We just need the item detail; use the existing list or fetch by id apiFetch(API + '/items?page=1&per_page=1').then(function() {}); }); selectPurchaseItem({id: itemId, name: 'Producto #' + itemId}); showPurchaseModal(); } function closePurchaseModal() { document.getElementById('purchaseModal').classList.remove('is-open'); document.getElementById('purchaseResult').innerHTML = ''; clearPurchaseSelection(); } function clearPurchaseSelection() { purchaseSelectedItem = null; var ids = ['purchaseItemId','purchaseItemSearch','purchaseQty','purchaseCost','purchaseInvoice','purchaseNotes']; ids.forEach(function(id) { var el = document.getElementById(id); if (el) el.value = ''; }); var results = document.getElementById('purchaseItemResults'); if (results) results.style.display = 'none'; var selected = document.getElementById('purchaseItemSelected'); if (selected) selected.textContent = ''; } function selectPurchaseItem(item) { purchaseSelectedItem = item; document.getElementById('purchaseItemId').value = item.id; document.getElementById('purchaseItemSearch').value = item.name || item.part_number || item.barcode || ('#' + item.id); document.getElementById('purchaseItemResults').style.display = 'none'; document.getElementById('purchaseItemSelected').innerHTML = '' + esc(item.name || '') + '' + (item.part_number ? ' · No. parte: ' + esc(item.part_number) : '') + (item.barcode ? ' · Barcode: ' + esc(item.barcode) : ''); document.getElementById('purchaseQty').focus(); } function searchPurchaseItems(query) { var resultsEl = document.getElementById('purchaseItemResults'); if (!query || query.length < 2) { resultsEl.style.display = 'none'; return; } apiFetch(API + '/items?q=' + encodeURIComponent(query) + '&per_page=10').then(function(res) { var items = (res && res.data) || []; if (!items.length) { resultsEl.innerHTML = '
Sin resultados
'; resultsEl.style.display = 'block'; return; } resultsEl.innerHTML = items.map(function(it) { return '
' + '
' + esc(it.name) + '
' + '
' + (it.part_number ? 'No. parte: ' + esc(it.part_number) + ' · ' : '') + (it.barcode ? 'Barcode: ' + esc(it.barcode) + ' · ' : '') + 'Stock: ' + (it.stock || 0) + '
' + '
'; }).join(''); resultsEl.querySelectorAll('.purchase-search-result').forEach(function(row) { row.onclick = function() { var id = parseInt(row.dataset.id); var item = items.find(function(x) { return x.id === id; }); if (item) selectPurchaseItem(item); }; }); resultsEl.style.display = 'block'; }).catch(function() { resultsEl.style.display = 'none'; }); } function wirePurchaseSearch() { var input = document.getElementById('purchaseItemSearch'); var resultsEl = document.getElementById('purchaseItemResults'); if (!input) return; input.addEventListener('input', function() { if (purchaseSelectedItem && input.value !== purchaseSelectedItem.name) { purchaseSelectedItem = null; document.getElementById('purchaseItemId').value = ''; document.getElementById('purchaseItemSelected').textContent = ''; } clearTimeout(purchaseSearchTimeout); purchaseSearchTimeout = setTimeout(function() { searchPurchaseItems(input.value.trim()); }, 250); }); input.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); // Try exact barcode match first var query = input.value.trim(); if (!query) return; apiFetch(API + '/items?q=' + encodeURIComponent(query) + '&per_page=20').then(function(res) { var items = (res && res.data) || []; var exact = items.find(function(it) { return (it.barcode || '').toLowerCase() === query.toLowerCase() || (it.part_number || '').toLowerCase() === query.toLowerCase(); }); if (exact) { selectPurchaseItem(exact); } else if (items.length === 1) { selectPurchaseItem(items[0]); } else { searchPurchaseItems(query); } }); } else if (e.key === 'Escape') { if (resultsEl) resultsEl.style.display = 'none'; } }); document.addEventListener('click', function(e) { if (resultsEl && !input.contains(e.target) && !resultsEl.contains(e.target)) { resultsEl.style.display = 'none'; } }); } 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 obligatorios'; return; } apiFetch(API + '/purchase', { method: 'POST', body: JSON.stringify(data) }).then(function (result) { if (result && result.operation_id) { document.getElementById('purchaseResult').innerHTML = 'Compra registrada (op #' + result.operation_id + ')'; closePurchaseModal(); if (window.loadInventoryStats) window.loadInventoryStats(); loadItems(currentPage); } else { document.getElementById('purchaseResult').innerHTML = '' + (result ? result.error || 'Error' : 'Error de red') + ''; } }); } // ===================================================================== // ADJUSTMENT / AJUSTE (adjustmentModal) // ===================================================================== function showAdjustmentModal() { document.getElementById('adjustmentModal').classList.add('is-open'); } function closeAdjustmentModal() { document.getElementById('adjustmentModal').classList.remove('is-open'); document.getElementById('adjustResult').innerHTML = ''; } 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; } apiFetch(API + '/adjustment', { method: 'POST', body: JSON.stringify(data) }).then(function (result) { if (result && result.operation_id) { document.getElementById('adjustResult').innerHTML = 'Ajuste registrado (op #' + result.operation_id + ')'; closeAdjustmentModal(); ['adjustItemId','adjustQty','adjustReason'].forEach(function(id) { var el = document.getElementById(id); if (el) el.value = ''; }); if (window.loadInventoryStats) window.loadInventoryStats(); if (window.loadOperations) window.loadOperations('ajustes', 1); loadItems(currentPage); } else { document.getElementById('adjustResult').innerHTML = '' + (result ? result.error || 'Error' : 'Error de red') + ''; } }); } // ===================================================================== // TRANSFER / TRASPASO (transferModal) // ===================================================================== var transferLines = []; var transferDocOpen = null; function showTransferModal() { document.getElementById('transferModal').classList.add('is-open'); loadTransferBranches(); renderTransferLines(); loadTransferDocs(); setTimeout(function () { var el = document.getElementById('transferScan'); if (el) el.focus(); }, 100); } function closeTransferModal() { document.getElementById('transferModal').classList.remove('is-open'); document.getElementById('transferResult').innerHTML = ''; } window.showTransferModal = showTransferModal; window.closeTransferModal = closeTransferModal; window.transferAddByCode = transferAddByCode; window.countAddByCode = countAddByCode; function switchTransferTab(tab) { document.getElementById('tr-panel-new').style.display = tab === 'new' ? '' : 'none'; document.getElementById('tr-panel-list').style.display = tab === 'list' ? '' : 'none'; document.getElementById('tr-tab-new').classList.toggle('active', tab === 'new'); document.getElementById('tr-tab-list').classList.toggle('active', tab === 'list'); document.getElementById('transferNewFooter').style.display = tab === 'new' ? 'flex' : 'none'; if (tab === 'list') loadTransferDocs(); } function loadTransferBranches() { fetch('/pos/api/config/branches', { headers: { 'Authorization': 'Bearer ' + getToken() } }) .then(function (r) { return r.json(); }) .then(function (d) { var sel = document.getElementById('transferTo'); sel.innerHTML = ''; (d.data || []).forEach(function (b) { sel.innerHTML += ''; }); }).catch(function () {}); } function renderTransferLines() { var el = document.getElementById('transferLines'); if (!transferLines.length) { el.innerHTML = '

Escanea o busca productos para agregarlos

'; return; } el.innerHTML = transferLines.map(function (l, i) { return '
' + '' + esc(l.name) + ' ' + esc(l.part_number) + ' · Stock origen: ' + l.stock + '' + '' + '' + '
'; }).join(''); } function transferAddByCode(code) { apiFetch(API + '/items/lookup?code=' + encodeURIComponent(code)) .then(function (d) { if (!d || !d.found) { document.getElementById('transferResult').innerHTML = 'Código no encontrado: ' + esc(code) + ''; return; } var it = d.item; var existing = transferLines.find(function (l) { return l.inventory_id === it.id; }); if (existing) existing.quantity += 1; else transferLines.push({ inventory_id: it.id, part_number: it.part_number, name: it.name, stock: it.stock, quantity: 1 }); renderTransferLines(); }).catch(function () { document.getElementById('transferResult').innerHTML = 'Error de búsqueda'; }); } function transferLineQty(i, v) { transferLines[i].quantity = Math.max(1, parseInt(v, 10) || 1); } function transferLineRemove(i) { transferLines.splice(i, 1); renderTransferLines(); } function transferDocSave(andSend) { var toBranch = parseInt(document.getElementById('transferTo').value); if (!toBranch) { alert('Selecciona sucursal destino'); return; } if (!transferLines.length) { alert('Agrega al menos un producto'); return; } apiFetch(API + '/transfer-docs', { method: 'POST', body: JSON.stringify({ to_branch_id: toBranch, notes: document.getElementById('transferNotes').value.trim(), items: transferLines.map(function (l) { return { inventory_id: l.inventory_id, quantity: l.quantity }; }), }) }).then(function (d) { if (!d || !d.id) throw new Error(d ? d.error || 'Error' : 'Error de red'); if (!andSend) { document.getElementById('transferResult').innerHTML = 'Borrador #' + d.id + ' guardado'; transferLines = []; renderTransferLines(); return; } return apiFetch(API + '/transfer-docs/' + d.id + '/send', { method: 'POST', body: '{}' }) .then(function (s) { document.getElementById('transferResult').innerHTML = 'Transferencia #' + d.id + ' enviada'; transferLines = []; renderTransferLines(); if (window.loadInventoryStats) window.loadInventoryStats(); }); }).catch(function (e) { document.getElementById('transferResult').innerHTML = '' + esc(e.message) + ''; }); } function loadTransferDocs() { apiFetch(API + '/transfer-docs').then(function (d) { var docs = (d && d.data) || []; var el = document.getElementById('transferDocsList'); if (!docs.length) { el.innerHTML = '

Sin transferencias registradas

'; return; } var statusColor = { draft: 'var(--color-text-muted)', sent: 'var(--color-warning)', received: 'var(--color-success)', cancelled: 'var(--color-error)' }; el.innerHTML = docs.map(function (t) { return '
' + '
#' + t.id + ' ' + esc(t.from_name) + ' → ' + esc(t.to_name) + ' [' + t.status + ']' + '
' + t.total_qty + ' piezas · ' + (t.created_at || '').slice(0, 16).replace('T', ' ') + '
' + '›
'; }).join(''); }); } function openTransferDoc(id) { apiFetch(API + '/transfer-docs/' + id).then(function (d) { switchTransferTab('new'); document.getElementById('tr-panel-new').style.display = 'none'; document.getElementById('tr-panel-list').style.display = ''; transferDocOpen = d; var el = document.getElementById('transferDocsList'); var html = '
'; html += '
#' + d.id + ' ' + esc(d.from_name) + ' → ' + esc(d.to_name) + ' [' + d.status + ']
'; html += '
'; d.lines.forEach(function (l) { html += '
' + '' + esc(l.name) + ' ' + esc(l.part_number) + '' + 'Env: ' + l.quantity + ''; if (d.status === 'sent') { html += '' + ''; } else { html += 'Rec: ' + l.qty_received + ''; } html += '
'; }); html += '
'; html += '
'; if (d.status === 'draft') { html += ' '; } if (d.status === 'sent') { html += ' '; } if (d.status === 'draft') { html += ''; } html += '
'; el.innerHTML = html; }); } function transferDocSend(id) { apiFetch(API + '/transfer-docs/' + id + '/send', { method: 'POST', body: '{}' }) .then(function (r) { if (r && r.ok) { if (window.loadInventoryStats) window.loadInventoryStats(); openTransferDoc(id); } else alert((r && r.error) || 'Error'); }); } function transferDocReceive(id) { var received = {}; document.querySelectorAll('[id^="tr-rec-"]').forEach(function (inp) { var lineId = parseInt(inp.id.replace('tr-rec-', ''), 10); received[lineId] = parseInt(inp.value, 10) || 0; }); apiFetch(API + '/transfer-docs/' + id + '/receive', { method: 'POST', body: JSON.stringify({ received: received }) }).then(function (r) { if (r && r.ok) { alert('Recepción confirmada' + (r.differences ? ' — ' + r.differences + ' diferencia(s) ajustadas en destino' : '')); if (window.loadInventoryStats) window.loadInventoryStats(); loadItems(currentPage); openTransferDoc(id); } else alert((r && r.error) || 'Error'); }); } function transferDocCancel(id) { if (!confirm('¿Cancelar el borrador de transferencia?')) return; apiFetch(API + '/transfer-docs/' + id + '/cancel', { method: 'POST', body: '{}' }) .then(function () { loadTransferDocs(); }); } // ===================================================================== // OPERATIONS LIST (Entradas, Salidas, Traspasos, Ajustes) // ===================================================================== var opTypeMap = { 'entradas': 'PURCHASE', 'salidas': 'SALE', 'traspasos': 'TRANSFER', 'ajustes': 'ADJUST' }; var opColspan = { entradas: 8, salidas: 7, traspasos: 8, ajustes: 7 }; function loadOperations(type, page) { var opType = opTypeMap[type]; if (!opType) return; page = page || 1; var params = new URLSearchParams({ type: opType, page: page, per_page: 50 }); apiFetch(API + '/operations?' + params.toString()).then(function (data) { if (!data) return; var tbodyId = type + 'TableBody'; var footerId = type + 'Footer'; var pagId = type + 'Pagination'; var tbody = document.getElementById(tbodyId); var ops = data.data || []; if (!ops.length) { tbody.innerHTML = 'Sin registros'; document.getElementById(pagId).innerHTML = ''; document.getElementById(footerId).textContent = ''; return; } tbody.innerHTML = ops.map(function (op) { return renderOperationRow(op, type); }).join(''); var pg = data.pagination || {}; if (pg.total_pages > 1) { document.getElementById(pagId).innerHTML = ''; } else { document.getElementById(pagId).innerHTML = ''; } document.getElementById(footerId).textContent = (pg.total || 0) + ' registros'; }); } window.loadOperations = loadOperations; window._loadOperations = loadOperations; function renderOperationRow(op, type) { var dateStr = op.created_at ? new Date(op.created_at).toLocaleString('es-MX', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : '-'; var productInfo = esc(op.part_number || op.barcode || '') + ' — ' + esc(op.product_name || ''); if (type === 'entradas') { return '' + '#' + op.id + '' + '' + dateStr + '' + '' + productInfo + '' + '' + (op.quantity || 0) + '' + '$' + fmt(op.cost_at_time || 0) + '' + '$' + fmt(op.total || 0) + '' + '' + esc(op.notes || '-') + '' + '' + esc(op.employee_name || '-') + '' + ''; } if (type === 'salidas') { return '' + '#' + op.id + '' + '' + dateStr + '' + '' + productInfo + '' + '' + (op.quantity || 0) + '' + '$' + fmt(op.total || 0) + '' + '' + esc(op.notes || '-') + '' + '' + esc(op.employee_name || '-') + '' + ''; } if (type === 'traspasos') { return '' + '#' + op.id + '' + '' + dateStr + '' + '' + productInfo + '' + '' + (op.quantity || 0) + '' + '' + esc(op.branch_name || '-') + '' + '' + esc(op.notes || '-') + '' + '' + esc(op.employee_name || '-') + '' + '' + ''; } if (type === 'ajustes') { return '' + '#' + op.id + '' + '' + dateStr + '' + '' + esc(op.operation_type || 'ADJUST') + '' + '' + productInfo + '' + '' + (op.quantity || 0) + '' + '' + esc(op.notes || '-') + '' + '' + esc(op.employee_name || '-') + '' + ''; } return '-'; } // ===================================================================== // PHYSICAL COUNT / CONTEO (countModal) // ===================================================================== function showCountModal() { document.getElementById('countModal').classList.add('is-open'); // Pre-add one line if empty if (!document.querySelectorAll('#countLines .count-row').length) { addCountLine(); } } function closeCountModal() { document.getElementById('countModal').classList.remove('is-open'); } var countRows = []; function addCountLine() { var input = document.getElementById('countScanInput'); if (input) input.focus(); } function countAddByCode(code, qty) { apiFetch(API + '/items/lookup?code=' + encodeURIComponent(code)) .then(function (d) { if (!d || !d.found) { alert('Código no encontrado: ' + code); return; } var it = d.item; var row = countRows.find(function (r) { return r.inventory_id === it.id; }); if (row) row.counted_quantity += (qty || 1); else countRows.push({ inventory_id: it.id, part_number: it.part_number, name: it.name, expected: it.stock, counted_quantity: (qty || 1), }); renderCountRows(); }); } function renderCountRows() { var el = document.getElementById('countLines'); if (!countRows.length) { el.innerHTML = '

Sin productos escaneados

'; return; } el.innerHTML = countRows.map(function (r, i) { var diff = r.counted_quantity - r.expected; var color = diff === 0 ? 'var(--color-success)' : (diff < 0 ? 'var(--color-error)' : 'var(--color-warning)'); return '
' + '' + esc(r.name) + ' ' + esc(r.part_number) + '' + 'Esp: ' + r.expected + '' + '' + '' + (diff > 0 ? '+' : '') + diff + '' + '' + '
'; }).join(''); } function countQty(i, v) { countRows[i].counted_quantity = Math.max(0, parseInt(v, 10) || 0); renderCountRows(); } function countRemove(i) { countRows.splice(i, 1); renderCountRows(); } function startPhysicalCount() { var items = countRows.map(function (r) { return { inventory_id: r.inventory_id, counted_quantity: r.counted_quantity }; }); if (!items.length) { alert('Escanea al menos un producto'); return; } apiFetch(API + '/physical-count/start', { method: 'POST', body: JSON.stringify({ items: items }) }).then(function (result) { 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 + ' — ' + esc(result.message) + '

'; html += ''; (result.results || []).forEach(function (r) { var color = r.difference === 0 ? 'var(--color-success)' : (r.difference < 0 ? 'var(--color-error)' : 'var(--color-warning)'); html += ''; }); html += '
IDEsperadoContadoDiferencia
' + r.inventory_id + '' + r.expected + '' + r.counted + '' + (r.difference > 0 ? '+' : '') + r.difference + '
'; html += ''; document.getElementById('countResults').innerHTML = html; countRows = []; renderCountRows(); }).catch(function (e) { document.getElementById('countResults').innerHTML = '' + esc(e.message) + ''; }); } // ===================================================================== function uploadItemImage(itemId) { var input = document.createElement('input'); input.type = 'file'; input.accept = 'image/jpeg,image/png,image/webp'; input.onchange = function () { if (!input.files || !input.files[0]) return; var file = input.files[0]; if (file.size > 5 * 1024 * 1024) { alert('Imagen demasiado grande (max 5 MB)'); return; } var fd = new FormData(); fd.append('file', file); var statusEl = document.getElementById('imgUploadStatus'); if (statusEl) statusEl.textContent = 'Subiendo...'; fetch(API + '/items/' + itemId + '/image', { method: 'POST', headers: { 'Authorization': 'Bearer ' + getToken() }, body: fd }) .then(function (r) { return r.json(); }) .then(function (result) { if (result.image_url) { // Refresh detail view viewProductDetail(itemId); } else { if (statusEl) statusEl.textContent = result.error || 'Error'; } }) .catch(function () { if (statusEl) statusEl.textContent = 'Error de red'; }); }; input.click(); } function deleteItemImage(itemId) { if (!confirm('Eliminar imagen de este producto?')) return; fetch(API + '/items/' + itemId + '/image', { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + getToken() } }) .then(function (r) { return r.json(); }) .then(function (result) { if (result.message) { viewProductDetail(itemId); } else { alert(result.error || 'Error'); } }) .catch(function () { alert('Error de red'); }); } function viewProductDetail(itemId) { apiFetch(API + '/items/' + itemId).then(function (data) { if (!data || data.error) { alert(data ? data.error : 'Error de red'); return; } var history = data.history || []; var html = ''; // Tab styles html += ''; // Tabs html += '
'; html += ''; html += ''; html += '
'; // Detail panel html += '
'; // Product image section html += '
'; if (data.image_url) { html += '' + esc(data.name) + ''; html += '
'; html += ''; html += ''; html += '
'; } else { html += '
'; html += ''; html += '
Sin imagen
'; html += '
'; html += ''; } html += ''; html += '
'; // Action buttons html += '
'; html += ''; if (data.image_url) { html += ''; } html += ''; html += '
'; // Product info header html += '
'; html += '
ID Inventario' + data.id + '
'; html += '
No. Parte' + esc(data.part_number) + '
'; html += '
Nombre' + esc(data.name) + '
'; html += '
Marca' + esc(data.brand || '-') + '
'; html += '
Categoría' + esc(data.category_name || '-') + '
'; html += '
Codigo de Barras' + esc(data.barcode) + '
'; html += '
Ubicacion' + esc(data.location || '-') + '
'; html += '
Stock' + (data.stock || 0) + '
'; html += '
'; // SKU Aliases section html += '
SKU Alternativos
'; html += '
'; html += '

Cargando SKU alternativos...

'; html += '
'; // Prices html += '
'; html += '
Costo$' + fmt(data.cost) + '
'; html += '
Mostrador$' + fmt(data.price_1) + '
'; html += '
Taller$' + fmt(data.price_2) + ' (-' + globalDiscounts[2] + '%)
'; html += '
Mayoreo$' + fmt(data.price_3) + ' (-' + globalDiscounts[3] + '%)
'; html += '
'; // Cross-references section html += '
Cross-References / Equivalencias
'; html += '
'; html += '

Cargando equivalencias...

'; html += '
'; // Load cross-references from catalog API var partNumber = data.part_number; var catalogPartId = data.catalog_part_id; (function loadCrossRefs() { // Try catalog part detail if we have catalog_part_id var url = catalogPartId ? '/pos/api/catalog/part/' + catalogPartId : '/pos/api/catalog/search?q=' + encodeURIComponent(partNumber); fetch(url, { headers: { 'Authorization': 'Bearer ' + getToken() } }) .then(function(r) { return r.json(); }) .then(function(d) { var el = document.getElementById('crossRefContent'); if (!el) return; var alternatives = d.alternatives || []; var bodegas = d.bodegas || []; // If it was a search, get alternatives from first result if (!catalogPartId && d.data && d.data.length > 0) { // Fetch detail for first match fetch('/pos/api/catalog/part/' + d.data[0].id_part, { headers: { 'Authorization': 'Bearer ' + getToken() } }) .then(function(r2) { return r2.json(); }) .then(function(d2) { renderCrossRefs(el, d2.alternatives || [], d2.bodegas || []); }) .catch(function() { el.innerHTML = '

Sin conexion al catalogo.

'; }); return; } renderCrossRefs(el, alternatives, bodegas); }) .catch(function() { var el = document.getElementById('crossRefContent'); if (el) el.innerHTML = '

Sin conexion al catalogo central.

'; }); })(); function renderCrossRefs(el, alternatives, bodegas) { var html2 = ''; if (bodegas && bodegas.length > 0) { html2 += '
Disponible en Bodegas:
'; html2 += ''; bodegas.forEach(function(b) { html2 += ''; }); html2 += '
BodegaStockPrecioUbicacion
' + esc(b.business_name || b.bodega || '') + '' + (b.stock || b.stock_quantity || 0) + '$' + fmt(b.price || 0) + '' + esc(b.location || b.warehouse_location || '') + '
'; } if (alternatives && alternatives.length > 0) { html2 += '
Partes Equivalentes (Aftermarket):
'; html2 += ''; alternatives.forEach(function(a) { html2 += ''; }); html2 += '
No. ParteFabricanteNombre
' + esc(a.part_number || a.cross_reference_number || '') + '' + esc(a.manufacturer || a.source_ref || '') + '' + esc(a.name || a.name_aftermarket_parts || '') + '
'; } if (!html2) { html2 = '

No se encontraron equivalencias para esta parte.

'; } el.innerHTML = html2; } // Close detail panel html += '
'; // Compatibility panel html += '
'; // Existing compatibilities html += '
'; html += '

Cargando compatibilidades...

'; html += '
'; // Manual add form html += '
Agregar Manualmente
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += ''; // Auto-match button var btnLabel = compatSource === 'qwen' ? 'Auto-Match con IA (QWEN)' : (compatSource === 'both' ? 'Auto-Match (TecDoc + IA)' : 'Auto-Match por TecDoc'); html += '
'; html += '
'; // Load SKU aliases (function loadSkuAliases() { fetch('/pos/api/inventory/items/' + itemId + '/skus', { headers: { 'Authorization': 'Bearer ' + getToken() } }) .then(function(r) { return r.json(); }) .then(function(d) { var el = document.getElementById('skuAliasContent'); if (!el) return; var list = d.aliases || []; var html2 = ''; if (list.length > 0) { html2 += ''; list.forEach(function(a) { html2 += ''; html2 += ''; }); html2 += '
SKUEtiqueta
' + esc(a.sku) + '' + esc(a.label || '-') + '
'; } else { html2 += '

Sin SKU alternativos.

'; } html2 += '
'; html2 += ''; html2 += ''; html2 += ''; html2 += '
'; el.innerHTML = html2; }) .catch(function() { var el = document.getElementById('skuAliasContent'); if (el) el.innerHTML = '

Error al cargar SKU alternativos.

'; }); })(); // Load vehicle compatibilities and makes (function loadCompatPanel() { fetch('/pos/api/inventory/items/' + itemId + '/vehicles', { headers: { 'Authorization': 'Bearer ' + getToken() } }) .then(function(r) { return r.json(); }) .then(function(d) { var el = document.getElementById('compatContent'); if (!el) return; var list = d.vehicles || []; var html2 = ''; if (list.length > 0) { html2 += ''; list.forEach(function(c) { var sourceLabel = c.source === 'qwen_ai' ? 'IA' : (c.source === 'auto_match' ? 'TecDoc' : esc(c.source || '')); html2 += ''; html2 += ''; }); html2 += '
MarcaModeloAnoMotorOrigen
' + esc(c.brand || '') + '' + esc(c.model || '') + '' + esc(c.year || '') + '' + esc(c.engine || '') + '' + sourceLabel + '
'; } else { html2 += '

Sin vehiculos vinculados.

'; } el.innerHTML = html2; }) .catch(function() { var el = document.getElementById('compatContent'); if (el) el.innerHTML = '

Error al cargar compatibilidades.

'; }); // Load makes fetch('/pos/api/inventory/vehicles/makes', { headers: { 'Authorization': 'Bearer ' + getToken() } }) .then(function(r) { return r.json(); }) .then(function(d) { var sel = document.getElementById('manualMake'); if (!sel) return; var opts = ''; (d.makes || []).forEach(function(m) { opts += ''; }); sel.innerHTML = opts; sel.disabled = false; }) .catch(function() { var sel = document.getElementById('manualMake'); if (sel) { sel.innerHTML = ''; } }); })(); // Movement history html += '
Historial de Movimientos
'; if (!history.length) { html += '

Sin movimientos

'; } else { html += ''; history.forEach(function (h) { var qtyColor = h.quantity > 0 ? 'var(--color-success)' : 'var(--color-error)'; html += '' + '' + '' + '' + '' + '' + '' + ''; }); html += '
FechaTipoCantidadCostoEmpleadoNotas
' + formatDateTime(h.date) + '' + esc(h.type) + '' + (h.quantity > 0 ? '+' : '') + h.quantity + '' + (h.cost ? '$' + fmt(h.cost) : '\u2014') + '' + esc(h.employee) + '' + esc(h.notes) + '
'; } document.getElementById('historyContent').innerHTML = html; document.getElementById('historyModal').classList.add('is-open'); }); } // Vehicle compatibility actions function autoMatchCompat(itemId) { fetch('/pos/api/inventory/items/' + itemId + '/vehicles/auto-match', { method: 'POST', headers: { 'Authorization': 'Bearer ' + getToken() } }).then(function(r) { return r.json(); }) .then(function(d) { var msg = ''; if (d.tecdoc && d.qwen) { var t = d.tecdoc.matched ? (d.tecdoc.matched_count || d.tecdoc.matches ? d.tecdoc.matches.length : 0) : 0; var q = d.qwen.total_qwen || 0; var qi = d.qwen.inserted || 0; msg = 'Auto-match completado.\nTecDoc: ' + t + ' vehiculos.\nIA QWEN: ' + qi + ' nuevos vinculados (de ' + q + ' encontrados).'; } else if (d.myes) { msg = 'Auto-match completado. Vehiculos encontrados: ' + (d.total_qwen || d.myes.length) + ' (nuevos vinculados: ' + (d.inserted || 0) + ')'; } else { msg = 'Auto-match completado. Vehiculos vinculados: ' + (d.matched ? 'Si' : 'No'); } alert(msg); viewProductDetail(itemId); }).catch(function() { alert('Error en auto-match'); }); } function removeCompat(itemId, compatId) { if (!confirm('Quitar compatibilidad con este vehiculo?')) return; fetch('/pos/api/inventory/items/' + itemId + '/vehicles/' + compatId, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + getToken() } }).then(function(r) { return r.json(); }) .then(function() { viewProductDetail(itemId); }).catch(function() { alert('Error al quitar compatibilidad'); }); } // Manual compatibility tab functions window.switchCompatTab = function(tab, btn) { document.querySelectorAll('.compat-tab-btn').forEach(function(b) { b.classList.remove('is-active'); }); document.querySelectorAll('.compat-tab-panel').forEach(function(p) { p.classList.remove('is-active'); }); btn.classList.add('is-active'); document.getElementById('compatTab-' + tab).classList.add('is-active'); }; window.onManualMakeChange = function(itemId) { var sel = document.getElementById('manualMake'); var modelSel = document.getElementById('manualModel'); var yearSel = document.getElementById('manualYear'); var engineSel = document.getElementById('manualEngine'); if (!sel || !modelSel) return; var opt = sel.options[sel.selectedIndex]; var brandId = opt ? opt.getAttribute('data-id') : null; modelSel.innerHTML = ''; modelSel.disabled = true; yearSel.innerHTML = ''; yearSel.disabled = true; engineSel.innerHTML = ''; engineSel.disabled = true; if (!brandId) { modelSel.innerHTML = ''; return; } fetch('/pos/api/inventory/vehicles/models?brand_id=' + brandId, { headers: { 'Authorization': 'Bearer ' + getToken() } }) .then(function(r) { return r.json(); }) .then(function(d) { var opts = ''; (d.models || []).forEach(function(m) { opts += ''; }); modelSel.innerHTML = opts; modelSel.disabled = false; }) .catch(function() { modelSel.innerHTML = ''; }); }; window.onManualModelChange = function(itemId) { var modelSel = document.getElementById('manualModel'); var yearSel = document.getElementById('manualYear'); var engineSel = document.getElementById('manualEngine'); if (!modelSel || !yearSel) return; var opt = modelSel.options[modelSel.selectedIndex]; var modelId = opt ? opt.getAttribute('data-id') : null; yearSel.innerHTML = ''; yearSel.disabled = true; engineSel.innerHTML = ''; engineSel.disabled = true; if (!modelId) { yearSel.innerHTML = ''; return; } fetch('/pos/api/inventory/vehicles/years?model_id=' + modelId, { headers: { 'Authorization': 'Bearer ' + getToken() } }) .then(function(r) { return r.json(); }) .then(function(d) { var opts = ''; (d.years || []).forEach(function(y) { opts += ''; }); yearSel.innerHTML = opts; yearSel.disabled = false; }) .catch(function() { yearSel.innerHTML = ''; }); }; window.onManualYearChange = function(itemId) { var modelSel = document.getElementById('manualModel'); var yearSel = document.getElementById('manualYear'); var engineSel = document.getElementById('manualEngine'); if (!modelSel || !yearSel || !engineSel) return; var mOpt = modelSel.options[modelSel.selectedIndex]; var yOpt = yearSel.options[yearSel.selectedIndex]; var modelId = mOpt ? mOpt.getAttribute('data-id') : null; var yearId = yOpt ? yOpt.getAttribute('data-id') : null; engineSel.innerHTML = ''; engineSel.disabled = true; if (!modelId || !yearId) { engineSel.innerHTML = ''; return; } fetch('/pos/api/inventory/vehicles/engines?model_id=' + modelId + '&year_id=' + yearId, { headers: { 'Authorization': 'Bearer ' + getToken() } }) .then(function(r) { return r.json(); }) .then(function(d) { var opts = ''; (d.engines || []).forEach(function(e) { opts += ''; }); engineSel.innerHTML = opts; engineSel.disabled = false; }) .catch(function() { engineSel.innerHTML = ''; }); }; window.submitManualCompat = function(itemId) { var makeSel = document.getElementById('manualMake'); var modelSel = document.getElementById('manualModel'); var yearSel = document.getElementById('manualYear'); var engineSel = document.getElementById('manualEngine'); if (!makeSel || !modelSel || !yearSel) return; var make = makeSel.value; var model = modelSel.value; var year = yearSel.value; var engine = engineSel ? engineSel.value : ''; var engineCode = engineSel && engineSel.selectedIndex > 0 ? (engineSel.options[engineSel.selectedIndex].getAttribute('data-code') || '') : ''; if (!make || !model || !year) { alert('Selecciona al menos marca, modelo y ano'); return; } fetch('/pos/api/inventory/items/' + itemId + '/vehicles/manual', { method: 'POST', headers: { 'Authorization': 'Bearer ' + getToken(), 'Content-Type': 'application/json' }, body: JSON.stringify({ make: make, model: model, year: parseInt(year), engine: engine, engine_code: engineCode }) }).then(function(r) { return r.json(); }) .then(function(d) { if (d.error) { alert(d.error); return; } viewProductDetail(itemId); }).catch(function() { alert('Error al agregar compatibilidad'); }); }; // SKU alias actions window.addSkuAlias = function(itemId) { var skuEl = document.getElementById('newAliasSku-' + itemId); var labelEl = document.getElementById('newAliasLabel-' + itemId); var sku = skuEl ? skuEl.value.trim() : ''; var label = labelEl ? labelEl.value.trim() : ''; if (!sku) { alert('Ingresa un SKU'); return; } fetch('/pos/api/inventory/items/' + itemId + '/skus', { method: 'POST', headers: { 'Authorization': 'Bearer ' + getToken(), 'Content-Type': 'application/json' }, body: JSON.stringify({ sku: sku, label: label }) }).then(function(r) { return r.json(); }) .then(function(d) { if (d.error) { alert(d.error); return; } viewProductDetail(itemId); }).catch(function() { alert('Error al agregar SKU'); }); }; window.removeSkuAlias = function(itemId, aliasId) { if (!confirm('Eliminar este SKU alternativo?')) return; fetch('/pos/api/inventory/items/' + itemId + '/skus/' + aliasId, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + getToken() } }).then(function(r) { return r.json(); }) .then(function() { viewProductDetail(itemId); }).catch(function() { alert('Error al eliminar SKU'); }); }; // ===================================================================== // RESTAURADAS (recuperadas de git HEAD) // ===================================================================== function viewHistory(itemId) { apiFetch(API + '/items/' + itemId + '/history').then(function (data) { if (!data) return; var history = data.data || []; var html = ''; if (!history.length) { html = renderEmptyState({ icon: '', title: 'Sin movimientos', subtitle: 'Este producto aún no tiene historial de entradas, salidas ni ajustes.' }); } else { html = ''; history.forEach(function (h) { var qtyColor = h.quantity > 0 ? 'var(--color-success)' : 'var(--color-error)'; html += '' + '' + '' + '' + '' + '' + '' + ''; }); html += '
FechaTipoCantidadCostoEmpleadoNotas
' + formatDateTime(h.date) + '' + esc(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('is-open'); }); } function closeHistoryModal() { document.getElementById('historyModal').classList.remove('is-open'); } 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; } apiFetch(API + '/transfer', { method: 'POST', body: JSON.stringify(data) }).then(function (result) { if (result && result.out_operation_id) { document.getElementById('transferResult').innerHTML = 'Transferencia registrada'; closeTransferModal(); ['transferItemId','transferFrom','transferTo','transferQty','transferNotes'].forEach(function(id) { var el = document.getElementById(id); if (el) el.value = ''; }); if (window.loadInventoryStats) window.loadInventoryStats(); if (window.loadOperations) window.loadOperations('traspasos', 1); loadItems(currentPage); } else { document.getElementById('transferResult').innerHTML = '' + (result ? result.error || 'Error' : 'Error de red') + ''; } }); } function approvePhysicalCount() { if (!draftCountId) { alert('No hay borrador activo'); return; } apiFetch(API + '/physical-count/approve', { method: 'POST', body: JSON.stringify({ count_id: draftCountId }) }).then(function (result) { if (result && result.status === 'approved') { document.getElementById('countResults').innerHTML = '' + esc(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'; } function loadAlerts() { var container = document.getElementById('alertsContent'); if (container) container.innerHTML = '
' + renderEmptyState({ icon: '', title: 'Cargando alertas...', subtitle: 'Revisando el estado del inventario' }) + '
'; apiFetch(API + '/alerts').then(function (data) { if (!data) return; var alerts = data.data || []; var counts = data.counts || {}; if (!container) return; if (!alerts.length) { container.innerHTML = renderEmptyState({ icon: '', title: 'Todo en orden', subtitle: 'No hay alertas activas en el inventario. Los niveles de stock están dentro de los límites configurados.' }); return; } // Summary bar var html = '
' + '
Resumen de alertas
' + (counts.critical ? '' + counts.critical + ' crítica' + (counts.critical !== 1 ? 's' : '') + '' : '') + (counts.warning ? '' + counts.warning + ' advertencia' + (counts.warning !== 1 ? 's' : '') + '' : '') + (counts.info ? '' + counts.info + ' informativa' + (counts.info !== 1 ? 's' : '') + '' : '') + '
'; // Group by severity var critical = alerts.filter(function (a) { return a.severity === 'critical'; }); var warning = alerts.filter(function (a) { return a.severity === 'warning'; }); var info = alerts.filter(function (a) { return a.severity !== 'critical' && a.severity !== 'warning'; }); html += renderAlertSection('Criticas', critical, 'critical', 'badge--low'); html += renderAlertSection('Advertencias', warning, 'warning', 'badge--over'); html += renderAlertSection('Informativas', info, 'info', 'badge--ok'); container.innerHTML = html; }); } function code128Svg(data, width, height) { data = String(data || ''); if (!data.length) return ''; var codes = [104]; var sum = 104; for (var i = 0; i < data.length; i++) { var c = data.charCodeAt(i); if (c < 32 || c > 126) c = 32; var v = c - 32; codes.push(v); sum += v * (i + 1); } codes.push(sum % 103); codes.push(106); var quiet = 10; var modules = quiet * 2; codes.forEach(function (cd) { CODE128_BARS[cd].split('').forEach(function (w) { modules += parseInt(w, 10); }); }); var mw = width / modules; var bars = ''; var x = quiet * mw; codes.forEach(function (cd) { var pat = CODE128_BARS[cd]; for (var k = 0; k < pat.length; k++) { var w = parseInt(pat[k], 10) * mw; if (k % 2 === 0) bars += ''; x += w; } }); return '' + bars + ''; } function printBarcode(barcode, partNumber, name) { var w = window.open('', '_blank', 'width=420,height=300'); w.document.write('Etiqueta'); w.document.write(code128Svg(barcode || partNumber, 320, 70)); w.document.write('

' + (barcode || partNumber) + '

'); w.document.write('

' + partNumber + '

'); w.document.write('

' + name + '

'); w.document.write(''); w.document.close(); w.print(); } function deleteItem(itemId) { if (!confirm('¿Eliminar este artículo del inventario? Se mantendrán los registros históricos.')) return; fetch(API + '/items/' + itemId, { method: 'DELETE', headers: getToken() ? { 'Authorization': 'Bearer ' + getToken() } : {} }).then(function(r) { return r.json(); }) .then(function(data) { if (data.error) { showToast(data.error, 'error', { title: 'Error al eliminar' }); return; } showToast('El artículo fue eliminado correctamente.', 'ok', { title: 'Eliminado' }); loadItems(currentPage); if (window.loadInventoryStats) window.loadInventoryStats(); }).catch(function() { showToast('No se pudo eliminar el artículo. Intenta de nuevo.', 'error', { title: 'Error' }); }); } function publishToMeli(itemId) { selectedItems.clear(); selectedItems.add(itemId); updateSelectionUI(); openMeliPublishModal(); } window.publishToMeli = publishToMeli; // ─── MercadoLibre Bulk Publish Modal ─────────────────────────────────── var meliPreviewData = {}; var meliCategoryAttrs = []; var meliCategorySearchTimeout; var meliCatItems = []; var meliCatActiveIndex = -1; window.openMeliPublishModal = function() { if (selectedItems.size === 0) { showToast('Selecciona al menos un producto', 'warn'); return; } document.getElementById('meliPublishModal').classList.add('is-open'); document.getElementById('meliPublishResult').innerHTML = ''; document.getElementById('meliCategoryId').value = ''; document.getElementById('meliCategorySearch').value = ''; document.getElementById('meliCategoryResults').innerHTML = ''; document.getElementById('meliAttrsSection').style.display = 'none'; document.getElementById('meliAttrsGrid').innerHTML = ''; meliCategoryAttrs = []; refreshMeliPublishPreview(); }; window.closeMeliPublishModal = function() { document.getElementById('meliPublishModal').classList.remove('is-open'); }; // ==== RESTAURADAS v4 (MELI + descuentos) ==== window.searchMeliCategories = function() { var q = document.getElementById('meliCategorySearch').value.trim(); var resultsDiv = document.getElementById('meliCategoryResults'); if (q.length < 2) { resultsDiv.innerHTML = ''; meliCatItems = []; meliCatActiveIndex = -1; return; } clearTimeout(meliCategorySearchTimeout); resultsDiv.innerHTML = '
Buscando...
'; meliCategorySearchTimeout = setTimeout(function() { fetch('/pos/api/marketplace-ext/categories?q=' + encodeURIComponent(q), { headers: { 'Authorization': 'Bearer ' + getToken(), 'Content-Type': 'application/json' } }) .then(function(r) { return r.json(); }) .then(function(data) { var cats = data.categories || []; meliCatItems = cats.slice(0, 10); meliCatActiveIndex = -1; if (!meliCatItems.length) { resultsDiv.innerHTML = '
Sin resultados
'; return; } var html = '
'; meliCatItems.forEach(function(c, idx) { html += '
' + '' + esc(c.category_name || c.category_id) + '' + '' + esc(c.category_id) + '' + '
'; }); html += '
'; resultsDiv.innerHTML = html; }) .catch(function() { resultsDiv.innerHTML = ''; meliCatItems = []; meliCatActiveIndex = -1; }); }, 300); }; window.handleMeliCatKeydown = function(e) { if (!meliCatItems.length) return; if (e.key === 'ArrowDown') { e.preventDefault(); meliCatActiveIndex = Math.min(meliCatActiveIndex + 1, meliCatItems.length - 1); highlightMeliCat(meliCatActiveIndex); } else if (e.key === 'ArrowUp') { e.preventDefault(); meliCatActiveIndex = Math.max(meliCatActiveIndex - 1, -1); highlightMeliCat(meliCatActiveIndex); } else if (e.key === 'Enter') { e.preventDefault(); if (meliCatActiveIndex >= 0) selectMeliCategoryIdx(meliCatActiveIndex); } else if (e.key === 'Escape') { document.getElementById('meliCategoryResults').innerHTML = ''; meliCatItems = []; meliCatActiveIndex = -1; } }; /* Cerrar dropdown al hacer click fuera */ document.addEventListener('click', function(e) { var field = document.getElementById('meliCategorySearch'); var results = document.getElementById('meliCategoryResults'); if (field && results && !field.contains(e.target) && !results.contains(e.target)) { results.innerHTML = ''; meliCatItems = []; meliCatActiveIndex = -1; } }); window.onMeliShippingChange = function() { var mode = document.getElementById('meliShippingMode').value; var costField = document.getElementById('meliShippingCostField'); if (costField) costField.style.display = (mode === 'custom') ? 'block' : 'none'; }; // ─── MELI publish helpers ────────────────────────────────────────────── // Contrato de `custom_data` esperado por validate_items / publish_items // (pos/services/marketplace_external_service.py): // { // titles: { "": "Título (<=60)" }, // prices: { "": 123.45 }, // stocks: { "": 3 }, // attributes: { "": [{ id: "BRAND", value_name: "Nissan" }] }, // shipping_cost: 150 // solo se usa si shipping_mode === 'custom' // } // Las claves son el id de inventario como string (el backend consulta con str(inv_id)). function _meliAttrValue(attr) { // Los atributos con lista cerrada se pintan como ' + ''; } html += '
' + imgHtml + '
' + '' + '
' + it.title.length + '/60
' + '
' + checks + '
' + '
' + '
' + '
' + '
' + '
'; }); container.innerHTML = html; scheduleMeliFeeEstimates(); }).catch(function() { container.innerHTML = '

Error de red

'; }); } window.selectMeliCategory = function(id, name) { document.getElementById('meliCategoryId').value = id; document.getElementById('meliCategorySearch').value = name; document.getElementById('meliCategoryResults').innerHTML = ''; meliCatItems = []; meliCatActiveIndex = -1; loadCategoryAttributes(id); scheduleMeliFeeEstimates(); }; window.selectMeliCategoryIdx = function(idx) { var c = meliCatItems[idx]; if (!c) return; selectMeliCategory(c.category_id, c.category_name || c.category_id); }; window.highlightMeliCat = function(idx) { meliCatActiveIndex = idx; var items = document.querySelectorAll('.meli-cat-item'); items.forEach(function(el, i) { el.classList.toggle('is-active', i === idx); }); }; window.loadCategoryAttributes = function(categoryId) { var grid = document.getElementById('meliAttrsGrid'); var section = document.getElementById('meliAttrsSection'); grid.innerHTML = '

Cargando atributos...

'; section.style.display = 'block'; fetch('/pos/api/marketplace-ext/categories/' + encodeURIComponent(categoryId) + '/attributes', { headers: { 'Authorization': 'Bearer ' + getToken() } }) .then(function(r){ return r.json(); }) .then(function(data) { meliCategoryAttrs = data.attributes || []; if (!meliCategoryAttrs.length) { grid.innerHTML = '

No hay atributos obligatorios adicionales.

'; return; } var html = ''; meliCategoryAttrs.forEach(function(attr) { var attrId = esc(attr.id); var attrName = esc(attr.name || attr.id); var inputHtml = ''; if (attr.values && attr.values.length) { // Some ML attributes (like BRAND) have a closed list but the API still // accepts free-text via value_name. Provide a select + "Other" fallback. var selectId = 'meliAttrSel-' + attrId; var otherId = 'meliAttrOther-' + attrId; inputHtml = '' + ''; } html += '
' + inputHtml + '
'; }); grid.innerHTML = html; }).catch(function() { grid.innerHTML = '

Error cargando atributos

'; }); }; window.onMeliAttrSelectChange = function(attrId) { var sel = document.getElementById('meliAttrSel-' + attrId); var other = document.getElementById('meliAttrOther-' + attrId); if (!sel || !other) return; if (sel.value === '__other__') { other.style.display = 'block'; other.focus(); } else { other.style.display = 'none'; other.value = ''; } }; window.updateMeliTitleCount = function(id) { var el = document.getElementById('meliTitle-' + id); var countEl = document.getElementById('meliTitleCount-' + id); if (el && countEl) countEl.textContent = el.value.length + '/60'; }; window.handleMeliImageUpload = function(itemId, input) { if (!input.files || !input.files[0]) return; var file = input.files[0]; var formData = new FormData(); formData.append('file', file); fetch('/pos/api/inventory/items/' + itemId + '/image', { method: 'POST', headers: { 'Authorization': 'Bearer ' + getToken() }, body: formData }).then(function(r){ return r.json(); }) .then(function(data) { if (data.image_url) { showToast('Imagen subida', 'success'); refreshMeliPublishPreview(); if (inventoryVS) inventoryVS.refresh(); } else { showToast(data.error || 'Error subiendo imagen', 'error'); } }).catch(function(){ showToast('Error de red', 'error'); }); }; // ─── Estimador de comisiones MeLi (por artículo) ─── var meliFeeTimeout; function fmtMoney(n) { return '$' + (parseFloat(n) || 0).toLocaleString('es-MX', {minimumFractionDigits: 2, maximumFractionDigits: 2}); } function updateMeliFeeEstimates() { var categoryId = (document.getElementById('meliCategoryId') || {}).value; var listingType = (document.getElementById('meliListingType') || {}).value || 'gold_special'; var ids = Object.keys(window.meliPreviewData || (typeof meliPreviewData !== 'undefined' ? meliPreviewData : {})); if (!ids.length) return; ids.forEach(function (id) { var feeEl = document.getElementById('meliFee-' + id); if (!feeEl) return; if (!categoryId) { feeEl.innerHTML = ''; return; } var priceEl = document.getElementById('meliPrice-' + id); var price = parseFloat(priceEl && priceEl.value) || 0; if (price <= 0) { feeEl.innerHTML = 'Ingresa un precio para estimar comisiones'; return; } feeEl.innerHTML = 'Calculando comisiones...'; fetch('/pos/api/marketplace-ext/listing-prices?price=' + encodeURIComponent(price) + '&category_id=' + encodeURIComponent(categoryId), { headers: { 'Authorization': 'Bearer ' + getToken() } }) .then(function (r) { return r.json().then(function (d) { return {ok: r.ok, data: d}; }); }) .then(function (res) { if (!res.ok || res.data.error) { feeEl.innerHTML = 'No se pudo estimar: ' + esc(res.data.error || 'error') + ''; return; } var offers = res.data.listing_prices || []; var offer = null; offers.forEach(function (o) { if (o.listing_type_id === listingType) offer = o; }); if (!offer && offers.length) offer = offers[0]; if (!offer || !offer.sale_fee_details) { feeEl.innerHTML = ''; return; } var fee = offer.sale_fee_details.gross_amount || 0; var pct = offer.sale_fee_details.percentage_fee || 0; var net = price - fee; feeEl.innerHTML = '💰 Comisión MeLi: ' + fmtMoney(fee) + ' (' + pct + '%)' + ' · Recibes: ' + fmtMoney(net) + '' + ' (estimado, sin costo de envío)'; }) .catch(function () { feeEl.innerHTML = ''; }); }); } function scheduleMeliFeeEstimates() { clearTimeout(meliFeeTimeout); meliFeeTimeout = setTimeout(updateMeliFeeEstimates, 500); } window.scheduleMeliFeeEstimates = scheduleMeliFeeEstimates; // ===================================================================== // EXPOSE GLOBALS (for onclick handlers in HTML) // ===================================================================== window._loadItems = function (p) { loadItems(p); }; window.loadItems = function (p, q) { loadItems(p, q); }; window.viewHistory = viewHistory; window.viewProductDetail = viewProductDetail; window.uploadItemImage = uploadItemImage; window.deleteItemImage = deleteItemImage; window.closeHistoryModal = closeHistoryModal; window.showCreateModal = showCreateModal; window.closeCreateModal = closeCreateModal; window.createItem = createItem; window.showPurchaseModal = showPurchaseModal; window.showPurchaseModalForItem = showPurchaseModalForItem; window.closePurchaseModal = closePurchaseModal; window.recordPurchase = recordPurchase; window.showAdjustmentModal = showAdjustmentModal; window.closeAdjustmentModal = closeAdjustmentModal; window.recordAdjustment = recordAdjustment; window.showTransferModal = showTransferModal; window.closeTransferModal = closeTransferModal; window.recordTransfer = recordTransfer; window.showCountModal = showCountModal; window.closeCountModal = closeCountModal; window.addCountLine = addCountLine; window.startPhysicalCount = startPhysicalCount; window.approvePhysicalCount = approvePhysicalCount; window.cancelDraft = cancelDraft; window.loadAlerts = loadAlerts; window.printBarcode = printBarcode; window.switchTransferTab = switchTransferTab; window.transferLineQty = transferLineQty; window.transferLineRemove = transferLineRemove; window.transferDocSave = transferDocSave; window.transferDocSend = transferDocSend; window.transferDocReceive = transferDocReceive; window.transferDocCancel = transferDocCancel; window.openTransferDoc = openTransferDoc; window.loadTransferDocs = loadTransferDocs; window.countQty = countQty; window.countRemove = countRemove; window.Inventory = window.Inventory || {}; window.Inventory = Object.assign(window.Inventory, { switchTransferTab, transferLineQty, transferLineRemove, transferDocSave, transferDocSend, transferDocReceive, transferDocCancel, openTransferDoc, countQty, countRemove, transferAddByCode, countAddByCode, }); window.deleteItem = deleteItem; window.autoMatchCompat = autoMatchCompat; window.removeCompat = removeCompat; // ─── Product Timeline ────────────────────────────────────────── window.showProductTimeline = function(itemId) { var modal = document.getElementById('productTimelineModal'); var body = document.getElementById('productTimelineBody'); body.innerHTML = '
'; modal.classList.add('is-open'); apiFetch(API + '/items/' + itemId + '/history').then(function(data) { var history = (data && data.data) ? data.data : []; var html = '
'; html += '
Producto creado
Registro inicial en inventario
'; history.forEach(function(h) { var color = h.quantity > 0 ? 'timeline__dot--green' : (h.quantity < 0 ? 'timeline__dot--red' : 'timeline__dot--blue'); var title = (h.type || 'Movimiento') + ' · ' + (h.quantity > 0 ? '+' : '') + h.quantity + ' unidades'; html += '
' + '
' + esc(h.date) + ' · ' + esc(h.employee) + '
' + '
' + esc(title) + '
' + (h.notes ? '
' + esc(h.notes) + '
' : '') + '
'; }); html += '
'; body.innerHTML = html; }); }; // ─── Image Comparator ────────────────────────────────────────── window.showImageCompare = function(imageUrl) { var modal = document.getElementById('imageCompareModal'); document.getElementById('imgCompareNew').src = imageUrl + '?t=' + Date.now(); document.getElementById('imgCompareOld').src = imageUrl + '?t=' + (Date.now() - 1); modal.classList.add('is-open'); setTimeout(function() { if (typeof initImageComparator === 'function') initImageComparator('#imgCompareContainer'); }, 100); }; // ─── Infinite Scroll ─────────────────────────────────────────── var _infiniteScrollInstance = null; function setupInfiniteScroll() { if (_infiniteScrollInstance) _infiniteScrollInstance.disconnect(); var sentinel = document.createElement('div'); sentinel.id = 'inventoryScrollSentinel'; sentinel.style.cssText = 'height:1px;'; var wrapper = document.querySelector('.table-wrapper'); if (wrapper) wrapper.appendChild(sentinel); _infiniteScrollInstance = new InfiniteScroll({ sentinelParent: wrapper, onLoad: function(done) { if (!currentSearch && currentPage < (window._inventoryTotalPages || 999)) { loadItems(currentPage + 1); } if (done) done(); } }); } // ─── Saved Filters ───────────────────────────────────────────── function renderSavedFilters() { var container = document.getElementById('savedFiltersContainer'); if (!container) return; SavedFilters.renderChips('savedFiltersContainer', function(filters) { if (filters.search) { var el = document.getElementById('productSearch'); if (el) { el.value = filters.search; loadItems(1, filters.search); } } }); } // ===================================================================== // INIT — load stock on page load // ===================================================================== loadItems(1); renderSavedFilters(); wirePurchaseSearch(); })();