- Replace ID Producto input with autocomplete search by name/part number/barcode - Support Enter key for barcode/part number exact match - Keep hidden inventory_id field for API compatibility - Bump inventory.js cache version
2134 lines
120 KiB
JavaScript
2134 lines
120 KiB
JavaScript
// /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 = '<div class="skeleton skeleton--text" style="width:80%;"></div>';
|
|
['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 '<tr style="cursor:pointer;" onclick="viewProductDetail(' + it.id + ')">' +
|
|
'<td onclick="event.stopPropagation();"><input type="checkbox" class="item-checkbox" data-id="' + it.id + '" ' + isChecked + ' onclick="event.stopPropagation();toggleItemSelection(' + it.id + ')"></td>' +
|
|
'<td class="td--mono" style="font-size:var(--text-caption);color:var(--color-text-muted);">' + it.id + '</td>' +
|
|
'<td class="td--mono">' + esc(it.barcode) + '</td>' +
|
|
'<td class="td--mono">' + esc(it.part_number) + '</td>' +
|
|
'<td class="td--primary">' + esc(it.name) + '</td>' +
|
|
'<td>' + esc(it.brand) + '</td>' +
|
|
'<td style="text-align:right" class="td--primary">' + it.stock + '</td>' +
|
|
'<td style="text-align:right" class="td--amount">$' + fmt(it.cost) + '</td>' +
|
|
'<td style="text-align:right" class="td--amount">$' + fmt(it.price_1) + '</td>' +
|
|
'<td style="text-align:right" class="td--amount">$' + fmt(it.price_2) + '</td>' +
|
|
'<td style="text-align:right" class="td--amount">$' + fmt(it.price_3) + '</td>' +
|
|
'<td>' + esc(it.location) + '</td>' +
|
|
'<td>' +
|
|
'<button class="btn btn--ghost btn--sm" onclick="event.stopPropagation();viewHistory(' + it.id + ')">Historial</button> ' +
|
|
'<button class="btn btn--ghost btn--sm" style="color:var(--color-accent);" onclick="event.stopPropagation();showPurchaseModalForItem(' + it.id + ')">Entrada</button> ' +
|
|
'<button class="btn btn--sm btn--meli" onclick="event.stopPropagation();publishToMeli(' + it.id + ')">ML</button> ' +
|
|
'<button class="btn btn--ghost btn--sm" onclick="event.stopPropagation();printBarcode(\'' + esc(it.barcode) + '\',\'' + esc(it.part_number) + '\',\'' + esc(it.name) + '\')">Etiqueta</button> ' +
|
|
'<button class="btn btn--ghost btn--sm" style="color:var(--color-error);" onclick="event.stopPropagation();deleteItem(' + it.id + ')">Eliminar</button>' +
|
|
'</td></tr>';
|
|
}
|
|
|
|
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 = '<tr><td colspan="12">' + renderEmptyState({
|
|
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>',
|
|
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 ? '<button class="btn btn--ghost btn--sm" onclick="document.getElementById(\'productSearch\').value=\'\';loadItems(1,\'\')">Limpiar búsqueda</button>' : '<button class="btn btn--primary btn--sm" onclick="openCreateModal()">Crear producto</button>'
|
|
}) + '</td></tr>';
|
|
document.getElementById('productPagination').innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
if (!inventoryVS) {
|
|
inventoryVS = new VirtualScroll({
|
|
container: tbody,
|
|
rowHeight: 48,
|
|
buffer: 3,
|
|
renderRow: renderInventoryRow,
|
|
emptyHtml: '<tr><td colspan="12">' + renderEmptyState({ title: 'Sin productos', subtitle: 'El inventario está vacío.' }) + '</td></tr>'
|
|
});
|
|
}
|
|
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 =
|
|
'<div class="pagination">' +
|
|
'<button class="page-btn" ' + (pg.page <= 1 ? 'disabled' : 'onclick="window._loadItems(' + (pg.page - 1) + ')"') + '>‹</button>' +
|
|
'<span style="padding:0 var(--space-2);font-size:var(--text-body-sm);color:var(--color-text-muted);">' + pg.page + ' / ' + pg.total_pages + ' (' + pg.total + ' productos)</span>' +
|
|
'<button class="page-btn" ' + (pg.page >= pg.total_pages ? 'disabled' : 'onclick="window._loadItems(' + (pg.page + 1) + ')"') + '>›</button>' +
|
|
'</div>';
|
|
} else {
|
|
pgEl.innerHTML = '<span style="font-size:var(--text-body-sm);color:var(--color-text-muted);">' + (pg.total || 0) + ' productos</span>';
|
|
}
|
|
});
|
|
}
|
|
|
|
// 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 = '<option value="">Selecciona categoría</option>';
|
|
data.categories.forEach(function(c) {
|
|
sel.innerHTML += '<option value="' + c.id + '">' + esc(c.name) + '</option>';
|
|
});
|
|
});
|
|
}
|
|
window.loadCategories = loadCategories;
|
|
|
|
function onCategoryChange(categoryId) {
|
|
var subSel = document.getElementById('newSubcategory');
|
|
if (!subSel) return;
|
|
if (!categoryId) {
|
|
subSel.innerHTML = '<option value="">Selecciona categoría primero</option>';
|
|
subSel.disabled = true;
|
|
return;
|
|
}
|
|
apiFetch(API + '/categories/' + categoryId + '/subcategories').then(function(data) {
|
|
if (!data || !data.subcategories) return;
|
|
subSel.innerHTML = '<option value="">Selecciona subcategoría</option>';
|
|
data.subcategories.forEach(function(s) {
|
|
subSel.innerHTML += '<option value="' + s.id + '">' + esc(s.name) + '</option>';
|
|
});
|
|
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 = '<span style="color:var(--color-text-muted);">Consultando IA...</span>';
|
|
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 = '<span style="color:var(--color-accent);font-size:var(--text-caption);">Sugerido por IA: ' + esc(parts.join(' | ')) + '</span>';
|
|
} else {
|
|
resultEl.innerHTML = '<span style="color:var(--color-text-muted);font-size:var(--text-caption);">IA no pudo identificar este numero de parte</span>';
|
|
}
|
|
}).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 = '<option value="">Selecciona categoría</option>';
|
|
if (subSel) { subSel.innerHTML = '<option value="">Selecciona categoría primero</option>'; 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 = '<span style="color:var(--color-error);">Numero de parte y nombre son obligatorios</span>';
|
|
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 = '<span style="color:var(--color-success);">' + msg + '</span>';
|
|
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 = '<span style="color:var(--color-error);">' + (result ? result.error || 'Error' : 'Error de red') + '</span>';
|
|
}
|
|
});
|
|
}
|
|
|
|
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 = '<span style="color:var(--color-error);">Selecciona un archivo CSV o Excel.</span>';
|
|
return;
|
|
}
|
|
var file = fileInput.files[0];
|
|
var formData = new FormData();
|
|
formData.append('file', file);
|
|
resultEl.style.display = 'block';
|
|
resultEl.innerHTML = '<span style="color:var(--color-text-muted);">Importando...</span>';
|
|
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 = '<span style="color:var(--color-error);">' + esc(data.error) + '</span>';
|
|
return;
|
|
}
|
|
var html = '<div style="color:var(--color-success);">Importacion completada: <strong>' + data.created + '</strong> producto(s) creado(s)';
|
|
if (data.skipped > 0) html += ', <strong>' + data.skipped + '</strong> saltado(s)';
|
|
html += '</div>';
|
|
if (data.warnings && data.warnings.length) {
|
|
html += '<div style="margin-top:8px;max-height:160px;overflow:auto;background:var(--color-surface);padding:8px;border-radius:6px;font-size:var(--text-caption);">';
|
|
html += '<strong style="color:var(--color-warning);">Advertencias (' + data.warnings.length + '):</strong><ul style="margin:4px 0 0 16px;padding:0;">';
|
|
data.warnings.forEach(function(w) {
|
|
html += '<li>' + esc(w) + '</li>';
|
|
});
|
|
html += '</ul></div>';
|
|
}
|
|
resultEl.innerHTML = html;
|
|
loadItems(currentPage);
|
|
if (window.loadInventoryStats) window.loadInventoryStats();
|
|
}).catch(function(err) {
|
|
resultEl.innerHTML = '<span style="color:var(--color-error);">Error de red: ' + esc(err.message) + '</span>';
|
|
});
|
|
}
|
|
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 =
|
|
'<strong>' + esc(item.name || '') + '</strong>' +
|
|
(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.items) || [];
|
|
if (!items.length) {
|
|
resultsEl.innerHTML = '<div style="padding:var(--space-3);color:var(--color-text-muted);font-size:var(--text-caption);">Sin resultados</div>';
|
|
resultsEl.style.display = 'block';
|
|
return;
|
|
}
|
|
resultsEl.innerHTML = items.map(function(it) {
|
|
return '<div class="purchase-search-result" style="padding:var(--space-3);cursor:pointer;border-bottom:1px solid var(--color-border);" ' +
|
|
'data-id="' + it.id + '">' +
|
|
'<div style="font-weight:var(--font-weight-semibold);">' + esc(it.name) + '</div>' +
|
|
'<div style="font-size:var(--text-caption);color:var(--color-text-muted);">' +
|
|
(it.part_number ? 'No. parte: ' + esc(it.part_number) + ' · ' : '') +
|
|
(it.barcode ? 'Barcode: ' + esc(it.barcode) + ' · ' : '') +
|
|
'Stock: ' + (it.stock || 0) +
|
|
'</div>' +
|
|
'</div>';
|
|
}).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.items) || [];
|
|
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 = '<span style="color:var(--color-error);">Complete todos los campos obligatorios</span>';
|
|
return;
|
|
}
|
|
apiFetch(API + '/purchase', { method: 'POST', body: JSON.stringify(data) }).then(function (result) {
|
|
if (result && result.operation_id) {
|
|
document.getElementById('purchaseResult').innerHTML = '<span style="color:var(--color-success);">Compra registrada (op #' + result.operation_id + ')</span>';
|
|
closePurchaseModal();
|
|
if (window.loadInventoryStats) window.loadInventoryStats();
|
|
loadItems(currentPage);
|
|
} else {
|
|
document.getElementById('purchaseResult').innerHTML = '<span style="color:var(--color-error);">' + (result ? result.error || 'Error' : 'Error de red') + '</span>';
|
|
}
|
|
});
|
|
}
|
|
|
|
// =====================================================================
|
|
// 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 = '<span style="color:var(--color-error);">Complete todos los campos (razon obligatoria)</span>';
|
|
return;
|
|
}
|
|
apiFetch(API + '/adjustment', { method: 'POST', body: JSON.stringify(data) }).then(function (result) {
|
|
if (result && result.operation_id) {
|
|
document.getElementById('adjustResult').innerHTML = '<span style="color:var(--color-success);">Ajuste registrado (op #' + result.operation_id + ')</span>';
|
|
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 = '<span style="color:var(--color-error);">' + (result ? result.error || 'Error' : 'Error de red') + '</span>';
|
|
}
|
|
});
|
|
}
|
|
|
|
// =====================================================================
|
|
// 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 = '<span style="color:var(--color-error);">Complete todos los campos</span>';
|
|
return;
|
|
}
|
|
apiFetch(API + '/transfer', { method: 'POST', body: JSON.stringify(data) }).then(function (result) {
|
|
if (result && result.out_operation_id) {
|
|
document.getElementById('transferResult').innerHTML = '<span style="color:var(--color-success);">Transferencia registrada</span>';
|
|
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 = '<span style="color:var(--color-error);">' + (result ? result.error || 'Error' : 'Error de red') + '</span>';
|
|
}
|
|
});
|
|
}
|
|
|
|
// =====================================================================
|
|
// 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 = '<tr><td colspan="' + (opColspan[type] || 8) + '" style="text-align:center;padding:30px;color:var(--color-text-muted);">Sin registros</td></tr>';
|
|
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 =
|
|
'<div class="pagination">' +
|
|
'<button class="page-btn" ' + (pg.page <= 1 ? 'disabled' : 'onclick="window._loadOperations(\'' + type + '\',' + (pg.page - 1) + ')"') + '>‹</button>' +
|
|
'<span style="padding:0 var(--space-2);font-size:var(--text-body-sm);color:var(--color-text-muted);">' + pg.page + ' / ' + pg.total_pages + ' (' + pg.total + ' registros)</span>' +
|
|
'<button class="page-btn" ' + (pg.page >= pg.total_pages ? 'disabled' : 'onclick="window._loadOperations(\'' + type + '\',' + (pg.page + 1) + ')"') + '>›</button>' +
|
|
'</div>';
|
|
} 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 '<tr>' +
|
|
'<td class="td--mono">#' + op.id + '</td>' +
|
|
'<td>' + dateStr + '</td>' +
|
|
'<td>' + productInfo + '</td>' +
|
|
'<td style="text-align:right">' + (op.quantity || 0) + '</td>' +
|
|
'<td style="text-align:right" class="td--amount">$' + fmt(op.cost_at_time || 0) + '</td>' +
|
|
'<td style="text-align:right" class="td--amount">$' + fmt(op.total || 0) + '</td>' +
|
|
'<td>' + esc(op.notes || '-') + '</td>' +
|
|
'<td>' + esc(op.employee_name || '-') + '</td>' +
|
|
'</tr>';
|
|
}
|
|
if (type === 'salidas') {
|
|
return '<tr>' +
|
|
'<td class="td--mono">#' + op.id + '</td>' +
|
|
'<td>' + dateStr + '</td>' +
|
|
'<td>' + productInfo + '</td>' +
|
|
'<td style="text-align:right">' + (op.quantity || 0) + '</td>' +
|
|
'<td style="text-align:right" class="td--amount">$' + fmt(op.total || 0) + '</td>' +
|
|
'<td>' + esc(op.notes || '-') + '</td>' +
|
|
'<td>' + esc(op.employee_name || '-') + '</td>' +
|
|
'</tr>';
|
|
}
|
|
if (type === 'traspasos') {
|
|
return '<tr>' +
|
|
'<td class="td--mono">#' + op.id + '</td>' +
|
|
'<td>' + dateStr + '</td>' +
|
|
'<td>' + productInfo + '</td>' +
|
|
'<td style="text-align:right">' + (op.quantity || 0) + '</td>' +
|
|
'<td>' + esc(op.branch_name || '-') + '</td>' +
|
|
'<td>' + esc(op.notes || '-') + '</td>' +
|
|
'<td>' + esc(op.employee_name || '-') + '</td>' +
|
|
'<td></td>' +
|
|
'</tr>';
|
|
}
|
|
if (type === 'ajustes') {
|
|
return '<tr>' +
|
|
'<td class="td--mono">#' + op.id + '</td>' +
|
|
'<td>' + dateStr + '</td>' +
|
|
'<td>' + esc(op.operation_type || 'ADJUST') + '</td>' +
|
|
'<td>' + productInfo + '</td>' +
|
|
'<td style="text-align:right">' + (op.quantity || 0) + '</td>' +
|
|
'<td>' + esc(op.notes || '-') + '</td>' +
|
|
'<td>' + esc(op.employee_name || '-') + '</td>' +
|
|
'</tr>';
|
|
}
|
|
return '<tr><td colspan="8">-</td></tr>';
|
|
}
|
|
|
|
// =====================================================================
|
|
// 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 =
|
|
'<input type="number" placeholder="ID producto" class="count-inv-id" style="width:140px;" />' +
|
|
'<input type="number" placeholder="Cantidad contada" class="count-qty" style="width:160px;" />' +
|
|
'<button class="btn btn--ghost btn--sm" onclick="this.parentElement.remove()">Quitar</button>';
|
|
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 = '<span style="color:var(--color-error);">' + (result ? result.error || 'Error' : 'Error de red') + '</span>';
|
|
return;
|
|
}
|
|
|
|
draftCountId = result.count_id;
|
|
var html = '<h4 style="margin-bottom:var(--space-3);">Borrador #' + result.count_id + ' — ' + esc(result.message) + '</h4>';
|
|
html += '<table class="data-table"><thead><tr><th>ID</th><th>Esperado</th><th>Contado</th><th>Diferencia</th></tr></thead><tbody>';
|
|
(result.results || []).forEach(function (r) {
|
|
var color = r.difference === 0 ? 'var(--color-success)' : (r.difference < 0 ? 'var(--color-error)' : 'var(--color-warning)');
|
|
html += '<tr><td>' + r.inventory_id + '</td><td>' + r.expected + '</td><td>' + r.counted + '</td><td style="color:' + color + ';font-weight:600;">' + (r.difference > 0 ? '+' : '') + r.difference + '</td></tr>';
|
|
});
|
|
html += '</tbody></table>';
|
|
html += '<div style="margin-top:var(--space-3);display:flex;gap:var(--space-2);">';
|
|
html += '<button class="btn btn--primary btn--sm" onclick="approvePhysicalCount()">Aprobar y aplicar ajustes</button>';
|
|
html += '<button class="btn btn--ghost btn--sm" onclick="cancelDraft()">Cancelar borrador</button>';
|
|
html += '</div>';
|
|
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 = '<span style="color:var(--color-success);">' + esc(result.message) + '</span>';
|
|
draftCountId = null;
|
|
} else {
|
|
document.getElementById('countResults').innerHTML += '<br><span style="color:var(--color-error);">' + (result ? result.error || 'Error' : 'Error de red') + '</span>';
|
|
}
|
|
});
|
|
}
|
|
|
|
function cancelDraft() {
|
|
draftCountId = null;
|
|
document.getElementById('countResults').innerHTML = '<span style="color:var(--color-text-muted);">Borrador cancelado</span>';
|
|
}
|
|
|
|
// =====================================================================
|
|
// ALERTS (panel-alertas)
|
|
// =====================================================================
|
|
|
|
function loadAlerts() {
|
|
var container = document.getElementById('alertsContent');
|
|
if (container) container.innerHTML = '<div style="padding:var(--space-6);">' + renderEmptyState({
|
|
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
|
|
title: 'Cargando alertas...',
|
|
subtitle: 'Revisando el estado del inventario'
|
|
}) + '</div>';
|
|
|
|
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: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>',
|
|
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 = '<div style="display:flex;gap:var(--space-3);flex-wrap:wrap;margin-bottom:var(--space-6);padding:var(--space-4);background:var(--color-surface-2);border-radius:var(--radius-lg);">' +
|
|
'<div style="font-size:var(--text-sm);font-weight:700;">Resumen de alertas</div>' +
|
|
(counts.critical ? '<span class="badge badge--low">' + counts.critical + ' crítica' + (counts.critical !== 1 ? 's' : '') + '</span>' : '') +
|
|
(counts.warning ? '<span class="badge badge--over">' + counts.warning + ' advertencia' + (counts.warning !== 1 ? 's' : '') + '</span>' : '') +
|
|
(counts.info ? '<span class="badge badge--ok">' + counts.info + ' informativa' + (counts.info !== 1 ? 's' : '') + '</span>' : '') +
|
|
'</div>';
|
|
|
|
// 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 = '<div class="section-heading"><span class="section-heading__title">' + title + '</span><div class="section-heading__line"></div><span class="badge ' + badgeClass + '">' + alerts.length + '</span></div>';
|
|
html += '<div class="alerts-grid" style="margin-bottom:var(--space-6);">';
|
|
display.forEach(function (a) {
|
|
var icon = a.type === 'zero' ? 'AGOTADO' : (a.type === 'low' ? 'BAJO' : a.type.toUpperCase());
|
|
html += buildAlertCard(a, icon, level);
|
|
});
|
|
html += '</div>';
|
|
|
|
if (remaining > 0) {
|
|
html += '<div style="text-align:center;margin-bottom:var(--space-6);">' +
|
|
'<button class="btn btn--ghost btn--sm" onclick="window._showMoreAlerts(\'' + level + '\')">Ver ' + remaining + ' más</button>' +
|
|
'</div>';
|
|
}
|
|
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 '<div class="alert-card ' + cls + '">' +
|
|
'<div class="alert-card__icon"><svg viewBox="0 0 24 24"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></div>' +
|
|
'<div class="alert-card__body">' +
|
|
'<div class="alert-card__title">[' + icon + '] ' + esc(a.part_number) + ' — ' + esc(a.name) + '</div>' +
|
|
'<div class="alert-card__desc">Stock: ' + a.stock +
|
|
(a.min_stock ? ' (min: ' + a.min_stock + ')' : '') +
|
|
(a.max_stock ? ' (max: ' + a.max_stock + ')' : '') +
|
|
' · Sucursal ' + a.branch_id + '</div>' +
|
|
'</div></div>';
|
|
}
|
|
|
|
// =====================================================================
|
|
// 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: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>',
|
|
title: 'Sin movimientos',
|
|
subtitle: 'Este producto aún no tiene historial de entradas, salidas ni ajustes.'
|
|
});
|
|
} else {
|
|
html = '<table class="data-table"><thead><tr><th>Fecha</th><th>Tipo</th><th>Cantidad</th><th>Costo</th><th>Empleado</th><th>Notas</th></tr></thead><tbody>';
|
|
history.forEach(function (h) {
|
|
var qtyColor = h.quantity > 0 ? 'var(--color-success)' : 'var(--color-error)';
|
|
html += '<tr>' +
|
|
'<td style="font-size:var(--text-caption);">' + esc(h.date) + '</td>' +
|
|
'<td>' + esc(h.type) + '</td>' +
|
|
'<td style="color:' + qtyColor + ';font-weight:600;">' + (h.quantity > 0 ? '+' : '') + h.quantity + '</td>' +
|
|
'<td class="td--amount">' + (h.cost ? '$' + fmt(h.cost) : '—') + '</td>' +
|
|
'<td>' + esc(h.employee) + '</td>' +
|
|
'<td style="font-size:var(--text-caption);">' + esc(h.notes) + '</td>' +
|
|
'</tr>';
|
|
});
|
|
html += '</tbody></table>';
|
|
}
|
|
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 = '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Cargando verificaciones...</p>';
|
|
|
|
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 = '<p style="color:var(--color-error);">Error cargando preview</p>'; return; }
|
|
var html = '';
|
|
data.items.forEach(function(it) {
|
|
if (!it.exists) {
|
|
html += '<div class="meli-preview-card" style="opacity:0.6;"><div style="color:var(--color-error);">Item #' + it.inventory_id + ' no encontrado</div></div>';
|
|
return;
|
|
}
|
|
meliPreviewData[it.inventory_id] = it;
|
|
var checks = '';
|
|
checks += '<span class="meli-check ' + (it.has_image ? 'ok' : 'fail') + '">' + (it.has_image ? '✅' : '❌') + ' Imagen</span>';
|
|
checks += '<span class="meli-check ' + (it.has_stock ? 'ok' : 'fail') + '">' + (it.has_stock ? '✅' : '❌') + ' Stock</span>';
|
|
checks += '<span class="meli-check ' + (it.has_price ? 'ok' : 'fail') + '">' + (it.has_price ? '✅' : '❌') + ' Precio</span>';
|
|
if (it.already_published) {
|
|
checks += '<span class="meli-check ok">✅ <a href="' + esc(it.existing_listing.permalink || '#') + '" target="_blank" style="color:var(--color-success);text-decoration:underline;">Ya publicado (' + esc(it.existing_listing.status) + ')</a></span>';
|
|
}
|
|
var imgSrc = it.image_url || '';
|
|
var imgHtml = imgSrc ? '<img src="' + esc(imgSrc) + '" alt="" onerror="this.style.display=\'none\'">' : '<div style="width:56px;height:56px;background:var(--color-surface-2);border-radius:var(--radius-sm);display:flex;align-items:center;justify-content:center;font-size:10px;color:var(--color-text-muted);">Sin img</div>';
|
|
if (!it.has_image) {
|
|
imgHtml = '<div class="meli-img-upload" onclick="document.getElementById(\'meliImgUpload-' + it.inventory_id + '\').click()">' +
|
|
'<div style="font-size:10px;">+ Subir</div>' +
|
|
'<input type="file" id="meliImgUpload-' + it.inventory_id + '" accept="image/*" onchange="handleMeliImageUpload(' + it.inventory_id + ', this)">' +
|
|
'</div>';
|
|
}
|
|
html += '<div class="meli-preview-card" id="meliCard-' + it.inventory_id + '">' +
|
|
imgHtml +
|
|
'<div>' +
|
|
'<input type="text" class="meli-title-input" id="meliTitle-' + it.inventory_id + '" value="' + esc(it.title) + '" maxlength="60" oninput="updateMeliTitleCount(' + it.inventory_id + ')">' +
|
|
'<div style="font-size:10px;color:var(--color-text-muted);text-align:right;" id="meliTitleCount-' + it.inventory_id + '">' + it.title.length + '/60</div>' +
|
|
'<div class="meli-checks-row">' + checks + '</div>' +
|
|
'</div>' +
|
|
'<div><label style="font-size:10px;color:var(--color-text-muted);">Precio</label><input type="number" class="meli-num-input" id="meliPrice-' + it.inventory_id + '" value="' + it.price + '"></div>' +
|
|
'<div><label style="font-size:10px;color:var(--color-text-muted);">Stock</label><input type="number" class="meli-num-input" id="meliStock-' + it.inventory_id + '" value="' + it.stock + '"></div>' +
|
|
'</div>';
|
|
});
|
|
container.innerHTML = html;
|
|
}).catch(function() { container.innerHTML = '<p style="color:var(--color-error);">Error de red</p>'; });
|
|
}
|
|
|
|
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 = '<div class="meli-cat-dropdown"><div class="meli-cat-loading">Buscando...</div></div>';
|
|
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 = '<div class="meli-cat-dropdown"><div class="meli-cat-empty">Sin resultados</div></div>'; return; }
|
|
var html = '<div class="meli-cat-dropdown">';
|
|
meliCatItems.forEach(function(c, idx) {
|
|
html += '<div class="meli-cat-item" data-idx="' + idx + '" onmouseenter="highlightMeliCat(' + idx + ')" onmousedown="selectMeliCategoryIdx(' + idx + ')">' +
|
|
'<span>' + esc(c.category_name || c.category_id) + '</span>' +
|
|
'<span class="cat-id">' + esc(c.category_id) + '</span>' +
|
|
'</div>';
|
|
});
|
|
html += '</div>';
|
|
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 = '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Cargando atributos...</p>';
|
|
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 = '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">No hay atributos obligatorios adicionales.</p>'; return; }
|
|
var html = '';
|
|
meliCategoryAttrs.forEach(function(attr) {
|
|
var attrId = esc(attr.id);
|
|
var attrName = esc(attr.name || attr.id);
|
|
var inputHtml = '<input type="text" class="meli-title-input" id="meliAttr-' + attrId + '" placeholder="' + attrName + '">';
|
|
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 = '<select class="meli-title-input" id="' + selectId + '" onchange="onMeliAttrSelectChange(\'' + attrId + '\')">' +
|
|
'<option value="">Selecciona ' + attrName + '</option>' +
|
|
attr.values.map(function(v) { return '<option value="' + esc(v.name) + '">' + esc(v.name) + '</option>'; }).join('') +
|
|
'<option value="__other__">Otra marca (escribir)...</option>' +
|
|
'</select>' +
|
|
'<input type="text" class="meli-title-input" id="' + otherId + '" placeholder="Escribe la ' + attrName + '" style="display:none;margin-top:6px;">';
|
|
}
|
|
html += '<div class="inv-field" id="meliAttrWrap-' + attrId + '"><label>' + attrName + (attr.tags && attr.tags.required ? ' *' : '') + '</label>' + inputHtml + '</div>';
|
|
});
|
|
grid.innerHTML = html;
|
|
}).catch(function() { grid.innerHTML = '<p style="color:var(--color-error);font-size:var(--text-caption);">Error cargando atributos</p>'; });
|
|
};
|
|
|
|
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 = '<span style="color:var(--color-error);">Selecciona una categoría de MercadoLibre</span>'; 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 = '<span style="color:var(--color-text-muted);">Validando con MercadoLibre...</span>';
|
|
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 = '<span style="color:var(--color-error);">Error: ' + esc(data.error) + '</span>'; return; }
|
|
var valid = (data.valid || []);
|
|
var invalid = (data.invalid || []);
|
|
var html = '<div style="margin-bottom:var(--space-2);"><span style="color:var(--color-success);">✅ ' + valid.length + ' válido(s)</span> · <span style="color:var(--color-error);">❌ ' + invalid.length + ' inválido(s)</span></div>';
|
|
|
|
// Show earnings estimate table for valid items
|
|
if (valid.length > 0) {
|
|
html += '<div style="margin:var(--space-3) 0;border:1px solid var(--color-border);border-radius:var(--radius-md);overflow:hidden;">';
|
|
html += '<div style="background:var(--color-surface-2);padding:var(--space-2) var(--space-3);font-size:var(--text-caption);font-weight:600;color:var(--color-text-primary);">💰 Estimado de ganancia (después de comisiones ML)</div>';
|
|
html += '<table style="width:100%;border-collapse:collapse;font-size:var(--text-caption);">';
|
|
html += '<tr style="background:var(--color-surface-1);"><th style="padding:6px 8px;text-align:left;">Item</th><th style="padding:6px 8px;text-align:right;">Precio</th><th style="padding:6px 8px;text-align:right;">Comisión</th><th style="padding:6px 8px;text-align:right;">Recibes</th></tr>';
|
|
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 += '<tr style="border-top:1px solid var(--color-border);">';
|
|
html += '<td style="padding:6px 8px;">#' + esc(v.inventory_id) + '</td>';
|
|
html += '<td style="padding:6px 8px;text-align:right;">$' + price.toFixed(2) + '</td>';
|
|
html += '<td style="padding:6px 8px;text-align:right;color:var(--color-error);">-$' + fee.toFixed(2) + ' (' + feePct.toFixed(1) + '%)</td>';
|
|
html += '<td style="padding:6px 8px;text-align:right;color:var(--color-success);font-weight:600;">$' + net.toFixed(2) + '</td>';
|
|
html += '</tr>';
|
|
});
|
|
html += '</table>';
|
|
if (valid.some(function(v){ return v.fee_source === 'approximation'; })) {
|
|
html += '<div style="padding:var(--space-2) var(--space-3);font-size:10px;color:var(--color-text-muted);background:var(--color-surface-0);">* Comisión estimada. El monto real puede variar según ML.</div>';
|
|
}
|
|
html += '</div>';
|
|
}
|
|
|
|
if (invalid.length) {
|
|
html += '<ul style="margin:0;padding-left:var(--space-4);font-size:var(--text-caption);color:var(--color-text-secondary);">';
|
|
invalid.forEach(function(f) {
|
|
html += '<li>Item #' + esc(f.inventory_id) + ': ' + esc(f.error) + '</li>';
|
|
});
|
|
html += '</ul>';
|
|
}
|
|
resultEl.innerHTML = html;
|
|
}).catch(function(e) { btn.disabled = false; resultEl.innerHTML = '<span style="color:var(--color-error);">Error: ' + esc(e.message) + '</span>'; });
|
|
};
|
|
|
|
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 = '<div style="margin-bottom:var(--space-2);"><span style="color:var(--color-success);">✅ ' + success + ' publicado(s)</span> · <span style="color:var(--color-error);">❌ ' + failed + ' fallo(s)</span></div>';
|
|
if (hasModeError) {
|
|
html += '<div style="background:var(--color-surface-2);border:1px solid var(--color-error);border-radius:var(--radius-md);padding:var(--space-3);margin:var(--space-2) 0;font-size:var(--text-caption);color:var(--color-text-primary);">' +
|
|
'<strong>⚠️ Tu cuenta de MercadoLibre no tiene modos de envío configurados.</strong><br>' +
|
|
'Esto es un requisito de MercadoLibre, no de Nexus.<br>' +
|
|
'Pasos para solucionarlo:<br>' +
|
|
'1. Entrá a <a href="https://www.mercadolibre.com.mx" target="_blank" style="color:var(--color-primary);text-decoration:underline;">mercadolibre.com.mx</a> con la cuenta de vendedor<br>' +
|
|
'2. Andá a <strong>Vender > Configuración de envíos</strong><br>' +
|
|
'3. Completá tu dirección de retiro y activá al menos un método de envío<br>' +
|
|
'4. Si no te aparece la opción, contactá a <a href="https://www.mercadolibre.com.mx/ayuda" target="_blank" style="color:var(--color-primary);text-decoration:underline;">soporte de MercadoLibre</a><br>' +
|
|
'<em>Nota: Algunas cuentas nuevas necesitan verificar identidad antes de poder configurar envíos.</em>' +
|
|
'</div>';
|
|
}
|
|
if (failedList.length) {
|
|
html += '<ul style="margin:0;padding-left:var(--space-4);font-size:var(--text-caption);color:var(--color-text-secondary);">';
|
|
failedList.forEach(function(f) {
|
|
html += '<li>Item #' + esc(f.inventory_id) + ': ' + esc(f.error) + '</li>';
|
|
});
|
|
html += '</ul>';
|
|
}
|
|
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 = '<span style="color:var(--color-error);">Timeout esperando resultado. Revisa la pestaña Publicaciones más tarde.</span>';
|
|
} else {
|
|
resultEl.innerHTML = '<span style="color:var(--color-text-muted);">Publicando en segundo plano... (' + attempts + 's)</span>';
|
|
}
|
|
}).catch(function() {
|
|
clearInterval(interval);
|
|
btn.disabled = false;
|
|
resultEl.innerHTML = '<span style="color:var(--color-error);">Error consultando progreso</span>';
|
|
});
|
|
}, 2000);
|
|
}
|
|
|
|
window.executeMeliPublish = function() {
|
|
var categoryId = document.getElementById('meliCategoryId').value.trim();
|
|
if (!categoryId) { document.getElementById('meliPublishResult').innerHTML = '<span style="color:var(--color-error);">Selecciona una categoría de MercadoLibre</span>'; 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 = '<span style="color:var(--color-text-muted);">' + (useAsync ? 'Encolando ' : 'Publicando ') + ids.length + ' producto(s)...</span>';
|
|
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 = '<span style="color:var(--color-error);">Error: ' + esc(data.error) + '</span>'; 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 = '<span style="color:var(--color-error);">Error: ' + esc(e.message) + '</span>'; });
|
|
};
|
|
|
|
// =====================================================================
|
|
// BARCODE LABEL PRINT
|
|
// =====================================================================
|
|
|
|
function printBarcode(barcode, partNumber, name) {
|
|
var w = window.open('', '_blank', 'width=400,height=250');
|
|
w.document.write('<html><head><title>Etiqueta</title><style>body{font-family:monospace;text-align:center;padding:20px;}h1{font-size:1.5rem;margin:8px 0;}p{margin:4px 0;}</style></head><body>');
|
|
w.document.write('<h1>' + barcode + '</h1>');
|
|
w.document.write('<p>' + partNumber + '</p>');
|
|
w.document.write('<p style="font-size:0.85rem;">' + name + '</p>');
|
|
w.document.write('</body></html>');
|
|
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 += '<style>';
|
|
html += '.compat-tabs{display:flex;gap:4px;border-bottom:1px solid var(--color-border);margin-bottom:16px;}';
|
|
html += '.compat-tab-btn{padding:8px 16px;font-size:var(--text-body-sm);font-weight:600;cursor:pointer;border:none;background:transparent;color:var(--color-text-muted);border-bottom:2px solid transparent;margin-bottom:-1px;}';
|
|
html += '.compat-tab-btn.is-active{color:var(--color-primary);border-bottom-color:var(--color-primary);background:var(--color-surface-0);border-radius:var(--radius-sm) var(--radius-sm) 0 0;}';
|
|
html += '.compat-tab-panel{display:none;}';
|
|
html += '.compat-tab-panel.is-active{display:block;}';
|
|
html += '.compat-form{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px;}';
|
|
html += '.compat-form label{font-size:var(--text-caption);color:var(--color-text-muted);display:block;margin-bottom:4px;}';
|
|
html += '</style>';
|
|
|
|
// Tabs
|
|
html += '<div class="compat-tabs">';
|
|
html += '<button class="compat-tab-btn is-active" onclick="switchCompatTab(\'detail\',this)">Detalle</button>';
|
|
html += '<button class="compat-tab-btn" onclick="switchCompatTab(\'compat\',this)">Compatibilidad</button>';
|
|
html += '</div>';
|
|
|
|
// Detail panel
|
|
html += '<div id="compatTab-detail" class="compat-tab-panel is-active">';
|
|
|
|
// Product image section
|
|
html += '<div style="text-align:center;margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--color-border);">';
|
|
if (data.image_url) {
|
|
html += '<img src="' + esc(data.image_url) + '?t=' + Date.now() + '" alt="' + esc(data.name) + '" loading="lazy" decoding="async" style="max-width:100%;max-height:220px;object-fit:contain;border-radius:var(--radius-sm);margin-bottom:8px;display:block;margin-left:auto;margin-right:auto;">';
|
|
html += '<div style="display:flex;gap:8px;justify-content:center;">';
|
|
html += '<button class="btn btn--ghost btn--sm" onclick="uploadItemImage(' + data.id + ')">Cambiar imagen</button>';
|
|
html += '<button class="btn btn--ghost btn--sm" style="color:var(--color-error);" onclick="deleteItemImage(' + data.id + ')">Eliminar imagen</button>';
|
|
html += '</div>';
|
|
} else {
|
|
html += '<div style="padding:24px;color:var(--color-text-muted);">';
|
|
html += '<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" style="opacity:0.4;"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>';
|
|
html += '<div style="margin-top:8px;">Sin imagen</div>';
|
|
html += '</div>';
|
|
html += '<button class="btn btn--primary btn--sm" onclick="uploadItemImage(' + data.id + ')">Subir imagen</button>';
|
|
}
|
|
html += '<span id="imgUploadStatus" style="display:block;margin-top:4px;font-size:var(--text-caption);color:var(--color-text-muted);"></span>';
|
|
html += '</div>';
|
|
|
|
// Action buttons
|
|
html += '<div style="display:flex;gap:8px;justify-content:center;margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--color-border);">';
|
|
html += '<button class="btn btn--ghost btn--sm" onclick="showProductTimeline(' + data.id + ')">📅 Timeline</button>';
|
|
if (data.image_url) {
|
|
html += '<button class="btn btn--ghost btn--sm" onclick="showImageCompare(\'' + esc(data.image_url) + '\')">🖼️ Comparar</button>';
|
|
}
|
|
html += '<button class="btn btn--ghost btn--sm" onclick="viewHistory(' + data.id + ')">📜 Historial</button>';
|
|
html += '</div>';
|
|
|
|
// Product info header
|
|
html += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--color-border);">';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">ID Inventario</span><strong style="font-family:var(--font-mono);">' + data.id + '</strong></div>';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">No. Parte</span><strong>' + esc(data.part_number) + '</strong></div>';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Nombre</span><strong>' + esc(data.name) + '</strong></div>';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Marca</span>' + esc(data.brand || '-') + '</div>';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Categoría</span>' + esc(data.category_name || '-') + '</div>';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Codigo de Barras</span><span style="font-family:var(--font-mono);">' + esc(data.barcode) + '</span></div>';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Ubicacion</span>' + esc(data.location || '-') + '</div>';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Stock</span><strong style="font-size:1.2em;">' + (data.stock || 0) + '</strong></div>';
|
|
html += '</div>';
|
|
|
|
// SKU Aliases section
|
|
html += '<div style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:var(--tracking-widest);margin-bottom:8px;">SKU Alternativos</div>';
|
|
html += '<div id="skuAliasContent" style="margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--color-border);">';
|
|
html += '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Cargando SKU alternativos...</p>';
|
|
html += '</div>';
|
|
|
|
// Prices
|
|
html += '<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--color-border);">';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Costo</span><span class="td--amount">$' + fmt(data.cost) + '</span></div>';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Mostrador</span><span class="td--amount">$' + fmt(data.price_1) + '</span></div>';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Taller</span><span class="td--amount">$' + fmt(data.price_2) + '</span><span style="font-size:var(--text-caption);color:var(--color-text-muted);"> (-' + globalDiscounts[2] + '%)</span></div>';
|
|
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">Mayoreo</span><span class="td--amount">$' + fmt(data.price_3) + '</span><span style="font-size:var(--text-caption);color:var(--color-text-muted);"> (-' + globalDiscounts[3] + '%)</span></div>';
|
|
html += '</div>';
|
|
|
|
// Cross-references section
|
|
html += '<div style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:var(--tracking-widest);margin-bottom:8px;">Cross-References / Equivalencias</div>';
|
|
html += '<div id="crossRefContent" style="margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--color-border);">';
|
|
html += '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Cargando equivalencias...</p>';
|
|
html += '</div>';
|
|
|
|
// 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 = '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Sin conexion al catalogo.</p>'; });
|
|
return;
|
|
}
|
|
|
|
renderCrossRefs(el, alternatives, bodegas);
|
|
})
|
|
.catch(function() {
|
|
var el = document.getElementById('crossRefContent');
|
|
if (el) el.innerHTML = '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Sin conexion al catalogo central.</p>';
|
|
});
|
|
})();
|
|
|
|
function renderCrossRefs(el, alternatives, bodegas) {
|
|
var html2 = '';
|
|
|
|
if (bodegas && bodegas.length > 0) {
|
|
html2 += '<div style="margin-bottom:12px;"><strong style="font-size:var(--text-body-sm);color:var(--color-text-primary);">Disponible en Bodegas:</strong></div>';
|
|
html2 += '<table class="data-table"><thead><tr><th>Bodega</th><th>Stock</th><th>Precio</th><th>Ubicacion</th></tr></thead><tbody>';
|
|
bodegas.forEach(function(b) {
|
|
html2 += '<tr><td>' + esc(b.business_name || b.bodega || '') + '</td><td>' + (b.stock || b.stock_quantity || 0) + '</td><td class="td--amount">$' + fmt(b.price || 0) + '</td><td>' + esc(b.location || b.warehouse_location || '') + '</td></tr>';
|
|
});
|
|
html2 += '</tbody></table>';
|
|
}
|
|
|
|
if (alternatives && alternatives.length > 0) {
|
|
html2 += '<div style="margin-top:12px;margin-bottom:8px;"><strong style="font-size:var(--text-body-sm);color:var(--color-text-primary);">Partes Equivalentes (Aftermarket):</strong></div>';
|
|
html2 += '<table class="data-table"><thead><tr><th>No. Parte</th><th>Fabricante</th><th>Nombre</th></tr></thead><tbody>';
|
|
alternatives.forEach(function(a) {
|
|
html2 += '<tr><td class="td--mono">' + esc(a.part_number || a.cross_reference_number || '') + '</td><td>' + esc(a.manufacturer || a.source_ref || '') + '</td><td>' + esc(a.name || a.name_aftermarket_parts || '') + '</td></tr>';
|
|
});
|
|
html2 += '</tbody></table>';
|
|
}
|
|
|
|
if (!html2) {
|
|
html2 = '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">No se encontraron equivalencias para esta parte.</p>';
|
|
}
|
|
el.innerHTML = html2;
|
|
}
|
|
|
|
// Close detail panel
|
|
html += '</div>';
|
|
|
|
// Compatibility panel
|
|
html += '<div id="compatTab-compat" class="compat-tab-panel">';
|
|
|
|
// Existing compatibilities
|
|
html += '<div id="compatContent" style="margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--color-border);">';
|
|
html += '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Cargando compatibilidades...</p>';
|
|
html += '</div>';
|
|
|
|
// Manual add form
|
|
html += '<div style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:var(--tracking-widest);margin-bottom:8px;">Agregar Manualmente</div>';
|
|
html += '<div class="compat-form">';
|
|
html += '<div><label>Marca</label><select class="select-filter" id="manualMake" onchange="onManualMakeChange(' + itemId + ')" style="width:100%;"><option value="">Cargando...</option></select></div>';
|
|
html += '<div><label>Modelo</label><select class="select-filter" id="manualModel" onchange="onManualModelChange(' + itemId + ')" style="width:100%;" disabled><option value="">Selecciona marca</option></select></div>';
|
|
html += '<div><label>Ano</label><select class="select-filter" id="manualYear" onchange="onManualYearChange(' + itemId + ')" style="width:100%;" disabled><option value="">Selecciona modelo</option></select></div>';
|
|
html += '<div><label>Motor</label><select class="select-filter" id="manualEngine" style="width:100%;" disabled><option value="">Selecciona ano</option></select></div>';
|
|
html += '</div>';
|
|
html += '<button class="btn btn--primary btn--sm" onclick="submitManualCompat(' + itemId + ')">Agregar compatibilidad</button>';
|
|
|
|
// Auto-match button
|
|
var btnLabel = compatSource === 'qwen' ? 'Auto-Match con IA (QWEN)' : (compatSource === 'both' ? 'Auto-Match (TecDoc + IA)' : 'Auto-Match por TecDoc');
|
|
html += '<div style="margin-top:16px;"><button class="btn btn--ghost btn--sm" onclick="autoMatchCompat(' + itemId + ')">' + btnLabel + '</button></div>';
|
|
|
|
html += '</div>';
|
|
|
|
// 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 += '<table class="data-table"><thead><tr><th>SKU</th><th>Etiqueta</th><th></th></tr></thead><tbody>';
|
|
list.forEach(function(a) {
|
|
html2 += '<tr><td class="td--mono">' + esc(a.sku) + '</td><td>' + esc(a.label || '-') + '</td>';
|
|
html2 += '<td><button class="btn btn--ghost btn--sm" style="color:var(--color-error);" onclick="removeSkuAlias(' + itemId + ',' + a.id + ')">Quitar</button></td></tr>';
|
|
});
|
|
html2 += '</tbody></table>';
|
|
} else {
|
|
html2 += '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Sin SKU alternativos.</p>';
|
|
}
|
|
html2 += '<div style="margin-top:8px;display:flex;gap:8px;">';
|
|
html2 += '<input type="text" class="meli-title-input" id="newAliasSku-' + itemId + '" placeholder="Nuevo SKU" style="flex:1;">';
|
|
html2 += '<input type="text" class="meli-title-input" id="newAliasLabel-' + itemId + '" placeholder="Etiqueta (opcional)" style="flex:1;">';
|
|
html2 += '<button class="btn btn--primary btn--sm" onclick="addSkuAlias(' + itemId + ')">Agregar</button>';
|
|
html2 += '</div>';
|
|
el.innerHTML = html2;
|
|
})
|
|
.catch(function() {
|
|
var el = document.getElementById('skuAliasContent');
|
|
if (el) el.innerHTML = '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Error al cargar SKU alternativos.</p>';
|
|
});
|
|
})();
|
|
|
|
// 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 += '<table class="data-table"><thead><tr><th>Marca</th><th>Modelo</th><th>Ano</th><th>Motor</th><th>Origen</th><th></th></tr></thead><tbody>';
|
|
list.forEach(function(c) {
|
|
var sourceLabel = c.source === 'qwen_ai' ? '<span style="background:var(--color-primary);color:#000;padding:2px 6px;border-radius:4px;font-size:10px;font-weight:600;">IA</span>' : (c.source === 'auto_match' ? '<span style="background:var(--color-success);color:#fff;padding:2px 6px;border-radius:4px;font-size:10px;font-weight:600;">TecDoc</span>' : esc(c.source || ''));
|
|
html2 += '<tr><td>' + esc(c.brand || '') + '</td><td>' + esc(c.model || '') + '</td><td>' + esc(c.year || '') + '</td><td>' + esc(c.engine || '') + '</td><td>' + sourceLabel + '</td>';
|
|
html2 += '<td><button class="btn btn--ghost btn--sm" style="color:var(--color-error);" onclick="removeCompat(' + itemId + ',' + c.id + ')">Quitar</button></td></tr>';
|
|
});
|
|
html2 += '</tbody></table>';
|
|
} else {
|
|
html2 += '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Sin vehiculos vinculados.</p>';
|
|
}
|
|
el.innerHTML = html2;
|
|
})
|
|
.catch(function() {
|
|
var el = document.getElementById('compatContent');
|
|
if (el) el.innerHTML = '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Error al cargar compatibilidades.</p>';
|
|
});
|
|
|
|
// 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 = '<option value="">Selecciona marca</option>';
|
|
(d.makes || []).forEach(function(m) { opts += '<option value="' + esc(m.name) + '" data-id="' + m.id + '">' + esc(m.name) + '</option>'; });
|
|
sel.innerHTML = opts;
|
|
sel.disabled = false;
|
|
})
|
|
.catch(function() {
|
|
var sel = document.getElementById('manualMake');
|
|
if (sel) { sel.innerHTML = '<option value="">Error al cargar</option>'; }
|
|
});
|
|
})();
|
|
|
|
// Movement history
|
|
html += '<div style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:var(--tracking-widest);margin-bottom:8px;">Historial de Movimientos</div>';
|
|
if (!history.length) {
|
|
html += '<p style="color:var(--color-text-muted);text-align:center;padding:var(--space-4);">Sin movimientos</p>';
|
|
} else {
|
|
html += '<table class="data-table"><thead><tr><th>Fecha</th><th>Tipo</th><th>Cantidad</th><th>Costo</th><th>Empleado</th><th>Notas</th></tr></thead><tbody>';
|
|
history.forEach(function (h) {
|
|
var qtyColor = h.quantity > 0 ? 'var(--color-success)' : 'var(--color-error)';
|
|
html += '<tr>' +
|
|
'<td style="font-size:var(--text-caption);">' + esc(h.date) + '</td>' +
|
|
'<td>' + esc(h.type) + '</td>' +
|
|
'<td style="color:' + qtyColor + ';font-weight:600;">' + (h.quantity > 0 ? '+' : '') + h.quantity + '</td>' +
|
|
'<td class="td--amount">' + (h.cost ? '$' + fmt(h.cost) : '\u2014') + '</td>' +
|
|
'<td>' + esc(h.employee) + '</td>' +
|
|
'<td style="font-size:var(--text-caption);">' + esc(h.notes) + '</td>' +
|
|
'</tr>';
|
|
});
|
|
html += '</tbody></table>';
|
|
}
|
|
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 = '<option value="">Cargando...</option>';
|
|
modelSel.disabled = true;
|
|
yearSel.innerHTML = '<option value="">Selecciona modelo</option>';
|
|
yearSel.disabled = true;
|
|
engineSel.innerHTML = '<option value="">Selecciona ano</option>';
|
|
engineSel.disabled = true;
|
|
if (!brandId) {
|
|
modelSel.innerHTML = '<option value="">Selecciona marca</option>';
|
|
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 = '<option value="">Selecciona modelo</option>';
|
|
(d.models || []).forEach(function(m) { opts += '<option value="' + esc(m.name) + '" data-id="' + m.id + '">' + esc(m.name) + '</option>'; });
|
|
modelSel.innerHTML = opts;
|
|
modelSel.disabled = false;
|
|
})
|
|
.catch(function() { modelSel.innerHTML = '<option value="">Error</option>'; });
|
|
};
|
|
|
|
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 = '<option value="">Cargando...</option>';
|
|
yearSel.disabled = true;
|
|
engineSel.innerHTML = '<option value="">Selecciona ano</option>';
|
|
engineSel.disabled = true;
|
|
if (!modelId) {
|
|
yearSel.innerHTML = '<option value="">Selecciona modelo</option>';
|
|
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 = '<option value="">Selecciona ano</option>';
|
|
(d.years || []).forEach(function(y) { opts += '<option value="' + y.year + '" data-id="' + y.id + '">' + y.year + '</option>'; });
|
|
yearSel.innerHTML = opts;
|
|
yearSel.disabled = false;
|
|
})
|
|
.catch(function() { yearSel.innerHTML = '<option value="">Error</option>'; });
|
|
};
|
|
|
|
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 = '<option value="">Cargando...</option>';
|
|
engineSel.disabled = true;
|
|
if (!modelId || !yearId) {
|
|
engineSel.innerHTML = '<option value="">Selecciona ano</option>';
|
|
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 = '<option value="">Selecciona motor</option>';
|
|
(d.engines || []).forEach(function(e) { opts += '<option value="' + esc(e.name) + '" data-code="' + esc(e.code || '') + '">' + esc(e.name + (e.code ? ' (' + e.code + ')' : '')) + '</option>'; });
|
|
engineSel.innerHTML = opts;
|
|
engineSel.disabled = false;
|
|
})
|
|
.catch(function() { engineSel.innerHTML = '<option value="">Error</option>'; });
|
|
};
|
|
|
|
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 = '<div style="padding:20px;"><div class="skeleton skeleton--text"></div><div class="skeleton skeleton--text-sm"></div></div>';
|
|
modal.classList.add('is-open');
|
|
|
|
apiFetch(API + '/items/' + itemId + '/history').then(function(data) {
|
|
var history = (data && data.data) ? data.data : [];
|
|
var html = '<div class="timeline">';
|
|
html += '<div class="timeline__item"><div class="timeline__dot timeline__dot--green"></div><div class="timeline__content"><div class="timeline__date">Producto creado</div><div class="timeline__title">Registro inicial en inventario</div></div></div>';
|
|
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 += '<div class="timeline__item"><div class="timeline__dot ' + color + '"></div><div class="timeline__content">' +
|
|
'<div class="timeline__date">' + esc(h.date) + ' · ' + esc(h.employee) + '</div>' +
|
|
'<div class="timeline__title">' + esc(title) + '</div>' +
|
|
(h.notes ? '<div class="timeline__desc">' + esc(h.notes) + '</div>' : '') +
|
|
'</div></div>';
|
|
});
|
|
html += '</div>';
|
|
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();
|
|
})();
|