- 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
2602 lines
146 KiB
JavaScript
2602 lines
146 KiB
JavaScript
// /home/Autopartes/pos/static/js/inventory.js
|
||
// Inventory management UI — rewritten to match design-system HTML structure
|
||
// Panels: panel-stock, panel-entradas, panel-salidas, panel-traspasos, panel-ajustes, panel-conteos, panel-alertas
|
||
|
||
(function () {
|
||
'use strict';
|
||
|
||
var API = '/pos/api/inventory';
|
||
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) + ')"') + '>‹</button>' +
|
||
'<span style="padding:0 var(--space-2);font-size:var(--text-body-sm);color:var(--color-text-muted);">' + pg.page + ' / ' + pg.total_pages + ' (' + pg.total + ' productos)</span>' +
|
||
'<button class="page-btn" ' + (pg.page >= pg.total_pages ? 'disabled' : 'onclick="window._loadItems(' + (pg.page + 1) + ')"') + '>›</button>' +
|
||
'</div>';
|
||
} else {
|
||
pgEl.innerHTML = '<span style="font-size:var(--text-body-sm);color:var(--color-text-muted);">' + (pg.total || 0) + ' productos</span>';
|
||
}
|
||
}).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' : '') + '> ' + 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) + ')"') + '>‹</button>' +
|
||
'<span style="padding:0 var(--space-2);font-size:var(--text-body-sm);color:var(--color-text-muted);">' + pg.page + ' / ' + pg.total_pages + ' (' + pg.total + ' registros)</span>' +
|
||
'<button class="page-btn" ' + (pg.page >= pg.total_pages ? 'disabled' : 'onclick="window._loadOperations(\'' + type + '\',' + (pg.page + 1) + ')"') + '>›</button>' +
|
||
'</div>';
|
||
} else {
|
||
document.getElementById(pagId).innerHTML = '';
|
||
}
|
||
document.getElementById(footerId).textContent = (pg.total || 0) + ' registros';
|
||
});
|
||
}
|
||
window.loadOperations = loadOperations;
|
||
window._loadOperations = loadOperations;
|
||
|
||
function renderOperationRow(op, type) {
|
||
var dateStr = op.created_at ? new Date(op.created_at).toLocaleString('es-MX', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : '-';
|
||
var productInfo = esc(op.part_number || op.barcode || '') + ' — ' + esc(op.product_name || '');
|
||
if (type === 'entradas') {
|
||
return '<tr>' +
|
||
'<td class="td--mono">#' + op.id + '</td>' +
|
||
'<td>' + dateStr + '</td>' +
|
||
'<td>' + productInfo + '</td>' +
|
||
'<td style="text-align:right">' + (op.quantity || 0) + '</td>' +
|
||
'<td style="text-align:right" class="td--amount">$' + fmt(op.cost_at_time || 0) + '</td>' +
|
||
'<td style="text-align:right" class="td--amount">$' + fmt(op.total || 0) + '</td>' +
|
||
'<td>' + esc(op.notes || '-') + '</td>' +
|
||
'<td>' + esc(op.employee_name || '-') + '</td>' +
|
||
'</tr>';
|
||
}
|
||
if (type === 'salidas') {
|
||
return '<tr>' +
|
||
'<td class="td--mono">#' + op.id + '</td>' +
|
||
'<td>' + dateStr + '</td>' +
|
||
'<td>' + productInfo + '</td>' +
|
||
'<td style="text-align:right">' + (op.quantity || 0) + '</td>' +
|
||
'<td style="text-align:right" class="td--amount">$' + fmt(op.total || 0) + '</td>' +
|
||
'<td>' + esc(op.notes || '-') + '</td>' +
|
||
'<td>' + esc(op.employee_name || '-') + '</td>' +
|
||
'</tr>';
|
||
}
|
||
if (type === 'traspasos') {
|
||
return '<tr>' +
|
||
'<td class="td--mono">#' + op.id + '</td>' +
|
||
'<td>' + dateStr + '</td>' +
|
||
'<td>' + productInfo + '</td>' +
|
||
'<td style="text-align:right">' + (op.quantity || 0) + '</td>' +
|
||
'<td>' + esc(op.branch_name || '-') + '</td>' +
|
||
'<td>' + esc(op.notes || '-') + '</td>' +
|
||
'<td>' + esc(op.employee_name || '-') + '</td>' +
|
||
'<td></td>' +
|
||
'</tr>';
|
||
}
|
||
if (type === 'ajustes') {
|
||
return '<tr>' +
|
||
'<td class="td--mono">#' + op.id + '</td>' +
|
||
'<td>' + dateStr + '</td>' +
|
||
'<td>' + esc(op.operation_type || 'ADJUST') + '</td>' +
|
||
'<td>' + productInfo + '</td>' +
|
||
'<td style="text-align:right">' + (op.quantity || 0) + '</td>' +
|
||
'<td>' + esc(op.notes || '-') + '</td>' +
|
||
'<td>' + esc(op.employee_name || '-') + '</td>' +
|
||
'</tr>';
|
||
}
|
||
return '<tr><td colspan="8">-</td></tr>';
|
||
}
|
||
|
||
// =====================================================================
|
||
// PHYSICAL COUNT / CONTEO (countModal)
|
||
// =====================================================================
|
||
|
||
function showCountModal() {
|
||
document.getElementById('countModal').classList.add('is-open');
|
||
// Pre-add one line if empty
|
||
if (!document.querySelectorAll('#countLines .count-row').length) {
|
||
addCountLine();
|
||
}
|
||
}
|
||
function closeCountModal() {
|
||
document.getElementById('countModal').classList.remove('is-open');
|
||
}
|
||
|
||
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();
|
||
})();
|