Files
Autoparts-DB/pos/static/js/inventory.js
consultoria-as a386ca5b4a
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled
Mejoras de sesion: estabilizacion, IA, Nexus Yonke y marketplace B2B
- F0 blindaje: Redis/Celery, pg_hba sin trust LAN, secretos a env 600, backups, webhook auth, jobs muertos
- F1 caja: bugs POS, reembolsos al corte, apartados, CFDI, topes de descuento en cotizacion/apartado
- F2/F7 IA de catalogo: autocategorizacion, dedup de SKUs con embeddings, OCR de facturas, descripciones, claves SAT, limpieza de fitment
- F3 IA en el flujo: vision Yonke, preguntas MeLi, venta cruzada, anomalias, sinonimos, busqueda semantica (rerank/embeddings)
- F4/F8 escala y cartera: /metrics+Prometheus+Grafana, pool, score de credito, churn, metricas multi-tenant
- Nexus Yonke (M1-M6): modelo de datos 1-de-1, alta por donador, vitrina publica, MeLi usado, consignacion, garantia por dias, reportes, etiquetas
- Marketplace B2B entre yonkes (catalogo + pedidos cross-tenant)
- Seguridad: auth en /api/admin/* (35 endpoints), alertas por tenant
- Migraciones v4.27/v4.28; timers expire-demos y rollup-metrics
2026-10-12 00:57:31 +00:00

2602 lines
146 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// /home/Autopartes/pos/static/js/inventory.js
// Inventory management UI — rewritten to match design-system HTML structure
// Panels: panel-stock, panel-entradas, panel-salidas, panel-traspasos, panel-ajustes, panel-conteos, panel-alertas
(function () {
'use strict';
var API = '/pos/api/inventory';
function getToken() { return localStorage.getItem('pos_token') || ''; }
if (!getToken()) { window.location.href = '/pos/login'; return; }
function authHeaders() { return { 'Authorization': 'Bearer ' + getToken(), 'Content-Type': 'application/json' }; }
var currentPage = 1;
var currentSearch = '';
var draftCountId = null;
var inventoryVS = null;
var compatSource = 'both'; // default, loaded from config
var inventorySearchController = null;
var user = window.POS_USER || {};
var userRole = (user.role || '').toLowerCase();
var userPerms = user.permissions || [];
var canEditPrices = userRole === 'owner' || userRole === 'admin' || userPerms.indexOf('config.edit_prices') !== -1;
var canCreateItem = userRole === 'owner' || userRole === 'admin' || userRole === 'counter' || userRole === 'cashier' || userRole === 'warehouse' || userPerms.indexOf('inventory.create') !== -1;
var canEditItem = userRole === 'owner' || userRole === 'admin' || userRole === 'warehouse' || userPerms.indexOf('inventory.edit') !== -1;
var canImportItems = userRole === 'owner' || userRole === 'admin' || userRole === 'warehouse' || userPerms.indexOf('inventory.edit') !== -1;
// Hide toolbar actions the user is not allowed to use
(function applyInventoryPermissions() {
var headerNew = document.getElementById('btnHeaderNewProduct');
var stockNew = document.getElementById('btnStockNewProduct');
var headerImport = document.getElementById('btnHeaderImport');
if (headerNew) headerNew.style.display = canCreateItem ? '' : 'none';
if (stockNew) stockNew.style.display = canCreateItem ? '' : 'none';
if (headerImport) headerImport.style.display = canImportItems ? '' : 'none';
})();
// Load compatibility source setting
(function loadCompatSource() {
fetch('/pos/api/config/vehicle-compat-source', { headers: { 'Authorization': 'Bearer ' + getToken() } })
.then(function(r) { return r.json(); })
.then(function(d) {
if (d.source) compatSource = d.source;
}).catch(function() {});
})();
// --- API helper ---
function apiFetch(url, opts) {
return fetch(url, Object.assign({ headers: authHeaders() }, opts || {}))
.then(function (resp) {
if (resp.status === 401) {
localStorage.removeItem('pos_token');
window.location.href = '/pos/login';
return null;
}
return resp.json();
});
}
// --- Helpers ---
function fmt(n) { return (parseFloat(n) || 0).toFixed(2); }
function esc(s) {
if (!s) return '';
var d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
function formatDateTime(isoStr) {
if (!isoStr) return '-';
var d = new Date(isoStr);
if (isNaN(d.getTime())) return esc(isoStr);
return d.toLocaleString('es-MX', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit'
});
}
// --- Dashboard summary badges ---
function loadSummary() {
var skeletonHtml = '<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 ' + getToken(), 'Content-Type': 'application/json' },
body: JSON.stringify({ discount_pct_2: d2, discount_pct_3: d3 })
}).then(function(r) { return r.json(); })
.then(function(res) {
showToast(res.message || 'Guardado', 'ok');
globalDiscounts[2] = d2;
globalDiscounts[3] = d3;
var discEl = document.getElementById('tierDiscountBadge');
if (discEl) {
discEl.textContent = 'Taller -' + d2 + '% · Mayoreo -' + d3 + '%';
}
closeTierDiscountModal();
}).catch(function() {
showToast('Error al guardar descuentos', 'error');
});
}
// Register Cmd+K items
if (typeof registerCmdKItem === 'function') {
registerCmdKItem({ group: 'Inventario', label: 'Ver stock', href: '/pos/inventory#stock', icon: '📦' });
registerCmdKItem({ group: 'Inventario', label: 'Alertas de stock', href: '/pos/inventory#alertas', icon: '⚠️' });
registerCmdKItem({ group: 'Inventario', label: 'Entradas de mercancía', href: '/pos/inventory#entradas', icon: '📥' });
registerCmdKItem({ group: 'Inventario', label: 'Salidas / Ventas', href: '/pos/inventory#salidas', icon: '📤' });
registerCmdKItem({ group: 'Inventario', label: 'Traspasos', href: '/pos/inventory#traspasos', icon: '🚚' });
registerCmdKItem({ group: 'Inventario', label: 'Ajustes', href: '/pos/inventory#ajustes', icon: '⚙️' });
registerCmdKItem({ group: 'Inventario', label: 'Conteos físicos', href: '/pos/inventory#conteos', icon: '🔢' });
}
// Handle hash-based tab switching (e.g. /pos/inventory#alertas)
(function handleHashTab() {
var hash = window.location.hash.replace('#', '');
if (hash && ['stock', 'entradas', 'salidas', 'traspasos', 'ajustes', 'conteos', 'alertas'].indexOf(hash) !== -1) {
setTimeout(function() { switchTab(hash); }, 100);
}
})();
// =====================================================================
// TAB SWITCHING — uses design-system switchTab() already in the HTML.
// We hook into it to trigger data loads when tabs are activated.
// =====================================================================
var _origSwitchTab = window.switchTab;
window.switchTab = function (name) {
if (typeof _origSwitchTab === 'function') _origSwitchTab(name);
if (name === 'alertas') loadAlerts();
if (name === 'stock') loadItems(currentPage);
};
// =====================================================================
// STOCK / PRODUCTS (panel-stock)
// =====================================================================
var selectedItems = new Set();
function renderInventoryRow(it) {
var isChecked = selectedItems.has(it.id) ? 'checked' : '';
return '<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> ' +
(canEditItem ? '<button class="btn btn--ghost btn--sm" onclick="event.stopPropagation();showEditItemModal(' + it.id + ')">Editar</button> ' : '') +
(canCreateItem ? '<button class="btn btn--ghost btn--sm" style="color:var(--color-accent);" onclick="event.stopPropagation();showPurchaseModalForItem(' + it.id + ')">Entrada</button> ' : '') +
(userPerms.indexOf('marketplace.manage') !== -1 || userRole === 'owner' || userRole === 'admin' ? '<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> ' +
(canEditItem ? '<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);
if (inventorySearchController) {
inventorySearchController.abort();
}
inventorySearchController = new AbortController();
apiFetch(API + '/items?' + params.toString(), { signal: inventorySearchController.signal })
.then(function (data) {
inventorySearchController = null;
if (!data) return;
var items = data.data || [];
if (!items.length) {
tbody.innerHTML = '<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) + ')"') + '>&lsaquo;</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) + ')"') + '>&rsaquo;</button>' +
'</div>';
} else {
pgEl.innerHTML = '<span style="font-size:var(--text-body-sm);color:var(--color-text-muted);">' + (pg.total || 0) + ' productos</span>';
}
}).catch(function (err) {
if (err.name === 'AbortError') return;
console.error('Inventory load error:', err);
});
}
// Search
var searchInput = document.getElementById('productSearch');
var searchTimeout;
if (searchInput) {
searchInput.addEventListener('input', function () {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(function () {
loadItems(1, searchInput.value.trim());
}, 350);
});
}
// =====================================================================
// CREATE ITEM (createModal)
// =====================================================================
function loadCategories(selectedId) {
var sel = document.getElementById('newCategory');
if (!sel) return;
apiFetch(API + '/categories/all').then(function(data) {
if (!data || !data.categories) return;
var cats = data.categories;
var top = cats.filter(function(c) { return !c.parent_id; });
var subs = cats.filter(function(c) { return c.parent_id; });
sel.innerHTML = '<option value="">Sin categoría</option>';
top.forEach(function(c) {
sel.innerHTML += '<optgroup label="' + esc(c.name) + '">' +
'<option value="' + c.id + '"' + (c.id === selectedId ? ' selected' : '') + '>' + esc(c.name) + '</option>';
subs.filter(function(s) { return s.parent_id === c.id; }).forEach(function(s) {
sel.innerHTML += '<option value="' + s.id + '"' + (s.id === selectedId ? ' selected' : '') + '>&nbsp;&nbsp;' + esc(s.name) + '</option>';
});
sel.innerHTML += '</optgroup>';
});
});
}
window.loadCategories = loadCategories;
function onCategoryChange(categoryId) {
// Kept for backwards compatibility; selector is now flat.
}
window.onCategoryChange = onCategoryChange;
function resetCreateModal() {
document.getElementById('editItemId').value = '';
document.getElementById('createModalTitle').textContent = 'Nuevo Producto';
document.getElementById('createModalBtn').textContent = 'Crear Producto';
var ids = ['newPartNumber','newName','newBrand','newBarcode','newSku2','newSku3','newUnit','newCost','newPrice1','newMinStock','newInitialStock','newMaxStock','newTaxRate','newLocation','newDescription'];
ids.forEach(function(id) {
var el = document.getElementById(id);
if (el) el.value = '';
});
document.getElementById('newCategory').innerHTML = '<option value="">Sin categoría</option>';
document.getElementById('newIsActive').value = 'true';
document.getElementById('initialStockField').style.display = '';
document.querySelectorAll('#createModal .price-field input').forEach(function(el) { el.disabled = false; });
}
function showCreateModal() {
resetCreateModal();
document.getElementById('createModal').classList.add('is-open');
loadCategories();
// Attach AI classification on part number blur
var pnInput = document.getElementById('newPartNumber');
if (pnInput && !pnInput._classifyBound) {
pnInput._classifyBound = true;
pnInput.addEventListener('blur', function () {
var pn = this.value.trim();
if (pn.length < 3) return;
var nameInput = document.getElementById('newName');
// Only auto-classify if name is still empty
if (nameInput && nameInput.value.trim()) return;
classifyPartNumber(pn);
});
}
}
function showEditItemModal(itemId) {
resetCreateModal();
document.getElementById('editItemId').value = itemId;
document.getElementById('createModalTitle').textContent = 'Editar Producto';
document.getElementById('createModalBtn').textContent = 'Guardar Cambios';
document.getElementById('initialStockField').style.display = 'none';
document.getElementById('createModal').classList.add('is-open');
apiFetch(API + '/items/' + itemId).then(function(it) {
if (!it) return;
document.getElementById('newPartNumber').value = it.part_number || '';
document.getElementById('newName').value = it.name || '';
document.getElementById('newBrand').value = it.brand || '';
document.getElementById('newBarcode').value = it.barcode || '';
document.getElementById('newUnit').value = it.unit || '';
document.getElementById('newCost').value = it.cost != null ? it.cost : '';
document.getElementById('newPrice1').value = it.price_1 != null ? it.price_1 : '';
document.getElementById('newMinStock').value = it.min_stock != null ? it.min_stock : '';
document.getElementById('newMaxStock').value = it.max_stock != null ? it.max_stock : '';
document.getElementById('newTaxRate').value = it.tax_rate != null ? it.tax_rate : '';
document.getElementById('newLocation').value = it.location || '';
document.getElementById('newDescription').value = it.description || '';
document.getElementById('newIsActive').value = (it.is_active === false ? 'false' : 'true');
document.getElementById('newSku2').value = (it.sku_aliases && it.sku_aliases[0]) ? it.sku_aliases[0].sku : '';
document.getElementById('newSku3').value = (it.sku_aliases && it.sku_aliases[1]) ? it.sku_aliases[1].sku : '';
if (!canEditPrices) {
document.querySelectorAll('#createModal .price-field input').forEach(function(el) { el.disabled = true; });
}
loadCategories(it.category_id);
}).catch(function(e) {
alert('Error al cargar producto: ' + e.message);
closeCreateModal();
});
}
window.showEditItemModal = showEditItemModal;
function classifyPartNumber(partNumber) {
var resultEl = document.getElementById('createResult');
resultEl.innerHTML = '<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 = '';
resetCreateModal();
}
function createItem() {
var editId = document.getElementById('editItemId').value;
var data = {
part_number: document.getElementById('newPartNumber').value.trim(),
name: document.getElementById('newName').value.trim(),
brand: document.getElementById('newBrand').value.trim(),
barcode: document.getElementById('newBarcode').value.trim() || undefined,
unit: document.getElementById('newUnit').value.trim() || undefined,
cost: parseFloat(document.getElementById('newCost').value) || 0,
price_1: parseFloat(document.getElementById('newPrice1').value) || 0,
min_stock: parseInt(document.getElementById('newMinStock').value) || 0,
max_stock: parseInt(document.getElementById('newMaxStock').value) || 0,
tax_rate: parseFloat(document.getElementById('newTaxRate').value) || 0,
location: document.getElementById('newLocation').value.trim(),
description: document.getElementById('newDescription').value.trim(),
is_active: document.getElementById('newIsActive').value === 'true',
sku_aliases: []
};
var sku2 = document.getElementById('newSku2').value.trim();
var sku3 = document.getElementById('newSku3').value.trim();
var categoryId = document.getElementById('newCategory').value;
if (categoryId) data.category_id = parseInt(categoryId);
if (sku2) data.sku_aliases.push({sku: sku2, label: 'Alternativo 1'});
if (sku3) data.sku_aliases.push({sku: sku3, label: 'Alternativo 2'});
if (!data.part_number || !data.name) {
document.getElementById('createResult').innerHTML = '<span style="color:var(--color-error);">Numero de parte y nombre son obligatorios</span>';
return;
}
var url = API + '/items';
var method = 'POST';
if (editId) {
url = API + '/items/' + editId;
method = 'PUT';
// Stock inicial solo en creación
} else {
data.initial_stock = parseInt(document.getElementById('newInitialStock').value) || 0;
}
apiFetch(url, { method: method, body: JSON.stringify(data) }).then(function (result) {
if (result && (result.id || result.message)) {
var msg = editId ? 'Producto actualizado' : ('Creado ID ' + result.id + ' | Barcode: ' + result.barcode);
if (!editId && result.vehicle_compatibilities_added > 0) {
msg += ' | ' + result.vehicle_compatibilities_added + ' vehiculo(s) asignado(s) por IA';
}
document.getElementById('createResult').innerHTML = '<span style="color:var(--color-success);">' + msg + '</span>';
loadItems(currentPage);
closeCreateModal();
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 ' + getToken(),
'X-Import-Mode': mode,
'X-Import-Strategy': strategy,
'X-Import-Stock': (document.getElementById('bulkImportSkipStock') || {}).checked ? 'skip' : 'add'
},
body: formData
}).then(function(resp) { return resp.json(); }).then(function(data) {
if (data.error) {
resultEl.innerHTML = '<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;
function downloadBulkImportTemplate() {
var headers = [
'numero_de_parte','nombre','marca','precio','cantidad','costo',
'sku_secundario','descripcion','categoria','fabricante','modelo','anio','motor','codigo_motor'
];
var example = [
'EJ-001','Filtro de aceite','ACDelco','150.00','10','90.00',
'EJ001-ALT','Filtro para sedan','Filtros','Nissan','Sentra','2020','1.8','MR18DE'
];
var csv = [headers.join(','), example.join(',')].join('\n');
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'plantilla_inventario.csv';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
window.downloadBulkImportTemplate = downloadBulkImportTemplate;
// --- Full inventory export (server-side CSV) ---
function loadExportBranches() {
var sel = document.getElementById('exportBranch');
if (!sel) return;
fetch('/pos/api/config/branches', {
headers: { 'Authorization': 'Bearer ' + getToken() }
}).then(function(resp) { return resp.ok ? resp.json() : {}; })
.then(function(data) {
var branches = data.data || data.branches || [];
branches.forEach(function(b) {
var opt = document.createElement('option');
opt.value = b.id;
opt.textContent = b.name;
sel.appendChild(opt);
});
}).catch(function() { /* selector queda solo con "todas" */ });
}
loadExportBranches();
function exportFullInventory() {
var btn = document.getElementById('btnExportFull');
var branchSel = document.getElementById('exportBranch');
var branchId = branchSel ? branchSel.value : '';
var url = API + '/export' + (branchId ? ('?branch_id=' + encodeURIComponent(branchId)) : '');
if (btn) { btn.disabled = true; btn.textContent = '⏳ Generando CSV...'; }
fetch(url, {
headers: { 'Authorization': 'Bearer ' + getToken() }
}).then(function(resp) {
if (!resp.ok) {
return resp.json().catch(function() { return {}; }).then(function(err) {
throw new Error(err.error || ('Error ' + resp.status));
});
}
var disposition = resp.headers.get('Content-Disposition') || '';
var match = disposition.match(/filename="?([^";]+)"?/);
var filename = match ? match[1] : 'inventario_completo.csv';
return resp.blob().then(function(blob) { return { blob: blob, filename: filename }; });
}).then(function(result) {
var url = URL.createObjectURL(result.blob);
var a = document.createElement('a');
a.href = url;
a.download = result.filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
document.getElementById('exportModal').classList.remove('is-open');
showToast('Inventario completo descargado', 'ok');
}).catch(function(err) {
showToast('Error al exportar: ' + esc(err.message), 'error');
}).finally(function() {
if (btn) { btn.disabled = false; btn.textContent = '📦 Inventario completo (CSV)'; }
});
}
window.exportFullInventory = exportFullInventory;
function exportCurrentView() {
document.getElementById('exportModal').classList.remove('is-open');
if (typeof window.exportVisibleTableCSV === 'function') {
window.exportVisibleTableCSV('inventario');
}
}
window.exportCurrentView = exportCurrentView;
// =====================================================================
// PURCHASE / ENTRADA (purchaseModal)
// =====================================================================
let purchaseSearchTimeout = null;
let purchaseSelectedItem = null;
function showPurchaseModal() {
document.getElementById('purchaseModal').classList.add('is-open');
setTimeout(function() {
var el = document.getElementById('purchaseItemSearch');
if (el) el.focus();
}, 100);
}
function showPurchaseModalForItem(itemId) {
// Pre-fill by fetching item details
apiFetch(API + '/items?page=1&per_page=1').then(function() {
// We just need the item detail; use the existing list or fetch by id
apiFetch(API + '/items?page=1&per_page=1').then(function() {});
});
selectPurchaseItem({id: itemId, name: 'Producto #' + itemId});
showPurchaseModal();
}
function closePurchaseModal() {
document.getElementById('purchaseModal').classList.remove('is-open');
document.getElementById('purchaseResult').innerHTML = '';
clearPurchaseSelection();
}
function clearPurchaseSelection() {
purchaseSelectedItem = null;
var ids = ['purchaseItemId','purchaseItemSearch','purchaseQty','purchaseCost','purchaseInvoice','purchaseNotes'];
ids.forEach(function(id) {
var el = document.getElementById(id);
if (el) el.value = '';
});
var results = document.getElementById('purchaseItemResults');
if (results) results.style.display = 'none';
var selected = document.getElementById('purchaseItemSelected');
if (selected) selected.textContent = '';
}
function selectPurchaseItem(item) {
purchaseSelectedItem = item;
document.getElementById('purchaseItemId').value = item.id;
document.getElementById('purchaseItemSearch').value = item.name || item.part_number || item.barcode || ('#' + item.id);
document.getElementById('purchaseItemResults').style.display = 'none';
document.getElementById('purchaseItemSelected').innerHTML =
'<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.data) || [];
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.data) || [];
var exact = items.find(function(it) {
return (it.barcode || '').toLowerCase() === query.toLowerCase() ||
(it.part_number || '').toLowerCase() === query.toLowerCase();
});
if (exact) {
selectPurchaseItem(exact);
} else if (items.length === 1) {
selectPurchaseItem(items[0]);
} else {
searchPurchaseItems(query);
}
});
} else if (e.key === 'Escape') {
if (resultsEl) resultsEl.style.display = 'none';
}
});
document.addEventListener('click', function(e) {
if (resultsEl && !input.contains(e.target) && !resultsEl.contains(e.target)) {
resultsEl.style.display = 'none';
}
});
}
function recordPurchase() {
var data = {
inventory_id: parseInt(document.getElementById('purchaseItemId').value),
quantity: parseInt(document.getElementById('purchaseQty').value),
unit_cost: parseFloat(document.getElementById('purchaseCost').value),
supplier_invoice: document.getElementById('purchaseInvoice').value.trim(),
notes: document.getElementById('purchaseNotes').value.trim()
};
if (!data.inventory_id || !data.quantity || !data.unit_cost) {
document.getElementById('purchaseResult').innerHTML = '<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)
// =====================================================================
var transferLines = [];
var transferDocOpen = null;
function showTransferModal() {
document.getElementById('transferModal').classList.add('is-open');
loadTransferBranches();
renderTransferLines();
loadTransferDocs();
setTimeout(function () { var el = document.getElementById('transferScan'); if (el) el.focus(); }, 100);
}
function closeTransferModal() {
document.getElementById('transferModal').classList.remove('is-open');
document.getElementById('transferResult').innerHTML = '';
}
window.showTransferModal = showTransferModal;
window.closeTransferModal = closeTransferModal;
window.transferAddByCode = transferAddByCode;
window.countAddByCode = countAddByCode;
function switchTransferTab(tab) {
document.getElementById('tr-panel-new').style.display = tab === 'new' ? '' : 'none';
document.getElementById('tr-panel-list').style.display = tab === 'list' ? '' : 'none';
document.getElementById('tr-tab-new').classList.toggle('active', tab === 'new');
document.getElementById('tr-tab-list').classList.toggle('active', tab === 'list');
document.getElementById('transferNewFooter').style.display = tab === 'new' ? 'flex' : 'none';
if (tab === 'list') loadTransferDocs();
}
function loadTransferBranches() {
fetch('/pos/api/config/branches', { headers: { 'Authorization': 'Bearer ' + getToken() } })
.then(function (r) { return r.json(); })
.then(function (d) {
var sel = document.getElementById('transferTo');
sel.innerHTML = '<option value="">-- Seleccionar --</option>';
(d.data || []).forEach(function (b) {
sel.innerHTML += '<option value="' + b.id + '">' + esc(b.name) + '</option>';
});
}).catch(function () {});
}
function renderTransferLines() {
var el = document.getElementById('transferLines');
if (!transferLines.length) {
el.innerHTML = '<p style="padding:10px;color:var(--color-text-muted);font-size:var(--text-caption);">Escanea o busca productos para agregarlos</p>';
return;
}
el.innerHTML = transferLines.map(function (l, i) {
return '<div style="display:flex;align-items:center;gap:8px;padding:8px 10px;border-bottom:1px solid var(--color-border);">'
+ '<span style="flex:1;font-size:var(--text-body-sm);">' + esc(l.name) + ' <span style="font-family:var(--font-mono);color:var(--color-text-muted);">' + esc(l.part_number) + '</span> · Stock origen: ' + l.stock + '</span>'
+ '<input type="number" min="1" value="' + l.quantity + '" onchange="Inventory.transferLineQty(' + i + ', this.value)" style="width:80px;" class="form-input" />'
+ '<button class="btn btn--ghost btn--sm" onclick="Inventory.transferLineRemove(' + i + ')">✕</button>'
+ '</div>';
}).join('');
}
function transferAddByCode(code) {
apiFetch(API + '/items/lookup?code=' + encodeURIComponent(code))
.then(function (d) {
if (!d || !d.found) {
document.getElementById('transferResult').innerHTML = '<span style="color:var(--color-error);">Código no encontrado: ' + esc(code) + '</span>';
return;
}
var it = d.item;
var existing = transferLines.find(function (l) { return l.inventory_id === it.id; });
if (existing) existing.quantity += 1;
else transferLines.push({ inventory_id: it.id, part_number: it.part_number, name: it.name, stock: it.stock, quantity: 1 });
renderTransferLines();
}).catch(function () {
document.getElementById('transferResult').innerHTML = '<span style="color:var(--color-error);">Error de búsqueda</span>';
});
}
function transferLineQty(i, v) {
transferLines[i].quantity = Math.max(1, parseInt(v, 10) || 1);
}
function transferLineRemove(i) {
transferLines.splice(i, 1);
renderTransferLines();
}
function transferDocSave(andSend) {
var toBranch = parseInt(document.getElementById('transferTo').value);
if (!toBranch) { alert('Selecciona sucursal destino'); return; }
if (!transferLines.length) { alert('Agrega al menos un producto'); return; }
apiFetch(API + '/transfer-docs', {
method: 'POST',
body: JSON.stringify({
to_branch_id: toBranch,
notes: document.getElementById('transferNotes').value.trim(),
items: transferLines.map(function (l) {
return { inventory_id: l.inventory_id, quantity: l.quantity };
}),
})
}).then(function (d) {
if (!d || !d.id) throw new Error(d ? d.error || 'Error' : 'Error de red');
if (!andSend) {
document.getElementById('transferResult').innerHTML = '<span style="color:var(--color-success);">Borrador #' + d.id + ' guardado</span>';
transferLines = [];
renderTransferLines();
return;
}
return apiFetch(API + '/transfer-docs/' + d.id + '/send', { method: 'POST', body: '{}' })
.then(function (s) {
document.getElementById('transferResult').innerHTML = '<span style="color:var(--color-success);">Transferencia #' + d.id + ' enviada</span>';
transferLines = [];
renderTransferLines();
if (window.loadInventoryStats) window.loadInventoryStats();
});
}).catch(function (e) {
document.getElementById('transferResult').innerHTML = '<span style="color:var(--color-error);">' + esc(e.message) + '</span>';
});
}
function loadTransferDocs() {
apiFetch(API + '/transfer-docs').then(function (d) {
var docs = (d && d.data) || [];
var el = document.getElementById('transferDocsList');
if (!docs.length) {
el.innerHTML = '<p style="padding:10px;color:var(--color-text-muted);font-size:var(--text-caption);">Sin transferencias registradas</p>';
return;
}
var statusColor = { draft: 'var(--color-text-muted)', sent: 'var(--color-warning)', received: 'var(--color-success)', cancelled: 'var(--color-error)' };
el.innerHTML = docs.map(function (t) {
return '<div style="display:flex;align-items:center;gap:10px;padding:10px;border-bottom:1px solid var(--color-border);cursor:pointer;" onclick="Inventory.openTransferDoc(' + t.id + ')">'
+ '<div style="flex:1;"><strong>#' + t.id + '</strong> ' + esc(t.from_name) + ' → ' + esc(t.to_name)
+ ' <span style="color:' + (statusColor[t.status] || 'auto') + ';font-weight:600;">[' + t.status + ']</span>'
+ '<div style="font-size:var(--text-caption);color:var(--color-text-muted);">' + t.total_qty + ' piezas · ' + (t.created_at || '').slice(0, 16).replace('T', ' ') + '</div></div>'
+ '<span style="color:var(--color-text-muted);">›</span></div>';
}).join('');
});
}
function openTransferDoc(id) {
apiFetch(API + '/transfer-docs/' + id).then(function (d) {
switchTransferTab('new');
document.getElementById('tr-panel-new').style.display = 'none';
document.getElementById('tr-panel-list').style.display = '';
transferDocOpen = d;
var el = document.getElementById('transferDocsList');
var html = '<div style="padding:10px;"><button class="btn btn--ghost btn--sm" onclick="Inventory.switchTransferTab(\'list\')">← Volver</button></div>';
html += '<div style="padding:0 10px;"><strong>#' + d.id + '</strong> ' + esc(d.from_name) + ' → ' + esc(d.to_name)
+ ' <span style="font-weight:600;">[' + d.status + ']</span></div>';
html += '<div style="padding:10px;">';
d.lines.forEach(function (l) {
html += '<div style="display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid var(--color-border);">'
+ '<span style="flex:1;font-size:var(--text-body-sm);">' + esc(l.name) + ' <span style="font-family:var(--font-mono);color:var(--color-text-muted);">' + esc(l.part_number) + '</span></span>'
+ '<span>Env: ' + l.quantity + '</span>';
if (d.status === 'sent') {
html += '<label style="font-size:var(--text-caption);">Recibido:</label>'
+ '<input type="number" min="0" max="' + l.quantity + '" value="' + l.qty_received + '" id="tr-rec-' + l.id + '" class="form-input" style="width:70px;" />';
} else {
html += '<span>Rec: ' + l.qty_received + '</span>';
}
html += '</div>';
});
html += '</div>';
html += '<div style="padding:0 10px 10px;">';
if (d.status === 'draft') {
html += '<button class="btn btn--primary" onclick="Inventory.transferDocSend(' + d.id + ')">Enviar (descontar origen)</button> ';
}
if (d.status === 'sent') {
html += '<button class="btn btn--primary" onclick="Inventory.transferDocReceive(' + d.id + ')" style="background:var(--color-success);">Confirmar recepción</button> ';
}
if (d.status === 'draft') {
html += '<button class="btn btn--ghost" onclick="Inventory.transferDocCancel(' + d.id + ')" style="color:var(--color-error);">Cancelar</button>';
}
html += '</div><div id="transferResult" style="padding:0 10px;min-height:1.5em;"></div>';
el.innerHTML = html;
});
}
function transferDocSend(id) {
apiFetch(API + '/transfer-docs/' + id + '/send', { method: 'POST', body: '{}' })
.then(function (r) {
if (r && r.ok) { if (window.loadInventoryStats) window.loadInventoryStats(); openTransferDoc(id); }
else alert((r && r.error) || 'Error');
});
}
function transferDocReceive(id) {
var received = {};
document.querySelectorAll('[id^="tr-rec-"]').forEach(function (inp) {
var lineId = parseInt(inp.id.replace('tr-rec-', ''), 10);
received[lineId] = parseInt(inp.value, 10) || 0;
});
apiFetch(API + '/transfer-docs/' + id + '/receive', {
method: 'POST', body: JSON.stringify({ received: received })
}).then(function (r) {
if (r && r.ok) {
alert('Recepción confirmada' + (r.differences ? ' — ' + r.differences + ' diferencia(s) ajustadas en destino' : ''));
if (window.loadInventoryStats) window.loadInventoryStats();
loadItems(currentPage);
openTransferDoc(id);
} else alert((r && r.error) || 'Error');
});
}
function transferDocCancel(id) {
if (!confirm('¿Cancelar el borrador de transferencia?')) return;
apiFetch(API + '/transfer-docs/' + id + '/cancel', { method: 'POST', body: '{}' })
.then(function () { loadTransferDocs(); });
}
// =====================================================================
// OPERATIONS LIST (Entradas, Salidas, Traspasos, Ajustes)
// =====================================================================
var opTypeMap = {
'entradas': 'PURCHASE',
'salidas': 'SALE',
'traspasos': 'TRANSFER',
'ajustes': 'ADJUST'
};
var opColspan = { entradas: 8, salidas: 7, traspasos: 8, ajustes: 7 };
function loadOperations(type, page) {
var opType = opTypeMap[type];
if (!opType) return;
page = page || 1;
var params = new URLSearchParams({ type: opType, page: page, per_page: 50 });
apiFetch(API + '/operations?' + params.toString()).then(function (data) {
if (!data) return;
var tbodyId = type + 'TableBody';
var footerId = type + 'Footer';
var pagId = type + 'Pagination';
var tbody = document.getElementById(tbodyId);
var ops = data.data || [];
if (!ops.length) {
tbody.innerHTML = '<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) + ')"') + '>&lsaquo;</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) + ')"') + '>&rsaquo;</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');
}
var countRows = [];
function addCountLine() {
var input = document.getElementById('countScanInput');
if (input) input.focus();
}
function countAddByCode(code, qty) {
apiFetch(API + '/items/lookup?code=' + encodeURIComponent(code))
.then(function (d) {
if (!d || !d.found) {
alert('Código no encontrado: ' + code);
return;
}
var it = d.item;
var row = countRows.find(function (r) { return r.inventory_id === it.id; });
if (row) row.counted_quantity += (qty || 1);
else countRows.push({
inventory_id: it.id, part_number: it.part_number, name: it.name,
expected: it.stock, counted_quantity: (qty || 1),
});
renderCountRows();
});
}
function renderCountRows() {
var el = document.getElementById('countLines');
if (!countRows.length) {
el.innerHTML = '<p style="padding:10px;color:var(--color-text-muted);font-size:var(--text-caption);">Sin productos escaneados</p>';
return;
}
el.innerHTML = countRows.map(function (r, i) {
var diff = r.counted_quantity - r.expected;
var color = diff === 0 ? 'var(--color-success)' : (diff < 0 ? 'var(--color-error)' : 'var(--color-warning)');
return '<div style="display:flex;align-items:center;gap:8px;padding:8px 10px;border-bottom:1px solid var(--color-border);">'
+ '<span style="flex:1;font-size:var(--text-body-sm);">' + esc(r.name) + ' <span style="font-family:var(--font-mono);color:var(--color-text-muted);">' + esc(r.part_number) + '</span></span>'
+ '<span style="font-size:var(--text-caption);color:var(--color-text-muted);">Esp: ' + r.expected + '</span>'
+ '<input type="number" min="0" value="' + r.counted_quantity + '" onchange="Inventory.countQty(' + i + ', this.value)" class="form-input" style="width:80px;" />'
+ '<span style="width:60px;text-align:right;color:' + color + ';font-weight:600;">' + (diff > 0 ? '+' : '') + diff + '</span>'
+ '<button class="btn btn--ghost btn--sm" onclick="Inventory.countRemove(' + i + ')">✕</button>'
+ '</div>';
}).join('');
}
function countQty(i, v) {
countRows[i].counted_quantity = Math.max(0, parseInt(v, 10) || 0);
renderCountRows();
}
function countRemove(i) {
countRows.splice(i, 1);
renderCountRows();
}
function startPhysicalCount() {
var items = countRows.map(function (r) {
return { inventory_id: r.inventory_id, counted_quantity: r.counted_quantity };
});
if (!items.length) { alert('Escanea al menos un producto'); return; }
apiFetch(API + '/physical-count/start', { method: 'POST', body: JSON.stringify({ items: items }) }).then(function (result) {
if (!result || !result.count_id) {
document.getElementById('countResults').innerHTML = '<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 += '<button class="btn btn--primary" style="margin-top:var(--space-3);" onclick="approvePhysicalCount(' + result.count_id + ')">Aprobar y Ajustar Inventario</button>';
document.getElementById('countResults').innerHTML = html;
countRows = [];
renderCountRows();
}).catch(function (e) {
document.getElementById('countResults').innerHTML = '<span style="color:var(--color-error);">' + esc(e.message) + '</span>';
});
}
// =====================================================================
function uploadItemImage(itemId) {
var input = document.createElement('input');
input.type = 'file';
input.accept = 'image/jpeg,image/png,image/webp';
input.onchange = function () {
if (!input.files || !input.files[0]) return;
var file = input.files[0];
if (file.size > 5 * 1024 * 1024) {
alert('Imagen demasiado grande (max 5 MB)');
return;
}
var fd = new FormData();
fd.append('file', file);
var statusEl = document.getElementById('imgUploadStatus');
if (statusEl) statusEl.textContent = 'Subiendo...';
fetch(API + '/items/' + itemId + '/image', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + getToken() },
body: fd
})
.then(function (r) { return r.json(); })
.then(function (result) {
if (result.image_url) {
// Refresh detail view
viewProductDetail(itemId);
} else {
if (statusEl) statusEl.textContent = result.error || 'Error';
}
})
.catch(function () {
if (statusEl) statusEl.textContent = 'Error de red';
});
};
input.click();
}
function deleteItemImage(itemId) {
if (!confirm('Eliminar imagen de este producto?')) return;
fetch(API + '/items/' + itemId + '/image', {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + getToken() }
})
.then(function (r) { return r.json(); })
.then(function (result) {
if (result.message) {
viewProductDetail(itemId);
} else {
alert(result.error || 'Error');
}
})
.catch(function () { alert('Error de red'); });
}
function viewProductDetail(itemId) {
apiFetch(API + '/items/' + itemId).then(function (data) {
if (!data || data.error) {
alert(data ? data.error : 'Error de red');
return;
}
var history = data.history || [];
var html = '';
// Tab styles
html += '<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 ' + getToken() } })
.then(function(r) { return r.json(); })
.then(function(d) {
var el = document.getElementById('crossRefContent');
if (!el) return;
var alternatives = d.alternatives || [];
var bodegas = d.bodegas || [];
// If it was a search, get alternatives from first result
if (!catalogPartId && d.data && d.data.length > 0) {
// Fetch detail for first match
fetch('/pos/api/catalog/part/' + d.data[0].id_part, { headers: { 'Authorization': 'Bearer ' + getToken() } })
.then(function(r2) { return r2.json(); })
.then(function(d2) {
renderCrossRefs(el, d2.alternatives || [], d2.bodegas || []);
})
.catch(function() { el.innerHTML = '<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 ' + getToken() } })
.then(function(r) { return r.json(); })
.then(function(d) {
var el = document.getElementById('skuAliasContent');
if (!el) return;
var list = d.aliases || [];
var html2 = '';
if (list.length > 0) {
html2 += '<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 ' + getToken() } })
.then(function(r) { return r.json(); })
.then(function(d) {
var el = document.getElementById('compatContent');
if (!el) return;
var list = d.vehicles || [];
var html2 = '';
if (list.length > 0) {
html2 += '<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 ' + getToken() } })
.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);">' + formatDateTime(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 ' + getToken() }
}).then(function(r) { return r.json(); })
.then(function(d) {
var msg = '';
if (d.tecdoc && d.qwen) {
var t = d.tecdoc.matched ? (d.tecdoc.matched_count || d.tecdoc.matches ? d.tecdoc.matches.length : 0) : 0;
var q = d.qwen.total_qwen || 0;
var qi = d.qwen.inserted || 0;
msg = 'Auto-match completado.\nTecDoc: ' + t + ' vehiculos.\nIA QWEN: ' + qi + ' nuevos vinculados (de ' + q + ' encontrados).';
} else if (d.myes) {
msg = 'Auto-match completado. Vehiculos encontrados: ' + (d.total_qwen || d.myes.length) + ' (nuevos vinculados: ' + (d.inserted || 0) + ')';
} else {
msg = 'Auto-match completado. Vehiculos vinculados: ' + (d.matched ? 'Si' : 'No');
}
alert(msg);
viewProductDetail(itemId);
}).catch(function() { alert('Error en auto-match'); });
}
function removeCompat(itemId, compatId) {
if (!confirm('Quitar compatibilidad con este vehiculo?')) return;
fetch('/pos/api/inventory/items/' + itemId + '/vehicles/' + compatId, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + getToken() }
}).then(function(r) { return r.json(); })
.then(function() {
viewProductDetail(itemId);
}).catch(function() { alert('Error al quitar compatibilidad'); });
}
// Manual compatibility tab functions
window.switchCompatTab = function(tab, btn) {
document.querySelectorAll('.compat-tab-btn').forEach(function(b) { b.classList.remove('is-active'); });
document.querySelectorAll('.compat-tab-panel').forEach(function(p) { p.classList.remove('is-active'); });
btn.classList.add('is-active');
document.getElementById('compatTab-' + tab).classList.add('is-active');
};
window.onManualMakeChange = function(itemId) {
var sel = document.getElementById('manualMake');
var modelSel = document.getElementById('manualModel');
var yearSel = document.getElementById('manualYear');
var engineSel = document.getElementById('manualEngine');
if (!sel || !modelSel) return;
var opt = sel.options[sel.selectedIndex];
var brandId = opt ? opt.getAttribute('data-id') : null;
modelSel.innerHTML = '<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 ' + getToken() } })
.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 ' + getToken() } })
.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 ' + getToken() } })
.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 ' + getToken(), 'Content-Type': 'application/json' },
body: JSON.stringify({ make: make, model: model, year: parseInt(year), engine: engine, engine_code: engineCode })
}).then(function(r) { return r.json(); })
.then(function(d) {
if (d.error) { alert(d.error); return; }
viewProductDetail(itemId);
}).catch(function() { alert('Error al agregar compatibilidad'); });
};
// SKU alias actions
window.addSkuAlias = function(itemId) {
var skuEl = document.getElementById('newAliasSku-' + itemId);
var labelEl = document.getElementById('newAliasLabel-' + itemId);
var sku = skuEl ? skuEl.value.trim() : '';
var label = labelEl ? labelEl.value.trim() : '';
if (!sku) { alert('Ingresa un SKU'); return; }
fetch('/pos/api/inventory/items/' + itemId + '/skus', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + getToken(), 'Content-Type': 'application/json' },
body: JSON.stringify({ sku: sku, label: label })
}).then(function(r) { return r.json(); })
.then(function(d) {
if (d.error) { alert(d.error); return; }
viewProductDetail(itemId);
}).catch(function() { alert('Error al agregar SKU'); });
};
window.removeSkuAlias = function(itemId, aliasId) {
if (!confirm('Eliminar este SKU alternativo?')) return;
fetch('/pos/api/inventory/items/' + itemId + '/skus/' + aliasId, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + getToken() }
}).then(function(r) { return r.json(); })
.then(function() {
viewProductDetail(itemId);
}).catch(function() { alert('Error al eliminar SKU'); });
};
// =====================================================================
// RESTAURADAS (recuperadas de git HEAD)
// =====================================================================
function viewHistory(itemId) {
apiFetch(API + '/items/' + itemId + '/history').then(function (data) {
if (!data) return;
var history = data.data || [];
var html = '';
if (!history.length) {
html = renderEmptyState({
icon: '<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);">' + formatDateTime(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');
}
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>';
}
});
}
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>';
}
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 code128Svg(data, width, height) {
data = String(data || '');
if (!data.length) return '';
var codes = [104];
var sum = 104;
for (var i = 0; i < data.length; i++) {
var c = data.charCodeAt(i);
if (c < 32 || c > 126) c = 32;
var v = c - 32;
codes.push(v);
sum += v * (i + 1);
}
codes.push(sum % 103);
codes.push(106);
var quiet = 10;
var modules = quiet * 2;
codes.forEach(function (cd) {
CODE128_BARS[cd].split('').forEach(function (w) { modules += parseInt(w, 10); });
});
var mw = width / modules;
var bars = '';
var x = quiet * mw;
codes.forEach(function (cd) {
var pat = CODE128_BARS[cd];
for (var k = 0; k < pat.length; k++) {
var w = parseInt(pat[k], 10) * mw;
if (k % 2 === 0) bars += '<rect x="' + x.toFixed(2) + '" y="0" width="' + w.toFixed(2) + '" height="' + height + '"/>';
x += w;
}
});
return '<svg xmlns="http://www.w3.org/2000/svg" width="' + width + '" height="' + height + '" viewBox="0 0 ' + width + ' ' + height + '" fill="#000">' + bars + '</svg>';
}
function printBarcode(barcode, partNumber, name) {
var w = window.open('', '_blank', 'width=420,height=300');
w.document.write('<html><head><title>Etiqueta</title><style>body{font-family:system-ui,sans-serif;text-align:center;padding:16px;margin:0;}h1{font-size:1rem;margin:4px 0;}p{margin:2px 0;font-size:0.85rem;}</style></head><body>');
w.document.write(code128Svg(barcode || partNumber, 320, 70));
w.document.write('<h1>' + (barcode || partNumber) + '</h1>');
w.document.write('<p style="font-weight:600;">' + partNumber + '</p>');
w.document.write('<p style="font-size:0.85rem;">' + name + '</p>');
w.document.write('</body></html>');
w.document.close();
w.print();
}
function deleteItem(itemId) {
if (!confirm('¿Eliminar este artículo del inventario? Se mantendrán los registros históricos.')) return;
fetch(API + '/items/' + itemId, {
method: 'DELETE',
headers: getToken() ? { 'Authorization': 'Bearer ' + getToken() } : {}
}).then(function(r) { return r.json(); })
.then(function(data) {
if (data.error) { showToast(data.error, 'error', { title: 'Error al eliminar' }); return; }
showToast('El artículo fue eliminado correctamente.', 'ok', { title: 'Eliminado' });
loadItems(currentPage);
if (window.loadInventoryStats) window.loadInventoryStats();
}).catch(function() { showToast('No se pudo eliminar el artículo. Intenta de nuevo.', 'error', { title: 'Error' }); });
}
function publishToMeli(itemId) {
selectedItems.clear();
selectedItems.add(itemId);
updateSelectionUI();
openMeliPublishModal();
}
window.publishToMeli = publishToMeli;
// ─── MercadoLibre Bulk Publish Modal ───────────────────────────────────
var meliPreviewData = {};
var meliCategoryAttrs = [];
var meliCategorySearchTimeout;
var meliCatItems = [];
var meliCatActiveIndex = -1;
window.openMeliPublishModal = function() {
if (selectedItems.size === 0) { showToast('Selecciona al menos un producto', 'warn'); return; }
document.getElementById('meliPublishModal').classList.add('is-open');
document.getElementById('meliPublishResult').innerHTML = '';
document.getElementById('meliCategoryId').value = '';
document.getElementById('meliCategorySearch').value = '';
document.getElementById('meliCategoryResults').innerHTML = '';
document.getElementById('meliAttrsSection').style.display = 'none';
document.getElementById('meliAttrsGrid').innerHTML = '';
meliCategoryAttrs = [];
refreshMeliPublishPreview();
};
window.closeMeliPublishModal = function() {
document.getElementById('meliPublishModal').classList.remove('is-open');
};
// ==== RESTAURADAS v4 (MELI + descuentos) ====
window.searchMeliCategories = function() {
var q = document.getElementById('meliCategorySearch').value.trim();
var resultsDiv = document.getElementById('meliCategoryResults');
if (q.length < 2) { resultsDiv.innerHTML = ''; meliCatItems = []; meliCatActiveIndex = -1; return; }
clearTimeout(meliCategorySearchTimeout);
resultsDiv.innerHTML = '<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 ' + getToken(), 'Content-Type': 'application/json' } })
.then(function(r) { return r.json(); })
.then(function(data) {
var cats = data.categories || [];
meliCatItems = cats.slice(0, 10);
meliCatActiveIndex = -1;
if (!meliCatItems.length) { resultsDiv.innerHTML = '<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.handleMeliCatKeydown = function(e) {
if (!meliCatItems.length) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
meliCatActiveIndex = Math.min(meliCatActiveIndex + 1, meliCatItems.length - 1);
highlightMeliCat(meliCatActiveIndex);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
meliCatActiveIndex = Math.max(meliCatActiveIndex - 1, -1);
highlightMeliCat(meliCatActiveIndex);
} else if (e.key === 'Enter') {
e.preventDefault();
if (meliCatActiveIndex >= 0) selectMeliCategoryIdx(meliCatActiveIndex);
} else if (e.key === 'Escape') {
document.getElementById('meliCategoryResults').innerHTML = '';
meliCatItems = [];
meliCatActiveIndex = -1;
}
};
/* Cerrar dropdown al hacer click fuera */
document.addEventListener('click', function(e) {
var field = document.getElementById('meliCategorySearch');
var results = document.getElementById('meliCategoryResults');
if (field && results && !field.contains(e.target) && !results.contains(e.target)) {
results.innerHTML = '';
meliCatItems = [];
meliCatActiveIndex = -1;
}
});
window.onMeliShippingChange = function() {
var mode = document.getElementById('meliShippingMode').value;
var costField = document.getElementById('meliShippingCostField');
if (costField) costField.style.display = (mode === 'custom') ? 'block' : 'none';
};
// ─── MELI publish helpers ──────────────────────────────────────────────
// Contrato de `custom_data` esperado por validate_items / publish_items
// (pos/services/marketplace_external_service.py):
// {
// titles: { "<inventory_id>": "Título (<=60)" },
// prices: { "<inventory_id>": 123.45 },
// stocks: { "<inventory_id>": 3 },
// attributes: { "<inventory_id>": [{ id: "BRAND", value_name: "Nissan" }] },
// shipping_cost: 150 // solo se usa si shipping_mode === 'custom'
// }
// Las claves son el id de inventario como string (el backend consulta con str(inv_id)).
function _meliAttrValue(attr) {
// Los atributos con lista cerrada se pintan como <select> + input "Otra (escribir)";
// el resto como un input de texto libre.
var selEl = document.getElementById('meliAttrSel-' + attr.id);
if (selEl) {
if (selEl.value === '__other__') {
var otherEl = document.getElementById('meliAttrOther-' + attr.id);
return otherEl ? otherEl.value.trim() : '';
}
return (selEl.value || '').trim();
}
var el = document.getElementById('meliAttr-' + attr.id);
return el ? (el.value || '').trim() : '';
}
function _collectMeliCustomData() {
var ids = Array.from(selectedItems);
var customData = { titles: {}, prices: {}, stocks: {}, attributes: {} };
ids.forEach(function(id) {
var titleEl = document.getElementById('meliTitle-' + id);
var priceEl = document.getElementById('meliPrice-' + id);
var stockEl = document.getElementById('meliStock-' + id);
if (titleEl && titleEl.value.trim()) customData.titles[id] = titleEl.value.trim();
if (priceEl) {
var price = parseFloat(priceEl.value);
if (!isNaN(price)) customData.prices[id] = price;
}
if (stockEl) {
var stock = parseInt(stockEl.value, 10);
if (!isNaN(stock)) customData.stocks[id] = stock;
}
var attrs = [];
(meliCategoryAttrs || []).forEach(function(attr) {
var value = _meliAttrValue(attr);
if (attr.id && value) attrs.push({ id: attr.id, value_name: value });
});
if (attrs.length) customData.attributes[id] = attrs;
});
// El backend solo lo aplica con shipping_mode === 'custom'; si el campo no
// existe o está vacío simplemente se omite.
var shippingCostEl = document.getElementById('meliShippingCost');
var shippingCost = shippingCostEl ? parseFloat(shippingCostEl.value) : NaN;
if (!isNaN(shippingCost)) customData.shipping_cost = shippingCost;
return customData;
}
function _renderPublishResult(data, resultEl) {
if (!resultEl) return;
data = data || {};
var successList = data.success || [];
var failedList = data.failed || [];
var html = '<div style="margin-bottom:var(--space-2);"><span style="color:var(--color-success);">✅ ' + successList.length + ' publicado(s)</span> · <span style="color:var(--color-error);">❌ ' + failedList.length + ' fallo(s)</span></div>';
if (successList.length) {
html += '<ul style="margin:0 0 var(--space-2);padding-left:var(--space-4);font-size:var(--text-caption);color:var(--color-text-secondary);">';
successList.forEach(function(s) {
var itemId = s.external_item_id || s.item_id;
var label = 'Item #' + esc(s.inventory_id);
if (itemId) {
label += ' → <a href="' + esc(s.permalink || '#') + '" target="_blank" rel="noopener" style="color:var(--color-primary);">' + esc(itemId) + '</a>';
}
html += '<li>' + label + '</li>';
});
html += '</ul>';
}
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 (successList.length > 0) {
selectedItems.clear();
updateSelectionUI();
if (inventoryVS) inventoryVS.refresh();
}
}
function _pollMeliAsync(taskId, resultEl, btn) {
var attempts = 0;
var maxAttempts = 60; // ~2 min (una consulta cada 2s)
var interval = setInterval(function() {
attempts++;
fetch('/pos/api/marketplace-ext/listings/async/' + encodeURIComponent(taskId), { headers: { 'Authorization': 'Bearer ' + getToken() } })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.error) {
clearInterval(interval);
if (btn) btn.disabled = false;
resultEl.innerHTML = '<span style="color:var(--color-error);">Error: ' + esc(data.error) + '</span>';
return;
}
if (data.status === 'done') {
clearInterval(interval);
if (btn) btn.disabled = false;
_renderPublishResult(data.result || {}, resultEl);
setTimeout(function() { closeMeliPublishModal(); }, 3000);
} else if (attempts >= maxAttempts) {
clearInterval(interval);
if (btn) 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 * 2) + 's)</span>';
}
}).catch(function() {
clearInterval(interval);
if (btn) btn.disabled = false;
resultEl.innerHTML = '<span style="color:var(--color-error);">Error consultando progreso</span>';
});
}, 2000);
}
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 ' + getToken(), '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>'; });
};
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 ' + getToken(), '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>'; });
};
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 ' + getToken(), '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' + (it.images_count > 1 ? ' (' + it.images_count + ')' : '') + '</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.condition) {
var condLabel = it.condition === 'nuevo' ? 'Nuevo' : (it.condition === 'reconstruido' ? 'Reconstruido' : 'Usado');
if (it.condition_grade) condLabel += ' · Grado ' + it.condition_grade;
checks += '<span class="meli-check ok">' + esc(condLabel) + '</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 + '" oninput="scheduleMeliFeeEstimates()"></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 id="meliFee-' + it.inventory_id + '" style="font-size:10px;color:var(--color-text-muted);min-width:130px;line-height:1.35;"></div>' +
'</div>';
});
container.innerHTML = html;
scheduleMeliFeeEstimates();
}).catch(function() { container.innerHTML = '<p style="color:var(--color-error);">Error de red</p>'; });
}
window.selectMeliCategory = function(id, name) {
document.getElementById('meliCategoryId').value = id;
document.getElementById('meliCategorySearch').value = name;
document.getElementById('meliCategoryResults').innerHTML = '';
meliCatItems = [];
meliCatActiveIndex = -1;
loadCategoryAttributes(id);
scheduleMeliFeeEstimates();
};
window.selectMeliCategoryIdx = function(idx) {
var c = meliCatItems[idx];
if (!c) return;
selectMeliCategory(c.category_id, c.category_name || c.category_id);
};
window.highlightMeliCat = function(idx) {
meliCatActiveIndex = idx;
var items = document.querySelectorAll('.meli-cat-item');
items.forEach(function(el, i) { el.classList.toggle('is-active', i === idx); });
};
window.loadCategoryAttributes = function(categoryId) {
var grid = document.getElementById('meliAttrsGrid');
var section = document.getElementById('meliAttrsSection');
grid.innerHTML = '<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 ' + getToken() } })
.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.updateMeliTitleCount = function(id) {
var el = document.getElementById('meliTitle-' + id);
var countEl = document.getElementById('meliTitleCount-' + id);
if (el && countEl) countEl.textContent = el.value.length + '/60';
};
window.handleMeliImageUpload = function(itemId, input) {
if (!input.files || !input.files[0]) return;
var file = input.files[0];
var formData = new FormData();
formData.append('file', file);
fetch('/pos/api/inventory/items/' + itemId + '/image', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + getToken() },
body: formData
}).then(function(r){ return r.json(); })
.then(function(data) {
if (data.image_url) {
showToast('Imagen subida', 'success');
refreshMeliPublishPreview();
if (inventoryVS) inventoryVS.refresh();
} else {
showToast(data.error || 'Error subiendo imagen', 'error');
}
}).catch(function(){ showToast('Error de red', 'error'); });
};
// ─── Estimador de comisiones MeLi (por artículo) ───
var meliFeeTimeout;
function fmtMoney(n) {
return '$' + (parseFloat(n) || 0).toLocaleString('es-MX', {minimumFractionDigits: 2, maximumFractionDigits: 2});
}
function updateMeliFeeEstimates() {
var categoryId = (document.getElementById('meliCategoryId') || {}).value;
var listingType = (document.getElementById('meliListingType') || {}).value || 'gold_special';
var ids = Object.keys(window.meliPreviewData || (typeof meliPreviewData !== 'undefined' ? meliPreviewData : {}));
if (!ids.length) return;
ids.forEach(function (id) {
var feeEl = document.getElementById('meliFee-' + id);
if (!feeEl) return;
if (!categoryId) { feeEl.innerHTML = ''; return; }
var priceEl = document.getElementById('meliPrice-' + id);
var price = parseFloat(priceEl && priceEl.value) || 0;
if (price <= 0) { feeEl.innerHTML = '<span style="color:var(--color-text-muted);">Ingresa un precio para estimar comisiones</span>'; return; }
feeEl.innerHTML = '<span style="color:var(--color-text-muted);">Calculando comisiones...</span>';
fetch('/pos/api/marketplace-ext/listing-prices?price=' + encodeURIComponent(price) + '&category_id=' + encodeURIComponent(categoryId), { headers: { 'Authorization': 'Bearer ' + getToken() } })
.then(function (r) { return r.json().then(function (d) { return {ok: r.ok, data: d}; }); })
.then(function (res) {
if (!res.ok || res.data.error) { feeEl.innerHTML = '<span style="color:var(--color-warning);">No se pudo estimar: ' + esc(res.data.error || 'error') + '</span>'; return; }
var offers = res.data.listing_prices || [];
var offer = null;
offers.forEach(function (o) { if (o.listing_type_id === listingType) offer = o; });
if (!offer && offers.length) offer = offers[0];
if (!offer || !offer.sale_fee_details) { feeEl.innerHTML = ''; return; }
var fee = offer.sale_fee_details.gross_amount || 0;
var pct = offer.sale_fee_details.percentage_fee || 0;
var net = price - fee;
feeEl.innerHTML = '💰 Comisión MeLi: <strong style="color:var(--color-warning);">' + fmtMoney(fee) + ' (' + pct + '%)</strong>'
+ ' · Recibes: <strong style="color:' + (net > 0 ? 'var(--color-success)' : 'var(--color-error)') + ';">' + fmtMoney(net) + '</strong>'
+ ' <span style="opacity:.65;">(estimado, sin costo de envío)</span>';
})
.catch(function () { feeEl.innerHTML = ''; });
});
}
function scheduleMeliFeeEstimates() {
clearTimeout(meliFeeTimeout);
meliFeeTimeout = setTimeout(updateMeliFeeEstimates, 500);
}
window.scheduleMeliFeeEstimates = scheduleMeliFeeEstimates;
// =====================================================================
// EXPOSE GLOBALS (for onclick handlers in HTML)
// =====================================================================
window._loadItems = function (p) { loadItems(p); };
window.loadItems = function (p, q) { loadItems(p, q); };
window.viewHistory = viewHistory;
window.viewProductDetail = viewProductDetail;
window.uploadItemImage = uploadItemImage;
window.deleteItemImage = deleteItemImage;
window.closeHistoryModal = closeHistoryModal;
window.showCreateModal = showCreateModal;
window.closeCreateModal = closeCreateModal;
window.createItem = createItem;
window.showPurchaseModal = showPurchaseModal;
window.showPurchaseModalForItem = showPurchaseModalForItem;
window.closePurchaseModal = closePurchaseModal;
window.recordPurchase = recordPurchase;
window.showAdjustmentModal = showAdjustmentModal;
window.closeAdjustmentModal = closeAdjustmentModal;
window.recordAdjustment = recordAdjustment;
window.showTransferModal = showTransferModal;
window.closeTransferModal = closeTransferModal;
window.recordTransfer = recordTransfer;
window.showCountModal = showCountModal;
window.closeCountModal = closeCountModal;
window.addCountLine = addCountLine;
window.startPhysicalCount = startPhysicalCount;
window.approvePhysicalCount = approvePhysicalCount;
window.cancelDraft = cancelDraft;
window.loadAlerts = loadAlerts;
window.printBarcode = printBarcode;
window.switchTransferTab = switchTransferTab;
window.transferLineQty = transferLineQty;
window.transferLineRemove = transferLineRemove;
window.transferDocSave = transferDocSave;
window.transferDocSend = transferDocSend;
window.transferDocReceive = transferDocReceive;
window.transferDocCancel = transferDocCancel;
window.openTransferDoc = openTransferDoc;
window.loadTransferDocs = loadTransferDocs;
window.countQty = countQty;
window.countRemove = countRemove;
window.Inventory = window.Inventory || {};
window.Inventory = Object.assign(window.Inventory, {
switchTransferTab, transferLineQty, transferLineRemove, transferDocSave,
transferDocSend, transferDocReceive, transferDocCancel, openTransferDoc,
countQty, countRemove, transferAddByCode, countAddByCode,
});
window.deleteItem = deleteItem;
window.autoMatchCompat = autoMatchCompat;
window.removeCompat = removeCompat;
// ─── Product Timeline ──────────────────────────────────────────
window.showProductTimeline = function(itemId) {
var modal = document.getElementById('productTimelineModal');
var body = document.getElementById('productTimelineBody');
body.innerHTML = '<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();
})();