// /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';
var token = localStorage.getItem('pos_token');
if (!token) { window.location.href = '/pos/login'; return; }
var headers = { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' };
var currentPage = 1;
var currentSearch = '';
var draftCountId = null;
var inventoryVS = null;
var compatSource = 'both'; // default, loaded from config
// Load compatibility source setting
(function loadCompatSource() {
fetch('/pos/api/config/vehicle-compat-source', { headers: { 'Authorization': 'Bearer ' + token } })
.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: headers }, 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;
}
// --- 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 ' + token, '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) + ' ' +
'' +
'Historial ' +
'Entrada ' +
'ML ' +
'Etiqueta ' +
'Eliminar ' +
' ';
}
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);
apiFetch(API + '/items?' + params.toString()).then(function (data) {
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 ? 'Limpiar búsqueda ' : 'Crear producto '
}) + ' ';
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 ';
}
});
}
// 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() {
var sel = document.getElementById('newCategory');
if (!sel) return;
apiFetch(API + '/categories').then(function(data) {
if (!data || !data.categories) return;
sel.innerHTML = 'Selecciona categoría ';
data.categories.forEach(function(c) {
sel.innerHTML += '' + esc(c.name) + ' ';
});
});
}
window.loadCategories = loadCategories;
function onCategoryChange(categoryId) {
var subSel = document.getElementById('newSubcategory');
if (!subSel) return;
if (!categoryId) {
subSel.innerHTML = 'Selecciona categoría primero ';
subSel.disabled = true;
return;
}
apiFetch(API + '/categories/' + categoryId + '/subcategories').then(function(data) {
if (!data || !data.subcategories) return;
subSel.innerHTML = 'Selecciona subcategoría ';
data.subcategories.forEach(function(s) {
subSel.innerHTML += '' + esc(s.name) + ' ';
});
subSel.disabled = false;
});
}
window.onCategoryChange = onCategoryChange;
function showCreateModal() {
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 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 = '';
var catSel = document.getElementById('newCategory');
var subSel = document.getElementById('newSubcategory');
if (catSel) catSel.innerHTML = 'Selecciona categoría ';
if (subSel) { subSel.innerHTML = 'Selecciona categoría primero '; subSel.disabled = true; }
}
function createItem() {
var elPrice2 = document.getElementById('newPrice2');
var elPrice3 = document.getElementById('newPrice3');
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: elPrice2 ? (parseFloat(elPrice2.value) || 0) : 0,
price_3: elPrice3 ? (parseFloat(elPrice3.value) || 0) : 0,
min_stock: parseInt(document.getElementById('newMinStock').value) || 0,
initial_stock: parseInt(document.getElementById('newInitialStock').value) || 0,
location: document.getElementById('newLocation').value.trim(),
sku_aliases: []
};
var sku2 = document.getElementById('newSku2').value.trim();
var sku3 = document.getElementById('newSku3').value.trim();
var categoryId = document.getElementById('newCategory').value;
var subcategoryId = document.getElementById('newSubcategory').value;
if (sku2) data.sku_aliases.push({sku: sku2, label: 'Alternativo 1'});
if (sku3) data.sku_aliases.push({sku: sku3, label: 'Alternativo 2'});
if (subcategoryId) {
data.category_id = parseInt(subcategoryId);
} else if (categoryId) {
data.category_id = parseInt(categoryId);
}
if (!data.part_number || !data.name) {
document.getElementById('createResult').innerHTML = 'Numero de parte y nombre son obligatorios ';
return;
}
apiFetch(API + '/items', { method: 'POST', body: JSON.stringify(data) }).then(function (result) {
if (result && result.id) {
var msg = 'Creado ID ' + result.id + ' | Barcode: ' + result.barcode;
if (result.vehicle_compatibilities_added > 0) {
msg += ' | ' + result.vehicle_compatibilities_added + ' vehiculo(s) asignado(s) por IA';
}
document.getElementById('createResult').innerHTML = '' + msg + ' ';
loadItems(currentPage);
// Close modal, clear form, refresh badges
closeCreateModal();
['newPartNumber','newName','newBrand','newBarcode','newSku2','newSku3','newCost','newPrice1','newMinStock','newInitialStock','newLocation'].forEach(function(id) {
var el = document.getElementById(id);
if (el) el.value = '';
});
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 ' + token,
'X-Import-Mode': mode,
'X-Import-Strategy': strategy
},
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 + '): ';
data.warnings.forEach(function(w) {
html += '' + esc(w) + ' ';
});
html += ' ';
}
resultEl.innerHTML = html;
loadItems(currentPage);
if (window.loadInventoryStats) window.loadInventoryStats();
}).catch(function(err) {
resultEl.innerHTML = 'Error de red: ' + esc(err.message) + ' ';
});
}
window.submitBulkImport = submitBulkImport;
// =====================================================================
// 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)
// =====================================================================
function showTransferModal() {
document.getElementById('transferModal').classList.add('is-open');
}
function closeTransferModal() {
document.getElementById('transferModal').classList.remove('is-open');
document.getElementById('transferResult').innerHTML = '';
}
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') + ' ';
}
});
}
// =====================================================================
// 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');
}
function addCountLine() {
var container = document.getElementById('countLines');
var row = document.createElement('div');
row.className = 'count-row';
row.innerHTML =
' ' +
' ' +
'Quitar ';
container.appendChild(row);
}
function startPhysicalCount() {
var rows = document.querySelectorAll('#countLines .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; }
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 += 'ID Esperado Contado Diferencia ';
(result.results || []).forEach(function (r) {
var color = r.difference === 0 ? 'var(--color-success)' : (r.difference < 0 ? 'var(--color-error)' : 'var(--color-warning)');
html += '' + r.inventory_id + ' ' + r.expected + ' ' + r.counted + ' ' + (r.difference > 0 ? '+' : '') + r.difference + ' ';
});
html += '
';
html += '';
html += 'Aprobar y aplicar ajustes ';
html += 'Cancelar borrador ';
html += '
';
document.getElementById('countResults').innerHTML = html;
});
}
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 ';
}
// =====================================================================
// ALERTS (panel-alertas)
// =====================================================================
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 renderAlertSection(title, alerts, level, badgeClass) {
if (!alerts.length) return '';
var initialLimit = 30;
var showAll = window._alertsShowAll && window._alertsShowAll[level];
var display = showAll ? alerts : alerts.slice(0, initialLimit);
var remaining = alerts.length - display.length;
var html = '' + title + '
' + alerts.length + ' ';
html += '';
display.forEach(function (a) {
var icon = a.type === 'zero' ? 'AGOTADO' : (a.type === 'low' ? 'BAJO' : a.type.toUpperCase());
html += buildAlertCard(a, icon, level);
});
html += '
';
if (remaining > 0) {
html += '' +
'Ver ' + remaining + ' más ' +
'
';
}
return html;
}
window._showMoreAlerts = function(level) {
window._alertsShowAll = window._alertsShowAll || {};
window._alertsShowAll[level] = true;
loadAlerts();
};
function buildAlertCard(a, icon, level) {
var cls = level === 'critical' ? 'alert-card--critical' : (level === 'warning' ? 'alert-card--warning' : 'alert-card--info');
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 + '
' +
'
';
}
// =====================================================================
// HISTORY MODAL
// =====================================================================
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 = 'Fecha Tipo Cantidad Costo Empleado Notas ';
history.forEach(function (h) {
var qtyColor = h.quantity > 0 ? 'var(--color-success)' : 'var(--color-error)';
html += '' +
'' + esc(h.date) + ' ' +
'' + esc(h.type) + ' ' +
'' + (h.quantity > 0 ? '+' : '') + h.quantity + ' ' +
'' + (h.cost ? '$' + fmt(h.cost) : '—') + ' ' +
'' + esc(h.employee) + ' ' +
'' + esc(h.notes) + ' ' +
' ';
});
html += '
';
}
document.getElementById('historyContent').innerHTML = html;
document.getElementById('historyModal').classList.add('is-open');
});
}
function closeHistoryModal() {
document.getElementById('historyModal').classList.remove('is-open');
}
// =====================================================================
// DELETE ITEM
// =====================================================================
function deleteItem(itemId) {
if (!confirm('¿Eliminar este artículo del inventario? Se mantendrán los registros históricos.')) return;
var token = localStorage.getItem('pos_token') || '';
fetch(API + '/items/' + itemId, {
method: 'DELETE',
headers: token ? { 'Authorization': 'Bearer ' + token } : {}
}).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' }); });
}
// =====================================================================
// MERCADOLIBRE PUBLISH
// =====================================================================
function publishToMeli(itemId) {
selectedItems.clear();
selectedItems.add(itemId);
updateSelectionUI();
openMeliPublishModal();
}
window.publishToMeli = publishToMeli;
// ─── MercadoLibre Bulk Publish Modal ───────────────────────────────────
var meliPreviewData = {};
var meliCategoryAttrs = [];
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');
};
function refreshMeliPublishPreview() {
var container = document.getElementById('meliPublishItemsPreview');
var countEl = document.getElementById('meliPublishSelectedCount');
countEl.textContent = selectedItems.size + ' producto(s) seleccionado(s)';
container.innerHTML = 'Cargando verificaciones...
';
var ids = Array.from(selectedItems);
fetch('/pos/api/marketplace-ext/inventory-check', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
body: JSON.stringify({ inventory_ids: ids })
}).then(function(r){ return r.json(); })
.then(function(data) {
if (!data.items) { container.innerHTML = 'Error cargando preview
'; return; }
var html = '';
data.items.forEach(function(it) {
if (!it.exists) {
html += 'Item #' + it.inventory_id + ' no encontrado
';
return;
}
meliPreviewData[it.inventory_id] = it;
var checks = '';
checks += '' + (it.has_image ? '✅' : '❌') + ' Imagen ';
checks += '' + (it.has_stock ? '✅' : '❌') + ' Stock ';
checks += '' + (it.has_price ? '✅' : '❌') + ' Precio ';
if (it.already_published) {
checks += '✅ Ya publicado (' + esc(it.existing_listing.status) + ') ';
}
var imgSrc = it.image_url || '';
var imgHtml = imgSrc ? ' ' : 'Sin img
';
if (!it.has_image) {
imgHtml = '';
}
html += '' +
imgHtml +
'
' +
'
' +
'
' + it.title.length + '/60
' +
'
' + checks + '
' +
'
' +
'
Precio
' +
'
Stock
' +
'
';
});
container.innerHTML = html;
}).catch(function() { container.innerHTML = 'Error de red
'; });
}
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 ' + token },
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'); });
};
var meliCategorySearchTimeout;
var meliCatItems = [];
var meliCatActiveIndex = -1;
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 = '';
meliCategorySearchTimeout = setTimeout(function() {
fetch('/pos/api/marketplace-ext/categories?q=' + encodeURIComponent(q), { headers: { 'Authorization': 'Bearer ' + token, '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 = ''; 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.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.selectMeliCategoryIdx = function(idx) {
var c = meliCatItems[idx];
if (!c) return;
selectMeliCategory(c.category_id, c.category_name || c.category_id);
};
window.selectMeliCategory = function(id, name) {
document.getElementById('meliCategoryId').value = id;
document.getElementById('meliCategorySearch').value = name;
document.getElementById('meliCategoryResults').innerHTML = '';
meliCatItems = [];
meliCatActiveIndex = -1;
loadCategoryAttributes(id);
};
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 ' + token } })
.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 = '' +
'Selecciona ' + attrName + ' ' +
attr.values.map(function(v) { return '' + esc(v.name) + ' '; }).join('') +
'Otra marca (escribir)... ' +
' ' +
' ';
}
html += '' + attrName + (attr.tags && attr.tags.required ? ' *' : '') + ' ' + 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.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';
};
function _collectMeliCustomData() {
var ids = Array.from(selectedItems);
var customData = { titles: {}, prices: {}, stocks: {}, attributes: {} };
var mode = document.getElementById('meliShippingMode').value;
if (mode === 'custom') {
var costEl = document.getElementById('meliShippingCost');
if (costEl) customData.shipping_cost = parseFloat(costEl.value) || 0;
}
ids.forEach(function(id) {
var titleEl = document.getElementById('meliTitle-' + id);
var priceEl = document.getElementById('meliPrice-' + id);
var stockEl = document.getElementById('meliStock-' + id);
if (titleEl) customData.titles[id] = titleEl.value;
if (priceEl) customData.prices[id] = parseFloat(priceEl.value);
if (stockEl) customData.stocks[id] = parseInt(stockEl.value);
var attrs = [];
meliCategoryAttrs.forEach(function(attr) {
var val = '';
var sel = document.getElementById('meliAttrSel-' + attr.id);
if (sel) {
if (sel.value === '__other__') {
var otherEl = document.getElementById('meliAttrOther-' + attr.id);
val = otherEl ? otherEl.value : '';
} else {
val = sel.value;
}
} else {
var el = document.getElementById('meliAttr-' + attr.id);
if (el) val = el.value;
}
if (val) {
attrs.push({ id: attr.id, value_name: val });
}
});
if (attrs.length) customData.attributes[id] = attrs;
});
return customData;
}
window.validateMeliPublish = function() {
var categoryId = document.getElementById('meliCategoryId').value.trim();
if (!categoryId) { document.getElementById('meliPublishResult').innerHTML = 'Selecciona una categoría de MercadoLibre '; return; }
var listingType = document.getElementById('meliListingType').value;
var shippingMode = document.getElementById('meliShippingMode').value;
var ids = Array.from(selectedItems);
var resultEl = document.getElementById('meliPublishResult');
var btn = document.getElementById('meliValidateBtn');
btn.disabled = true;
resultEl.innerHTML = 'Validando con MercadoLibre... ';
fetch('/pos/api/marketplace-ext/listings/validate', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
body: JSON.stringify({
inventory_ids: ids,
category_id: categoryId,
listing_type: listingType,
shipping_mode: shippingMode,
custom_data: _collectMeliCustomData()
})
}).then(function(r){ return r.json(); })
.then(function(data) {
btn.disabled = false;
if (data.error) { resultEl.innerHTML = 'Error: ' + esc(data.error) + ' '; return; }
var valid = (data.valid || []);
var invalid = (data.invalid || []);
var html = '✅ ' + valid.length + ' válido(s) · ❌ ' + invalid.length + ' inválido(s)
';
// Show earnings estimate table for valid items
if (valid.length > 0) {
html += '';
html += '
💰 Estimado de ganancia (después de comisiones ML)
';
html += '
';
html += 'Item Precio Comisión Recibes ';
valid.forEach(function(v) {
var price = v.price || 0;
var fee = v.fee_amount || 0;
var net = v.net_amount || 0;
var feePct = v.fee_pct || 0;
html += '';
html += '#' + esc(v.inventory_id) + ' ';
html += '$' + price.toFixed(2) + ' ';
html += '-$' + fee.toFixed(2) + ' (' + feePct.toFixed(1) + '%) ';
html += '$' + net.toFixed(2) + ' ';
html += ' ';
});
html += '
';
if (valid.some(function(v){ return v.fee_source === 'approximation'; })) {
html += '
* Comisión estimada. El monto real puede variar según ML.
';
}
html += '
';
}
if (invalid.length) {
html += '';
invalid.forEach(function(f) {
html += 'Item #' + esc(f.inventory_id) + ': ' + esc(f.error) + ' ';
});
html += ' ';
}
resultEl.innerHTML = html;
}).catch(function(e) { btn.disabled = false; resultEl.innerHTML = 'Error: ' + esc(e.message) + ' '; });
};
function _renderPublishResult(data, resultEl) {
var success = (data.success || []).length;
var failedList = data.failed || [];
var failed = failedList.length;
var hasModeError = failedList.some(function(f) { return (f.error || '').toLowerCase().indexOf('user has not mode') !== -1; });
var html = '✅ ' + success + ' publicado(s) · ❌ ' + failed + ' fallo(s)
';
if (hasModeError) {
html += '' +
'
⚠️ Tu cuenta de MercadoLibre no tiene modos de envío configurados. ' +
'Esto es un requisito de MercadoLibre, no de Nexus.
' +
'Pasos para solucionarlo:
' +
'1. Entrá a
mercadolibre.com.mx con la cuenta de vendedor
' +
'2. Andá a
Vender > Configuración de envíos ' +
'3. Completá tu dirección de retiro y activá al menos un método de envío
' +
'4. Si no te aparece la opción, contactá a
soporte de MercadoLibre ' +
'
Nota: Algunas cuentas nuevas necesitan verificar identidad antes de poder configurar envíos. ' +
'
';
}
if (failedList.length) {
html += '';
failedList.forEach(function(f) {
html += 'Item #' + esc(f.inventory_id) + ': ' + esc(f.error) + ' ';
});
html += ' ';
}
resultEl.innerHTML = html;
if (success > 0) {
selectedItems.clear();
updateSelectionUI();
if (inventoryVS) inventoryVS.refresh();
}
}
function _pollMeliAsync(taskId, resultEl, btn) {
var attempts = 0;
var maxAttempts = 60; // 2 min
var interval = setInterval(function() {
attempts++;
fetch('/pos/api/marketplace-ext/listings/async/' + encodeURIComponent(taskId), { headers: { 'Authorization': 'Bearer ' + token } })
.then(function(r){ return r.json(); })
.then(function(data) {
if (data.status === 'done') {
clearInterval(interval);
btn.disabled = false;
_renderPublishResult(data.result || {}, resultEl);
setTimeout(function() { closeMeliPublishModal(); }, 3000);
} else if (attempts >= maxAttempts) {
clearInterval(interval);
btn.disabled = false;
resultEl.innerHTML = 'Timeout esperando resultado. Revisa la pestaña Publicaciones más tarde. ';
} else {
resultEl.innerHTML = 'Publicando en segundo plano... (' + attempts + 's) ';
}
}).catch(function() {
clearInterval(interval);
btn.disabled = false;
resultEl.innerHTML = 'Error consultando progreso ';
});
}, 2000);
}
window.executeMeliPublish = function() {
var categoryId = document.getElementById('meliCategoryId').value.trim();
if (!categoryId) { document.getElementById('meliPublishResult').innerHTML = 'Selecciona una categoría de MercadoLibre '; return; }
var listingType = document.getElementById('meliListingType').value;
var shippingMode = document.getElementById('meliShippingMode').value;
var ids = Array.from(selectedItems);
var resultEl = document.getElementById('meliPublishResult');
var btn = document.getElementById('meliPublishBtn');
btn.disabled = true;
var useAsync = ids.length > 3;
var endpoint = useAsync ? '/pos/api/marketplace-ext/listings/async' : '/pos/api/marketplace-ext/listings';
resultEl.innerHTML = '' + (useAsync ? 'Encolando ' : 'Publicando ') + ids.length + ' producto(s)... ';
fetch(endpoint, {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
body: JSON.stringify({
inventory_ids: ids,
category_id: categoryId,
listing_type: listingType,
shipping_mode: shippingMode,
custom_data: _collectMeliCustomData()
})
}).then(function(r) { return r.json(); })
.then(function(data) {
if (data.error) { btn.disabled = false; resultEl.innerHTML = 'Error: ' + esc(data.error) + ' '; return; }
if (useAsync && data.task_id) {
_pollMeliAsync(data.task_id, resultEl, btn);
} else {
btn.disabled = false;
_renderPublishResult(data, resultEl);
if ((data.success || []).length > 0) {
setTimeout(function() { closeMeliPublishModal(); }, 2500);
}
}
}).catch(function(e) { btn.disabled = false; resultEl.innerHTML = 'Error: ' + esc(e.message) + ' '; });
};
// =====================================================================
// BARCODE LABEL PRINT
// =====================================================================
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();
}
// =====================================================================
// PRODUCT DETAIL MODAL (shows item info + movement history)
// =====================================================================
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 ' + token },
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 ' + token }
})
.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 += 'Detalle ';
html += 'Compatibilidad ';
html += '
';
// Detail panel
html += '';
// Product image section
html += '
';
if (data.image_url) {
html += '
';
html += '
';
html += 'Cambiar imagen ';
html += 'Eliminar imagen ';
html += '
';
} else {
html += '
';
html += '
';
html += '
Sin imagen
';
html += '
';
html += '
Subir imagen ';
}
html += '
';
html += '
';
// Action buttons
html += '
';
html += '📅 Timeline ';
if (data.image_url) {
html += '🖼️ Comparar ';
}
html += '📜 Historial ';
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 ' + token } })
.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 ' + token } })
.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 += '
Bodega Stock Precio Ubicacion ';
bodegas.forEach(function(b) {
html2 += '' + esc(b.business_name || b.bodega || '') + ' ' + (b.stock || b.stock_quantity || 0) + ' $' + fmt(b.price || 0) + ' ' + esc(b.location || b.warehouse_location || '') + ' ';
});
html2 += '
';
}
if (alternatives && alternatives.length > 0) {
html2 += '
Partes Equivalentes (Aftermarket):
';
html2 += '
No. Parte Fabricante Nombre ';
alternatives.forEach(function(a) {
html2 += '' + esc(a.part_number || a.cross_reference_number || '') + ' ' + esc(a.manufacturer || a.source_ref || '') + ' ' + esc(a.name || a.name_aftermarket_parts || '') + ' ';
});
html2 += '
';
}
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 += '
Agregar compatibilidad ';
// Auto-match button
var btnLabel = compatSource === 'qwen' ? 'Auto-Match con IA (QWEN)' : (compatSource === 'both' ? 'Auto-Match (TecDoc + IA)' : 'Auto-Match por TecDoc');
html += '
' + btnLabel + '
';
html += '
';
// Load SKU aliases
(function loadSkuAliases() {
fetch('/pos/api/inventory/items/' + itemId + '/skus', { headers: { 'Authorization': 'Bearer ' + token } })
.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 += 'SKU Etiqueta ';
list.forEach(function(a) {
html2 += '' + esc(a.sku) + ' ' + esc(a.label || '-') + ' ';
html2 += 'Quitar ';
});
html2 += '
';
} else {
html2 += 'Sin SKU alternativos.
';
}
html2 += '';
html2 += ' ';
html2 += ' ';
html2 += 'Agregar ';
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 ' + token } })
.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 += 'Marca Modelo Ano Motor Origen ';
list.forEach(function(c) {
var sourceLabel = c.source === 'qwen_ai' ? 'IA ' : (c.source === 'auto_match' ? 'TecDoc ' : esc(c.source || ''));
html2 += '' + esc(c.brand || '') + ' ' + esc(c.model || '') + ' ' + esc(c.year || '') + ' ' + esc(c.engine || '') + ' ' + sourceLabel + ' ';
html2 += 'Quitar ';
});
html2 += '
';
} 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 ' + token } })
.then(function(r) { return r.json(); })
.then(function(d) {
var sel = document.getElementById('manualMake');
if (!sel) return;
var opts = 'Selecciona marca ';
(d.makes || []).forEach(function(m) { opts += '' + esc(m.name) + ' '; });
sel.innerHTML = opts;
sel.disabled = false;
})
.catch(function() {
var sel = document.getElementById('manualMake');
if (sel) { sel.innerHTML = 'Error al cargar '; }
});
})();
// Movement history
html += 'Historial de Movimientos
';
if (!history.length) {
html += 'Sin movimientos
';
} else {
html += 'Fecha Tipo Cantidad Costo Empleado Notas ';
history.forEach(function (h) {
var qtyColor = h.quantity > 0 ? 'var(--color-success)' : 'var(--color-error)';
html += '' +
'' + esc(h.date) + ' ' +
'' + esc(h.type) + ' ' +
'' + (h.quantity > 0 ? '+' : '') + h.quantity + ' ' +
'' + (h.cost ? '$' + fmt(h.cost) : '\u2014') + ' ' +
'' + esc(h.employee) + ' ' +
'' + esc(h.notes) + ' ' +
' ';
});
html += '
';
}
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 ' + token }
}).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 ' + token }
}).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 = 'Cargando... ';
modelSel.disabled = true;
yearSel.innerHTML = 'Selecciona modelo ';
yearSel.disabled = true;
engineSel.innerHTML = 'Selecciona ano ';
engineSel.disabled = true;
if (!brandId) {
modelSel.innerHTML = 'Selecciona marca ';
return;
}
fetch('/pos/api/inventory/vehicles/models?brand_id=' + brandId, { headers: { 'Authorization': 'Bearer ' + token } })
.then(function(r) { return r.json(); })
.then(function(d) {
var opts = 'Selecciona modelo ';
(d.models || []).forEach(function(m) { opts += '' + esc(m.name) + ' '; });
modelSel.innerHTML = opts;
modelSel.disabled = false;
})
.catch(function() { modelSel.innerHTML = 'Error '; });
};
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 = 'Cargando... ';
yearSel.disabled = true;
engineSel.innerHTML = 'Selecciona ano ';
engineSel.disabled = true;
if (!modelId) {
yearSel.innerHTML = 'Selecciona modelo ';
return;
}
fetch('/pos/api/inventory/vehicles/years?model_id=' + modelId, { headers: { 'Authorization': 'Bearer ' + token } })
.then(function(r) { return r.json(); })
.then(function(d) {
var opts = 'Selecciona ano ';
(d.years || []).forEach(function(y) { opts += '' + y.year + ' '; });
yearSel.innerHTML = opts;
yearSel.disabled = false;
})
.catch(function() { yearSel.innerHTML = 'Error '; });
};
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 = 'Cargando... ';
engineSel.disabled = true;
if (!modelId || !yearId) {
engineSel.innerHTML = 'Selecciona ano ';
return;
}
fetch('/pos/api/inventory/vehicles/engines?model_id=' + modelId + '&year_id=' + yearId, { headers: { 'Authorization': 'Bearer ' + token } })
.then(function(r) { return r.json(); })
.then(function(d) {
var opts = 'Selecciona motor ';
(d.engines || []).forEach(function(e) { opts += '' + esc(e.name + (e.code ? ' (' + e.code + ')' : '')) + ' '; });
engineSel.innerHTML = opts;
engineSel.disabled = false;
})
.catch(function() { engineSel.innerHTML = 'Error '; });
};
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 ' + token, '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 ' + token, '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 ' + token }
}).then(function(r) { return r.json(); })
.then(function() {
viewProductDetail(itemId);
}).catch(function() { alert('Error al eliminar SKU'); });
};
// =====================================================================
// 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.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();
})();