feat(catalog): supplier catalog cleanup, fuzzy matching, and navigation fixes
- Cleaned 137+ fake engine-displacement models from supplier imports (v3/v4 scripts: Chevrolet, Ford, Chrysler, Dodge, Jeep, Nissan, etc.) - Removed 1,251+ corrupted models (INT. prefixes, year-suffix, torque specs, empty names, trailing-year variants) - Migrated supplier tables to master DB (supplier_catalog, supplier_catalog_compat, supplier_catalog_interchange) - Fixed _get_mye_ids_with_parts() to query supplier_catalog_compat from master DB so supplier-only vehicles appear for all tenants - Added fuzzy model matcher with parenthesis stripping, noise suffix removal, compact matching, prefix/substring fallback, model aliases, and ±3 year proximity - Matched compat rows: KEEP GREEN +14,152, KNADIAN +3,021, VAZLO +127,500, LUK +477, RAYBESTOS +1,743 - Added KNADIAN catalog importer with year-range expansion and future-year filtering - Added VAZLO catalog importer with position parsing and SKU-in-model cleanup - Added Keep Green, LUK, Yokomitsu, Raybestos catalog importers - Cache clearing after cleanups (_classify_cache_*, nexus:mye_ids:*, nexus:brand_mye_counts:*) Final match rates: - KEEP GREEN: 90.3% - VAZLO: 93.6% - YOKOMITSU: 100.0% - KNADIAN: 57.4% - LUK: 51.0% - RAYBESTOS: 55.9%
This commit is contained in:
@@ -812,6 +812,18 @@
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.chart-canvas-wrap {
|
||||
position: relative;
|
||||
height: 220px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chart-canvas-wrap canvas {
|
||||
display: block;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.rank-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -197,4 +197,14 @@
|
||||
}).catch(function() {});
|
||||
} catch(e) {}
|
||||
|
||||
// ─── Service Worker update handler ───
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.addEventListener('message', function (event) {
|
||||
if (event.data && event.data.type === 'SW_UPDATED') {
|
||||
console.log('[AppInit] SW updated to', event.data.cacheName, '— reloading...');
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
@@ -195,7 +195,19 @@
|
||||
currentAbort = null;
|
||||
}
|
||||
var opts = { headers: headers };
|
||||
if (url.indexOf('/pos/api/') === 0 && url.indexOf('mode=') !== -1 || url.indexOf('/years') !== -1 || url.indexOf('/brands') !== -1 || url.indexOf('/models') !== -1 || url.indexOf('/engines') !== -1 || url.indexOf('/categories') !== -1 || url.indexOf('/groups') !== -1 || url.indexOf('/part-types') !== -1 || url.indexOf('/parts') !== -1 || url.indexOf('/search') !== -1) {
|
||||
var isCatalogNav = url.indexOf('/pos/api/') === 0 && (
|
||||
url.indexOf('mode=') !== -1 ||
|
||||
url.indexOf('/years') !== -1 ||
|
||||
url.indexOf('/brands') !== -1 ||
|
||||
url.indexOf('/models') !== -1 ||
|
||||
url.indexOf('/engines') !== -1 ||
|
||||
url.indexOf('/categories') !== -1 ||
|
||||
url.indexOf('/groups') !== -1 ||
|
||||
url.indexOf('/part-types') !== -1 ||
|
||||
url.indexOf('/parts') !== -1 ||
|
||||
url.indexOf('/search') !== -1
|
||||
);
|
||||
if (isCatalogNav) {
|
||||
currentAbort = new AbortController();
|
||||
opts.signal = currentAbort.signal;
|
||||
}
|
||||
@@ -233,7 +245,7 @@
|
||||
if (!s) return '';
|
||||
var d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
return d.innerHTML.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ─── Breadcrumb ───
|
||||
@@ -290,9 +302,9 @@
|
||||
|
||||
function resetNav() {
|
||||
nav.level = 'brands';
|
||||
pushNavState();
|
||||
nav.brand = nav.model = nav.year = nav.engine = nav.category = nav.group = nav.partType = null;
|
||||
nav.nxGroup = nav.nxSubgroup = nav.nxPartType = null;
|
||||
pushNavState();
|
||||
}
|
||||
|
||||
function resetNavFrom(level) {
|
||||
@@ -927,9 +939,14 @@
|
||||
partsGrid.querySelectorAll('.part-card').forEach(function (card) {
|
||||
card.addEventListener('click', function () {
|
||||
var pid = this.dataset.partId;
|
||||
var src = this.dataset.source || '';
|
||||
if (typeof pid === 'string' && pid.indexOf('inv:') === 0) {
|
||||
return;
|
||||
}
|
||||
if (src === 'supplier_catalog' || (typeof pid === 'string' && pid.indexOf('sc:') === 0)) {
|
||||
openSupplierDetail(pid.replace('sc:', ''));
|
||||
return;
|
||||
}
|
||||
openPartDetail(parseInt(pid));
|
||||
});
|
||||
});
|
||||
@@ -988,17 +1005,23 @@
|
||||
partsGrid.innerHTML = data.data.map(function (p) {
|
||||
// Stock badge — prefer tenant stock, then warehouse network, else fallback
|
||||
var stockBadge;
|
||||
if (p.local_stock > 0) {
|
||||
var isSupplier = p.source === 'supplier_catalog' || (typeof p.id_part === 'string' && p.id_part.indexOf('sc:') === 0);
|
||||
if (isSupplier) {
|
||||
stockBadge = '<span class="stock-badge stock-badge--none" style="background:#f59e0b;color:#fff;">Cat. Proveedor</span>';
|
||||
} else if (p.local_stock > 0) {
|
||||
stockBadge = '<span class="stock-badge stock-badge--local">En stock: ' + p.local_stock + '</span>';
|
||||
} else if (p.in_stock_network || p.bodega_count > 0) {
|
||||
stockBadge = '<span class="stock-badge stock-badge--bodega">' + p.bodega_count + ' bodega' + (p.bodega_count > 1 ? 's' : '') + '</span>';
|
||||
} else {
|
||||
stockBadge = '<span class="stock-badge stock-badge--none">Sin stock</span>';
|
||||
}
|
||||
// Local inventory native badge
|
||||
var sourceBadge = p.source === 'local_inventory'
|
||||
? '<span class="stock-badge stock-badge--local" style="margin-left:4px;background:#4f46e5;">Stock Local</span>'
|
||||
: '';
|
||||
// Source badge for local inventory or supplier catalog
|
||||
var sourceBadge = '';
|
||||
if (p.source === 'local_inventory') {
|
||||
sourceBadge = '<span class="stock-badge stock-badge--local" style="margin-left:4px;background:#4f46e5;">Stock Local</span>';
|
||||
} else if (isSupplier) {
|
||||
sourceBadge = '<span class="stock-badge stock-badge--local" style="margin-left:4px;background:#f59e0b;color:#fff;">Cat. Proveedor</span>';
|
||||
}
|
||||
|
||||
var imgHtml = p.image_url
|
||||
? '<img src="' + esc(p.image_url) + '" alt="' + esc(p.name) + '" loading="lazy" decoding="async">'
|
||||
@@ -1039,10 +1062,15 @@
|
||||
partsGrid.querySelectorAll('.part-card').forEach(function (card) {
|
||||
card.addEventListener('click', function () {
|
||||
var pid = this.dataset.partId;
|
||||
var src = this.dataset.source || '';
|
||||
if (typeof pid === 'string' && pid.indexOf('inv:') === 0) {
|
||||
// local-inventory item: info already visible on card
|
||||
return;
|
||||
}
|
||||
if (src === 'supplier_catalog' || (typeof pid === 'string' && pid.indexOf('sc:') === 0)) {
|
||||
openSupplierDetail(pid.replace('sc:', ''));
|
||||
return;
|
||||
}
|
||||
openPartDetail(parseInt(pid));
|
||||
});
|
||||
});
|
||||
@@ -1185,6 +1213,73 @@
|
||||
});
|
||||
}
|
||||
|
||||
function openSupplierDetail(supplierId) {
|
||||
detailBody.innerHTML = '<div class="loading is-visible"><div class="spinner"></div></div>';
|
||||
detailFooter.style.display = 'none';
|
||||
detailPanel.classList.add('is-open');
|
||||
detailOverlay.classList.add('is-visible');
|
||||
|
||||
apiFetch('/pos/api/supplier-catalog/items/' + supplierId).then(function (data) {
|
||||
if (!data || data.error) {
|
||||
detailBody.innerHTML = '<p style="color:var(--color-error);padding:var(--space-4);">Error al cargar detalle.</p>';
|
||||
return;
|
||||
}
|
||||
var p = data;
|
||||
var html = '';
|
||||
|
||||
html += '<div class="detail-section">';
|
||||
html += '<div style="font-size:var(--text-caption);color:var(--color-text-muted);margin-bottom:var(--space-1);">' + esc(p.supplier_name) + ' > ' + esc(p.category || '') + '</div>';
|
||||
html += '<div class="detail-oem">' + esc(p.sku) + '</div>';
|
||||
html += '<div class="detail-name">' + esc((p.name || '').replace(/\\n/g, ' ')) + '</div>';
|
||||
if (p.description) html += '<div class="detail-desc">' + esc(p.description) + '</div>';
|
||||
if (p.image_url) html += '<div style="margin-top:var(--space-3);text-align:center;"><img src="' + esc(p.image_url) + '" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:200px;object-fit:contain;border-radius:var(--radius-sm);"></div>';
|
||||
html += '</div>';
|
||||
|
||||
// Interchanges
|
||||
if (p.interchanges && p.interchanges.length) {
|
||||
html += '<div class="detail-section">';
|
||||
html += '<div class="detail-section__title">Intercambios OEM</div>';
|
||||
var seen = {};
|
||||
p.interchanges.forEach(function(ix) {
|
||||
var key = (ix.brand || '') + '|' + (ix.interchange_number || '');
|
||||
if (seen[key]) return;
|
||||
seen[key] = true;
|
||||
html += '<div style="display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid var(--color-border);">' +
|
||||
'<span style="font-weight:600;">' + esc(ix.brand || '') + '</span>' +
|
||||
'<span style="color:var(--color-text-muted);font-family:monospace;">' + esc(ix.interchange_number || '') + '</span>' +
|
||||
'</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Compatibilities — deduplicate by (make, model, year, engine)
|
||||
if (p.compatibilities && p.compatibilities.length) {
|
||||
html += '<div class="detail-section">';
|
||||
html += '<div class="detail-section__title">Vehiculos compatibles</div>';
|
||||
var seenCompat = {};
|
||||
var uniqCompat = [];
|
||||
p.compatibilities.forEach(function(c) {
|
||||
var key = (c.make || '') + '|' + (c.model || '') + '|' + (c.year || '') + '|' + (c.engine || '');
|
||||
if (seenCompat[key]) return;
|
||||
seenCompat[key] = true;
|
||||
uniqCompat.push(c);
|
||||
});
|
||||
var currentMake = '';
|
||||
uniqCompat.forEach(function(c) {
|
||||
if (c.make !== currentMake) {
|
||||
currentMake = c.make;
|
||||
html += '<div style="font-weight:600;margin-top:8px;">' + esc(c.make) + '</div>';
|
||||
}
|
||||
html += '<div style="padding-left:12px;color:var(--color-text-muted);font-size:var(--text-body-sm);">' +
|
||||
esc(c.model) + ' ' + c.year + ' ' + esc(c.engine || '') + '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
detailBody.innerHTML = html;
|
||||
});
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
detailPanel.classList.remove('is-open');
|
||||
detailOverlay.classList.remove('is-visible');
|
||||
@@ -1398,17 +1493,22 @@
|
||||
}
|
||||
searchDropdown.innerHTML = data.data.map(function (r) {
|
||||
var isLocal = r.source === 'local_inventory' || (typeof r.id_part === 'string' && r.id_part.indexOf('inv:') === 0);
|
||||
var isSupplier = r.source === 'supplier_catalog' || (typeof r.id_part === 'string' && r.id_part.indexOf('sc:') === 0);
|
||||
var stockLabel = r.local_stock > 0
|
||||
? '<span class="stock-badge stock-badge--local" style="margin-left:auto;">Stock: ' + r.local_stock + '</span>'
|
||||
: '';
|
||||
var localBadge = isLocal
|
||||
? '<span class="stock-badge stock-badge--local" style="margin-right:4px;background:#4f46e5;">Stock Local</span>'
|
||||
: '';
|
||||
var sourceBadge = '';
|
||||
if (isLocal) {
|
||||
sourceBadge = '<span class="stock-badge stock-badge--local" style="margin-right:4px;background:#4f46e5;">Stock Local</span>';
|
||||
} else if (isSupplier) {
|
||||
sourceBadge = '<span class="stock-badge stock-badge--local" style="margin-right:4px;background:#f59e0b;color:#fff;">Cat. Proveedor</span>';
|
||||
}
|
||||
var oemNum = isLocal ? (r.oem_part_number || r.part_number || '') : (r.oem_part_number || '');
|
||||
return '<div class="search-result-item" data-part-id="' + r.id_part + '" data-name="' + esc(r.name) + '" data-pn="' + esc(oemNum) + '" data-price="' + (r.local_price || '') + '" data-stock="' + (r.local_stock || 0) + '">' +
|
||||
var cleanName = (r.name || '').replace(/\\n/g, ' ');
|
||||
return '<div class="search-result-item" data-part-id="' + r.id_part + '" data-name="' + esc(cleanName) + '" data-pn="' + esc(oemNum) + '" data-price="' + (r.local_price || '') + '" data-stock="' + (r.local_stock || 0) + '" data-source="' + (r.source || '') + '">' +
|
||||
'<div style="flex:1;">' +
|
||||
'<div class="search-result__oem">' + localBadge + esc(oemNum) + '</div>' +
|
||||
'<div class="search-result__name">' + esc(r.name) + '</div>' +
|
||||
'<div class="search-result__oem">' + sourceBadge + esc(oemNum) + '</div>' +
|
||||
'<div class="search-result__name">' + esc(cleanName) + '</div>' +
|
||||
(r.vehicle_info ? '<div class="search-result__vehicle">' + esc(r.vehicle_info) + '</div>' : '') +
|
||||
'</div>' +
|
||||
stockLabel +
|
||||
@@ -1420,6 +1520,7 @@
|
||||
el.addEventListener('click', function () {
|
||||
searchDropdown.classList.remove('is-visible');
|
||||
var pid = this.dataset.partId;
|
||||
var src = this.dataset.source || '';
|
||||
if (typeof pid === 'string' && pid.indexOf('inv:') === 0) {
|
||||
var info = '💠 Stock Local\n\n' +
|
||||
'Parte: ' + (this.dataset.pn || 'N/A') + '\n' +
|
||||
@@ -1429,6 +1530,10 @@
|
||||
alert(info);
|
||||
return;
|
||||
}
|
||||
if (src === 'supplier_catalog' || (typeof pid === 'string' && pid.indexOf('sc:') === 0)) {
|
||||
openSupplierDetail(pid.replace('sc:', ''));
|
||||
return;
|
||||
}
|
||||
openPartDetail(parseInt(pid));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
const token = localStorage.getItem('pos_token') || '';
|
||||
if (!token) return;
|
||||
|
||||
let hourlyChart = null;
|
||||
let topProductsChart = null;
|
||||
|
||||
function headers() {
|
||||
return { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' };
|
||||
}
|
||||
@@ -31,10 +34,11 @@
|
||||
function renderHourlyChart(hourly) {
|
||||
const ctx = document.getElementById('hourlySalesChart');
|
||||
if (!ctx) return;
|
||||
if (hourlyChart) { hourlyChart.destroy(); hourlyChart = null; }
|
||||
const labels = hourly.map(function (h) { return h.hour + ':00'; });
|
||||
const totals = hourly.map(function (h) { return h.total; });
|
||||
|
||||
new Chart(ctx, {
|
||||
hourlyChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
@@ -62,10 +66,38 @@
|
||||
function renderTopProductsChart(topProducts) {
|
||||
const ctx = document.getElementById('topProductsChart');
|
||||
if (!ctx) return;
|
||||
if (topProductsChart) { topProductsChart.destroy(); topProductsChart = null; }
|
||||
if (!topProducts || topProducts.length === 0) {
|
||||
// No sales today — render a friendly empty-state mini chart so the canvas
|
||||
// doesn't collapse or leave a blank hole.
|
||||
topProductsChart = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Sin ventas hoy'],
|
||||
datasets: [{
|
||||
data: [1],
|
||||
backgroundColor: ['rgba(136, 136, 136, 0.25)'],
|
||||
borderWidth: 0,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: { color: '#888', font: { size: 10 }, boxWidth: 10 }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = topProducts.map(function (p) { return p.name.substring(0, 20); });
|
||||
const revenues = topProducts.map(function (p) { return p.revenue; });
|
||||
|
||||
new Chart(ctx, {
|
||||
topProductsChart = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: labels,
|
||||
|
||||
@@ -287,8 +287,41 @@
|
||||
// CREATE ITEM (createModal)
|
||||
// =====================================================================
|
||||
|
||||
function loadCategories() {
|
||||
var sel = document.getElementById('newCategory');
|
||||
if (!sel) return;
|
||||
apiFetch(API + '/categories').then(function(data) {
|
||||
if (!data || !data.categories) return;
|
||||
sel.innerHTML = '<option value="">Selecciona categoría</option>';
|
||||
data.categories.forEach(function(c) {
|
||||
sel.innerHTML += '<option value="' + c.id + '">' + esc(c.name) + '</option>';
|
||||
});
|
||||
});
|
||||
}
|
||||
window.loadCategories = loadCategories;
|
||||
|
||||
function onCategoryChange(categoryId) {
|
||||
var subSel = document.getElementById('newSubcategory');
|
||||
if (!subSel) return;
|
||||
if (!categoryId) {
|
||||
subSel.innerHTML = '<option value="">Selecciona categoría primero</option>';
|
||||
subSel.disabled = true;
|
||||
return;
|
||||
}
|
||||
apiFetch(API + '/categories/' + categoryId + '/subcategories').then(function(data) {
|
||||
if (!data || !data.subcategories) return;
|
||||
subSel.innerHTML = '<option value="">Selecciona subcategoría</option>';
|
||||
data.subcategories.forEach(function(s) {
|
||||
subSel.innerHTML += '<option value="' + s.id + '">' + esc(s.name) + '</option>';
|
||||
});
|
||||
subSel.disabled = false;
|
||||
});
|
||||
}
|
||||
window.onCategoryChange = onCategoryChange;
|
||||
|
||||
function showCreateModal() {
|
||||
document.getElementById('createModal').classList.add('is-open');
|
||||
loadCategories();
|
||||
// Attach AI classification on part number blur
|
||||
var pnInput = document.getElementById('newPartNumber');
|
||||
if (pnInput && !pnInput._classifyBound) {
|
||||
@@ -334,6 +367,10 @@
|
||||
function closeCreateModal() {
|
||||
document.getElementById('createModal').classList.remove('is-open');
|
||||
document.getElementById('createResult').innerHTML = '';
|
||||
var catSel = document.getElementById('newCategory');
|
||||
var subSel = document.getElementById('newSubcategory');
|
||||
if (catSel) catSel.innerHTML = '<option value="">Selecciona categoría</option>';
|
||||
if (subSel) { subSel.innerHTML = '<option value="">Selecciona categoría primero</option>'; subSel.disabled = true; }
|
||||
}
|
||||
|
||||
function createItem() {
|
||||
@@ -350,8 +387,20 @@
|
||||
price_3: elPrice3 ? (parseFloat(elPrice3.value) || 0) : 0,
|
||||
min_stock: parseInt(document.getElementById('newMinStock').value) || 0,
|
||||
initial_stock: parseInt(document.getElementById('newInitialStock').value) || 0,
|
||||
location: document.getElementById('newLocation').value.trim()
|
||||
location: document.getElementById('newLocation').value.trim(),
|
||||
sku_aliases: []
|
||||
};
|
||||
var sku2 = document.getElementById('newSku2').value.trim();
|
||||
var sku3 = document.getElementById('newSku3').value.trim();
|
||||
var categoryId = document.getElementById('newCategory').value;
|
||||
var subcategoryId = document.getElementById('newSubcategory').value;
|
||||
if (sku2) data.sku_aliases.push({sku: sku2, label: 'Alternativo 1'});
|
||||
if (sku3) data.sku_aliases.push({sku: sku3, label: 'Alternativo 2'});
|
||||
if (subcategoryId) {
|
||||
data.category_id = parseInt(subcategoryId);
|
||||
} else if (categoryId) {
|
||||
data.category_id = parseInt(categoryId);
|
||||
}
|
||||
if (!data.part_number || !data.name) {
|
||||
document.getElementById('createResult').innerHTML = '<span style="color:var(--color-error);">Numero de parte y nombre son obligatorios</span>';
|
||||
return;
|
||||
@@ -366,7 +415,7 @@
|
||||
loadItems(currentPage);
|
||||
// Close modal, clear form, refresh badges
|
||||
closeCreateModal();
|
||||
['newPartNumber','newName','newBrand','newBarcode','newCost','newPrice1','newMinStock','newInitialStock','newLocation'].forEach(function(id) {
|
||||
['newPartNumber','newName','newBrand','newBarcode','newSku2','newSku3','newCost','newPrice1','newMinStock','newInitialStock','newLocation'].forEach(function(id) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) el.value = '';
|
||||
});
|
||||
@@ -377,6 +426,54 @@
|
||||
});
|
||||
}
|
||||
|
||||
function submitBulkImport() {
|
||||
var fileInput = document.getElementById('bulkImportFile');
|
||||
var resultEl = document.getElementById('bulkImportResult');
|
||||
var mode = document.getElementById('bulkImportMode').value;
|
||||
var strategy = document.getElementById('bulkImportStrategy').value;
|
||||
if (!fileInput.files || !fileInput.files[0]) {
|
||||
resultEl.style.display = 'block';
|
||||
resultEl.innerHTML = '<span style="color:var(--color-error);">Selecciona un archivo CSV o Excel.</span>';
|
||||
return;
|
||||
}
|
||||
var file = fileInput.files[0];
|
||||
var formData = new FormData();
|
||||
formData.append('file', file);
|
||||
resultEl.style.display = 'block';
|
||||
resultEl.innerHTML = '<span style="color:var(--color-text-muted);">Importando...</span>';
|
||||
fetch(API + '/items/bulk-import', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'X-Import-Mode': mode,
|
||||
'X-Import-Strategy': strategy
|
||||
},
|
||||
body: formData
|
||||
}).then(function(resp) { return resp.json(); }).then(function(data) {
|
||||
if (data.error) {
|
||||
resultEl.innerHTML = '<span style="color:var(--color-error);">' + esc(data.error) + '</span>';
|
||||
return;
|
||||
}
|
||||
var html = '<div style="color:var(--color-success);">Importacion completada: <strong>' + data.created + '</strong> producto(s) creado(s)';
|
||||
if (data.skipped > 0) html += ', <strong>' + data.skipped + '</strong> saltado(s)';
|
||||
html += '</div>';
|
||||
if (data.warnings && data.warnings.length) {
|
||||
html += '<div style="margin-top:8px;max-height:160px;overflow:auto;background:var(--color-surface);padding:8px;border-radius:6px;font-size:var(--text-caption);">';
|
||||
html += '<strong style="color:var(--color-warning);">Advertencias (' + data.warnings.length + '):</strong><ul style="margin:4px 0 0 16px;padding:0;">';
|
||||
data.warnings.forEach(function(w) {
|
||||
html += '<li>' + esc(w) + '</li>';
|
||||
});
|
||||
html += '</ul></div>';
|
||||
}
|
||||
resultEl.innerHTML = html;
|
||||
loadItems(currentPage);
|
||||
if (window.loadInventoryStats) window.loadInventoryStats();
|
||||
}).catch(function(err) {
|
||||
resultEl.innerHTML = '<span style="color:var(--color-error);">Error de red: ' + esc(err.message) + '</span>';
|
||||
});
|
||||
}
|
||||
window.submitBulkImport = submitBulkImport;
|
||||
|
||||
// =====================================================================
|
||||
// PURCHASE / ENTRADA (purchaseModal)
|
||||
// =====================================================================
|
||||
@@ -1006,17 +1103,36 @@
|
||||
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) {
|
||||
inputHtml = '<select class="meli-title-input" id="meliAttr-' + attrId + '">' +
|
||||
// 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('') +
|
||||
'</select>';
|
||||
'<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"><label>' + attrName + (attr.tags && attr.tags.required ? ' *' : '') + '</label>' + inputHtml + '</div>';
|
||||
html += '<div class="inv-field" id="meliAttrWrap-' + attrId + '"><label>' + attrName + (attr.tags && attr.tags.required ? ' *' : '') + '</label>' + inputHtml + '</div>';
|
||||
});
|
||||
grid.innerHTML = html;
|
||||
}).catch(function() { grid.innerHTML = '<p style="color:var(--color-error);font-size:var(--text-caption);">Error cargando atributos</p>'; });
|
||||
};
|
||||
|
||||
window.onMeliAttrSelectChange = function(attrId) {
|
||||
var sel = document.getElementById('meliAttrSel-' + attrId);
|
||||
var other = document.getElementById('meliAttrOther-' + attrId);
|
||||
if (!sel || !other) return;
|
||||
if (sel.value === '__other__') {
|
||||
other.style.display = 'block';
|
||||
other.focus();
|
||||
} else {
|
||||
other.style.display = 'none';
|
||||
other.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
window.handleMeliCatKeydown = function(e) {
|
||||
if (!meliCatItems.length) return;
|
||||
if (e.key === 'ArrowDown') {
|
||||
@@ -1071,9 +1187,21 @@
|
||||
if (stockEl) customData.stocks[id] = parseInt(stockEl.value);
|
||||
var attrs = [];
|
||||
meliCategoryAttrs.forEach(function(attr) {
|
||||
var el = document.getElementById('meliAttr-' + attr.id);
|
||||
if (el && el.value) {
|
||||
attrs.push({ id: attr.id, value_name: el.value });
|
||||
var val = '';
|
||||
var sel = document.getElementById('meliAttrSel-' + attr.id);
|
||||
if (sel) {
|
||||
if (sel.value === '__other__') {
|
||||
var otherEl = document.getElementById('meliAttrOther-' + attr.id);
|
||||
val = otherEl ? otherEl.value : '';
|
||||
} else {
|
||||
val = sel.value;
|
||||
}
|
||||
} else {
|
||||
var el = document.getElementById('meliAttr-' + attr.id);
|
||||
if (el) val = el.value;
|
||||
}
|
||||
if (val) {
|
||||
attrs.push({ id: attr.id, value_name: val });
|
||||
}
|
||||
});
|
||||
if (attrs.length) customData.attributes[id] = attrs;
|
||||
@@ -1298,6 +1426,26 @@
|
||||
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) {
|
||||
@@ -1330,12 +1478,19 @@
|
||||
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;">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>';
|
||||
@@ -1415,14 +1570,67 @@
|
||||
el.innerHTML = html2;
|
||||
}
|
||||
|
||||
// Vehicle compatibility 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;">Vehiculos Compatibles</div>';
|
||||
// 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>';
|
||||
|
||||
// Load vehicle compatibilities
|
||||
(function loadCompat() {
|
||||
// Manual add form
|
||||
html += '<div style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:var(--tracking-widest);margin-bottom:8px;">Agregar Manualmente</div>';
|
||||
html += '<div class="compat-form">';
|
||||
html += '<div><label>Marca</label><select class="select-filter" id="manualMake" onchange="onManualMakeChange(' + itemId + ')" style="width:100%;"><option value="">Cargando...</option></select></div>';
|
||||
html += '<div><label>Modelo</label><select class="select-filter" id="manualModel" onchange="onManualModelChange(' + itemId + ')" style="width:100%;" disabled><option value="">Selecciona marca</option></select></div>';
|
||||
html += '<div><label>Ano</label><select class="select-filter" id="manualYear" onchange="onManualYearChange(' + itemId + ')" style="width:100%;" disabled><option value="">Selecciona modelo</option></select></div>';
|
||||
html += '<div><label>Motor</label><select class="select-filter" id="manualEngine" style="width:100%;" disabled><option value="">Selecciona ano</option></select></div>';
|
||||
html += '</div>';
|
||||
html += '<button class="btn btn--primary btn--sm" onclick="submitManualCompat(' + itemId + ')">Agregar compatibilidad</button>';
|
||||
|
||||
// Auto-match button
|
||||
var btnLabel = compatSource === 'qwen' ? 'Auto-Match con IA (QWEN)' : (compatSource === 'both' ? 'Auto-Match (TecDoc + IA)' : 'Auto-Match por TecDoc');
|
||||
html += '<div style="margin-top:16px;"><button class="btn btn--ghost btn--sm" onclick="autoMatchCompat(' + itemId + ')">' + btnLabel + '</button></div>';
|
||||
|
||||
html += '</div>';
|
||||
|
||||
// Load SKU aliases
|
||||
(function loadSkuAliases() {
|
||||
fetch('/pos/api/inventory/items/' + itemId + '/skus', { headers: { 'Authorization': 'Bearer ' + token } })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
var el = document.getElementById('skuAliasContent');
|
||||
if (!el) return;
|
||||
var list = d.aliases || [];
|
||||
var html2 = '';
|
||||
if (list.length > 0) {
|
||||
html2 += '<table class="data-table"><thead><tr><th>SKU</th><th>Etiqueta</th><th></th></tr></thead><tbody>';
|
||||
list.forEach(function(a) {
|
||||
html2 += '<tr><td class="td--mono">' + esc(a.sku) + '</td><td>' + esc(a.label || '-') + '</td>';
|
||||
html2 += '<td><button class="btn btn--ghost btn--sm" style="color:var(--color-error);" onclick="removeSkuAlias(' + itemId + ',' + a.id + ')">Quitar</button></td></tr>';
|
||||
});
|
||||
html2 += '</tbody></table>';
|
||||
} else {
|
||||
html2 += '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Sin SKU alternativos.</p>';
|
||||
}
|
||||
html2 += '<div style="margin-top:8px;display:flex;gap:8px;">';
|
||||
html2 += '<input type="text" class="meli-title-input" id="newAliasSku-' + itemId + '" placeholder="Nuevo SKU" style="flex:1;">';
|
||||
html2 += '<input type="text" class="meli-title-input" id="newAliasLabel-' + itemId + '" placeholder="Etiqueta (opcional)" style="flex:1;">';
|
||||
html2 += '<button class="btn btn--primary btn--sm" onclick="addSkuAlias(' + itemId + ')">Agregar</button>';
|
||||
html2 += '</div>';
|
||||
el.innerHTML = html2;
|
||||
})
|
||||
.catch(function() {
|
||||
var el = document.getElementById('skuAliasContent');
|
||||
if (el) el.innerHTML = '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Error al cargar SKU alternativos.</p>';
|
||||
});
|
||||
})();
|
||||
|
||||
// Load vehicle compatibilities and makes
|
||||
(function loadCompatPanel() {
|
||||
fetch('/pos/api/inventory/items/' + itemId + '/vehicles', { headers: { 'Authorization': 'Bearer ' + token } })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
@@ -1441,15 +1649,28 @@
|
||||
} else {
|
||||
html2 += '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Sin vehiculos vinculados.</p>';
|
||||
}
|
||||
var btnLabel = compatSource === 'qwen' ? 'Auto-Match con IA (QWEN)' : (compatSource === 'both' ? 'Auto-Match (TecDoc + IA)' : 'Auto-Match por TecDoc');
|
||||
var btnDesc = compatSource === 'qwen' ? 'Busca compatibilidad usando inteligencia artificial' : (compatSource === 'both' ? 'Busca en catalogo central y con IA' : 'Busca en catalogo central y vincula automaticamente');
|
||||
html2 += '<div style="margin-top:8px;"><button class="btn btn--primary btn--sm" onclick="autoMatchCompat(' + itemId + ')">' + btnLabel + '</button> <span style="font-size:var(--text-caption);color:var(--color-text-muted);">' + btnDesc + '</span></div>';
|
||||
el.innerHTML = html2;
|
||||
})
|
||||
.catch(function() {
|
||||
var el = document.getElementById('compatContent');
|
||||
if (el) el.innerHTML = '<p style="color:var(--color-text-muted);font-size:var(--text-caption);">Error al cargar compatibilidades.</p>';
|
||||
});
|
||||
|
||||
// Load makes
|
||||
fetch('/pos/api/inventory/vehicles/makes', { headers: { 'Authorization': 'Bearer ' + token } })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
var sel = document.getElementById('manualMake');
|
||||
if (!sel) return;
|
||||
var opts = '<option value="">Selecciona marca</option>';
|
||||
(d.makes || []).forEach(function(m) { opts += '<option value="' + esc(m.name) + '" data-id="' + m.id + '">' + esc(m.name) + '</option>'; });
|
||||
sel.innerHTML = opts;
|
||||
sel.disabled = false;
|
||||
})
|
||||
.catch(function() {
|
||||
var sel = document.getElementById('manualMake');
|
||||
if (sel) { sel.innerHTML = '<option value="">Error al cargar</option>'; }
|
||||
});
|
||||
})();
|
||||
|
||||
// Movement history
|
||||
@@ -1510,6 +1731,150 @@
|
||||
}).catch(function() { alert('Error al quitar compatibilidad'); });
|
||||
}
|
||||
|
||||
// Manual compatibility tab functions
|
||||
window.switchCompatTab = function(tab, btn) {
|
||||
document.querySelectorAll('.compat-tab-btn').forEach(function(b) { b.classList.remove('is-active'); });
|
||||
document.querySelectorAll('.compat-tab-panel').forEach(function(p) { p.classList.remove('is-active'); });
|
||||
btn.classList.add('is-active');
|
||||
document.getElementById('compatTab-' + tab).classList.add('is-active');
|
||||
};
|
||||
|
||||
window.onManualMakeChange = function(itemId) {
|
||||
var sel = document.getElementById('manualMake');
|
||||
var modelSel = document.getElementById('manualModel');
|
||||
var yearSel = document.getElementById('manualYear');
|
||||
var engineSel = document.getElementById('manualEngine');
|
||||
if (!sel || !modelSel) return;
|
||||
var opt = sel.options[sel.selectedIndex];
|
||||
var brandId = opt ? opt.getAttribute('data-id') : null;
|
||||
modelSel.innerHTML = '<option value="">Cargando...</option>';
|
||||
modelSel.disabled = true;
|
||||
yearSel.innerHTML = '<option value="">Selecciona modelo</option>';
|
||||
yearSel.disabled = true;
|
||||
engineSel.innerHTML = '<option value="">Selecciona ano</option>';
|
||||
engineSel.disabled = true;
|
||||
if (!brandId) {
|
||||
modelSel.innerHTML = '<option value="">Selecciona marca</option>';
|
||||
return;
|
||||
}
|
||||
fetch('/pos/api/inventory/vehicles/models?brand_id=' + brandId, { headers: { 'Authorization': 'Bearer ' + token } })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
var opts = '<option value="">Selecciona modelo</option>';
|
||||
(d.models || []).forEach(function(m) { opts += '<option value="' + esc(m.name) + '" data-id="' + m.id + '">' + esc(m.name) + '</option>'; });
|
||||
modelSel.innerHTML = opts;
|
||||
modelSel.disabled = false;
|
||||
})
|
||||
.catch(function() { modelSel.innerHTML = '<option value="">Error</option>'; });
|
||||
};
|
||||
|
||||
window.onManualModelChange = function(itemId) {
|
||||
var modelSel = document.getElementById('manualModel');
|
||||
var yearSel = document.getElementById('manualYear');
|
||||
var engineSel = document.getElementById('manualEngine');
|
||||
if (!modelSel || !yearSel) return;
|
||||
var opt = modelSel.options[modelSel.selectedIndex];
|
||||
var modelId = opt ? opt.getAttribute('data-id') : null;
|
||||
yearSel.innerHTML = '<option value="">Cargando...</option>';
|
||||
yearSel.disabled = true;
|
||||
engineSel.innerHTML = '<option value="">Selecciona ano</option>';
|
||||
engineSel.disabled = true;
|
||||
if (!modelId) {
|
||||
yearSel.innerHTML = '<option value="">Selecciona modelo</option>';
|
||||
return;
|
||||
}
|
||||
fetch('/pos/api/inventory/vehicles/years?model_id=' + modelId, { headers: { 'Authorization': 'Bearer ' + token } })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
var opts = '<option value="">Selecciona ano</option>';
|
||||
(d.years || []).forEach(function(y) { opts += '<option value="' + y.year + '" data-id="' + y.id + '">' + y.year + '</option>'; });
|
||||
yearSel.innerHTML = opts;
|
||||
yearSel.disabled = false;
|
||||
})
|
||||
.catch(function() { yearSel.innerHTML = '<option value="">Error</option>'; });
|
||||
};
|
||||
|
||||
window.onManualYearChange = function(itemId) {
|
||||
var modelSel = document.getElementById('manualModel');
|
||||
var yearSel = document.getElementById('manualYear');
|
||||
var engineSel = document.getElementById('manualEngine');
|
||||
if (!modelSel || !yearSel || !engineSel) return;
|
||||
var mOpt = modelSel.options[modelSel.selectedIndex];
|
||||
var yOpt = yearSel.options[yearSel.selectedIndex];
|
||||
var modelId = mOpt ? mOpt.getAttribute('data-id') : null;
|
||||
var yearId = yOpt ? yOpt.getAttribute('data-id') : null;
|
||||
engineSel.innerHTML = '<option value="">Cargando...</option>';
|
||||
engineSel.disabled = true;
|
||||
if (!modelId || !yearId) {
|
||||
engineSel.innerHTML = '<option value="">Selecciona ano</option>';
|
||||
return;
|
||||
}
|
||||
fetch('/pos/api/inventory/vehicles/engines?model_id=' + modelId + '&year_id=' + yearId, { headers: { 'Authorization': 'Bearer ' + token } })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
var opts = '<option value="">Selecciona motor</option>';
|
||||
(d.engines || []).forEach(function(e) { opts += '<option value="' + esc(e.name) + '" data-code="' + esc(e.code || '') + '">' + esc(e.name + (e.code ? ' (' + e.code + ')' : '')) + '</option>'; });
|
||||
engineSel.innerHTML = opts;
|
||||
engineSel.disabled = false;
|
||||
})
|
||||
.catch(function() { engineSel.innerHTML = '<option value="">Error</option>'; });
|
||||
};
|
||||
|
||||
window.submitManualCompat = function(itemId) {
|
||||
var makeSel = document.getElementById('manualMake');
|
||||
var modelSel = document.getElementById('manualModel');
|
||||
var yearSel = document.getElementById('manualYear');
|
||||
var engineSel = document.getElementById('manualEngine');
|
||||
if (!makeSel || !modelSel || !yearSel) return;
|
||||
var make = makeSel.value;
|
||||
var model = modelSel.value;
|
||||
var year = yearSel.value;
|
||||
var engine = engineSel ? engineSel.value : '';
|
||||
var engineCode = engineSel && engineSel.selectedIndex > 0 ? (engineSel.options[engineSel.selectedIndex].getAttribute('data-code') || '') : '';
|
||||
if (!make || !model || !year) {
|
||||
alert('Selecciona al menos marca, modelo y ano');
|
||||
return;
|
||||
}
|
||||
fetch('/pos/api/inventory/items/' + itemId + '/vehicles/manual', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ make: make, model: model, year: parseInt(year), engine: engine, engine_code: engineCode })
|
||||
}).then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d.error) { alert(d.error); return; }
|
||||
viewProductDetail(itemId);
|
||||
}).catch(function() { alert('Error al agregar compatibilidad'); });
|
||||
};
|
||||
|
||||
// SKU alias actions
|
||||
window.addSkuAlias = function(itemId) {
|
||||
var skuEl = document.getElementById('newAliasSku-' + itemId);
|
||||
var labelEl = document.getElementById('newAliasLabel-' + itemId);
|
||||
var sku = skuEl ? skuEl.value.trim() : '';
|
||||
var label = labelEl ? labelEl.value.trim() : '';
|
||||
if (!sku) { alert('Ingresa un SKU'); return; }
|
||||
fetch('/pos/api/inventory/items/' + itemId + '/skus', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sku: sku, label: label })
|
||||
}).then(function(r) { return r.json(); })
|
||||
.then(function(d) {
|
||||
if (d.error) { alert(d.error); return; }
|
||||
viewProductDetail(itemId);
|
||||
}).catch(function() { alert('Error al agregar SKU'); });
|
||||
};
|
||||
|
||||
window.removeSkuAlias = function(itemId, aliasId) {
|
||||
if (!confirm('Eliminar este SKU alternativo?')) return;
|
||||
fetch('/pos/api/inventory/items/' + itemId + '/skus/' + aliasId, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
}).then(function(r) { return r.json(); })
|
||||
.then(function() {
|
||||
viewProductDetail(itemId);
|
||||
}).catch(function() { alert('Error al eliminar SKU'); });
|
||||
};
|
||||
|
||||
// =====================================================================
|
||||
// EXPOSE GLOBALS (for onclick handlers in HTML)
|
||||
// =====================================================================
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
});
|
||||
if (tab === 'listings') loadListings();
|
||||
if (tab === 'orders') loadOrders();
|
||||
if (tab === 'questions') loadQuestions();
|
||||
};
|
||||
|
||||
function closeModal(id) {
|
||||
@@ -81,7 +82,7 @@
|
||||
localStorage.setItem('meli_shipping', shipping);
|
||||
|
||||
var redirectUri = window.location.origin + '/pos/marketplace-external/callback';
|
||||
var authUrl = 'https://auth.mercadolibre.com.mx/authorization?response_type=code&client_id=' + encodeURIComponent(clientId) + '&redirect_uri=' + encodeURIComponent(redirectUri);
|
||||
var authUrl = 'https://auth.mercadolibre.com.mx/authorization?response_type=code&client_id=' + encodeURIComponent(clientId) + '&redirect_uri=' + encodeURIComponent(redirectUri) + '&scope=read+write+offline_access';
|
||||
window.location.href = authUrl;
|
||||
};
|
||||
|
||||
@@ -148,16 +149,18 @@
|
||||
var statusClass = 'meli-status--' + (l.external_status || 'pending');
|
||||
return '<div class="meli-card">'
|
||||
+ '<div style="display:flex;justify-content:space-between;align-items:start;margin-bottom:var(--space-3);">'
|
||||
+ '<div style="font-weight:700;font-size:var(--text-body-sm);line-height:1.3;">' + escapeHtml(l.title || l.inventory_name || 'Sin título') + '</div>'
|
||||
+ '<a href="' + escapeHtml(l.external_permalink || '#') + '" target="_blank" rel="noopener" style="font-weight:700;font-size:var(--text-body-sm);line-height:1.3;color:var(--color-primary);text-decoration:none;">' + escapeHtml(l.title || l.inventory_name || 'Sin título') + ' ↗</a>'
|
||||
+ '<span class="meli-status ' + statusClass + '">' + (l.external_status || '—') + '</span>'
|
||||
+ '</div>'
|
||||
+ '<div style="font-size:var(--text-caption);color:var(--color-text-muted);margin-bottom:var(--space-2);">'
|
||||
+ 'SKU: ' + escapeHtml(l.part_number || '—') + ' · ID ML: ' + escapeHtml(l.external_item_id || '—')
|
||||
+ 'SKU: ' + escapeHtml(l.part_number || '—') + ' · ID ML: <a href="' + escapeHtml(l.external_permalink || '#') + '" target="_blank" rel="noopener" style="color:var(--color-primary);">' + escapeHtml(l.external_item_id || '—') + '</a>'
|
||||
+ '</div>'
|
||||
+ '<div style="display:flex;gap:var(--space-2);margin-top:var(--space-3);">'
|
||||
+ '<button class="btn btn--ghost btn--xs" onclick="syncListing(' + l.id + ')">Sync</button>'
|
||||
+ (l.external_status === 'active' ? '<button class="btn btn--ghost btn--xs" onclick="pauseListing(' + l.id + ')">Pausar</button>' : '<button class="btn btn--ghost btn--xs" onclick="activateListing(' + l.id + ')">Activar</button>')
|
||||
+ '<button class="btn btn--danger btn--xs" onclick="deleteListing(' + l.id + ')">Cerrar</button>'
|
||||
+ (l.external_status === 'closed' || !l.is_active
|
||||
? '<button class="btn btn--danger btn--xs" onclick="deleteListingPermanently(' + l.id + ')">Eliminar</button>'
|
||||
: '<button class="btn btn--danger btn--xs" onclick="deleteListing(' + l.id + ')">Cerrar</button>')
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
@@ -202,6 +205,14 @@
|
||||
} catch (e) { alert('Error: ' + e.message); }
|
||||
};
|
||||
|
||||
window.deleteListingPermanently = async function(id) {
|
||||
if (!confirm('¿Eliminar permanentemente esta publicación del listado local? Esta acción no se puede deshacer.')) return;
|
||||
try {
|
||||
var res = await fetch(API + '/listings/' + id + '/permanent', { method: 'DELETE', headers: headers() });
|
||||
if (res.ok) { loadListings(); } else { alert('Error al eliminar'); }
|
||||
} catch (e) { alert('Error: ' + e.message); }
|
||||
};
|
||||
|
||||
// ─── Orders ────────────────────────────────────────────────────────────
|
||||
|
||||
var ordersData = [];
|
||||
@@ -451,11 +462,144 @@
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// ─── Questions ─────────────────────────────────────────────────────────
|
||||
|
||||
var questionsData = [];
|
||||
|
||||
window.loadQuestions = async function() {
|
||||
var container = document.getElementById('questionsContainer');
|
||||
container.innerHTML = '<div class="skeleton-grid">' + Array(6).fill('<div class="skeleton skeleton--card"></div>').join('') + '</div>';
|
||||
try {
|
||||
var res = await fetch(API + '/questions', { headers: headers() });
|
||||
if (!res.ok) throw new Error('Failed to load questions');
|
||||
var data = await res.json();
|
||||
questionsData = data.items || [];
|
||||
renderQuestions();
|
||||
} catch (e) {
|
||||
container.innerHTML = renderEmptyState({
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
|
||||
title: 'Sin preguntas',
|
||||
subtitle: 'No hay preguntas de compradores pendientes. Sincroniza con MercadoLibre para obtenerlas.',
|
||||
action: '<button class="btn btn--meli btn--sm" onclick="syncQuestions()">Sincronizar con ML</button>'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function renderQuestions() {
|
||||
var container = document.getElementById('questionsContainer');
|
||||
var statusFilter = document.getElementById('questionStatusFilter').value;
|
||||
var search = document.getElementById('questionSearch').value.toLowerCase();
|
||||
|
||||
var filtered = questionsData.filter(function(q) {
|
||||
if (statusFilter && q.status !== statusFilter) return false;
|
||||
if (search && !((q.question_text || '').toLowerCase().includes(search)) && !((q.listing_title || '').toLowerCase().includes(search))) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Stats bar
|
||||
var unanswered = questionsData.filter(function(q) { return q.status === 'unanswered'; }).length;
|
||||
var answered = questionsData.filter(function(q) { return q.status === 'answered'; }).length;
|
||||
var total = questionsData.length;
|
||||
var statsHtml = '<div style="display:flex;gap:var(--space-4);flex-wrap:wrap;margin-bottom:var(--space-4);">' +
|
||||
'<div class="meli-card" style="flex:1;min-width:140px;text-align:center;"><div style="font-size:28px;font-weight:800;color:var(--color-primary);">' + total + '</div><div style="font-size:var(--text-caption);color:var(--color-text-muted);">Total preguntas</div></div>' +
|
||||
'<div class="meli-card" style="flex:1;min-width:140px;text-align:center;"><div style="font-size:28px;font-weight:800;color:var(--color-error);">' + unanswered + '</div><div style="font-size:var(--text-caption);color:var(--color-text-muted);">Sin responder</div></div>' +
|
||||
'<div class="meli-card" style="flex:1;min-width:140px;text-align:center;"><div style="font-size:28px;font-weight:800;color:var(--color-success);">' + answered + '</div><div style="font-size:var(--text-caption);color:var(--color-text-muted);">Respondidas</div></div>' +
|
||||
'</div>';
|
||||
document.getElementById('questionsStatsBar').innerHTML = statsHtml;
|
||||
|
||||
if (!filtered.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"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
|
||||
title: 'Sin preguntas',
|
||||
subtitle: statusFilter ? 'No hay preguntas con el filtro seleccionado.' : 'No hay preguntas sincronizadas.',
|
||||
action: ''
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = filtered.map(function(q) {
|
||||
var statusClass = 'meli-status--' + (q.status || 'pending');
|
||||
var statusLabel = q.status === 'unanswered' ? 'Sin responder' : (q.status === 'answered' ? 'Respondida' : (q.status || '—'));
|
||||
var answerHtml = '';
|
||||
if (q.status === 'unanswered') {
|
||||
answerHtml = '<div style="margin-top:var(--space-2);">' +
|
||||
'<textarea class="meli-title-input" id="qAnswer-' + q.id + '" rows="2" placeholder="Escribe tu respuesta..."></textarea>' +
|
||||
'<button class="btn btn--primary btn--xs" style="margin-top:6px;" onclick="submitAnswer(' + q.id + ')">Enviar respuesta</button>' +
|
||||
'</div>';
|
||||
} else if (q.answer_text) {
|
||||
answerHtml = '<div style="margin-top:var(--space-2);padding:var(--space-2);background:var(--color-surface-0);border-radius:var(--radius-sm);font-size:var(--text-caption);color:var(--color-text-secondary);">' +
|
||||
'<strong>Respuesta:</strong> ' + escapeHtml(q.answer_text) +
|
||||
'</div>';
|
||||
}
|
||||
return '<div class="meli-card">'
|
||||
+ '<div style="display:flex;justify-content:space-between;align-items:start;margin-bottom:var(--space-3);">'
|
||||
+ '<div style="font-weight:700;font-size:var(--text-body-sm);line-height:1.3;">' + escapeHtml(q.listing_title || 'Artículo sin título') + '</div>'
|
||||
+ '<span class="meli-status ' + statusClass + '">' + statusLabel + '</span>'
|
||||
+ '</div>'
|
||||
+ '<div style="font-size:var(--text-caption);color:var(--color-text-muted);margin-bottom:var(--space-2);">'
|
||||
+ 'Comprador: ' + escapeHtml(q.buyer_nickname || '—') + ' · ' + (q.question_date ? new Date(q.question_date).toLocaleString('es-MX') : '—')
|
||||
+ '</div>'
|
||||
+ '<div style="font-size:var(--text-body-sm);color:var(--color-text-primary);margin-bottom:var(--space-2);">'
|
||||
+ '<strong>Pregunta:</strong> ' + escapeHtml(q.question_text)
|
||||
+ '</div>'
|
||||
+ answerHtml
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
window.filterQuestions = renderQuestions;
|
||||
|
||||
window.syncQuestions = async function() {
|
||||
var btn = document.querySelector('#panel-questions .btn--primary');
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Sincronizando...'; }
|
||||
try {
|
||||
var res = await fetch(API + '/questions/sync', { method: 'POST', headers: headers() });
|
||||
var data = await res.json();
|
||||
if (res.ok) {
|
||||
showToast('Sincronizadas ' + (data.synced || 0) + ' preguntas', 'ok', { title: 'Sincronización' });
|
||||
loadQuestions();
|
||||
} else {
|
||||
showToast(data.error || 'Error al sincronizar', 'error', { title: 'Error' });
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error', { title: 'Error de red' });
|
||||
} finally {
|
||||
if (btn) { btn.disabled = false; btn.textContent = '🔄 Actualizar'; }
|
||||
}
|
||||
};
|
||||
|
||||
window.submitAnswer = async function(questionId) {
|
||||
var textarea = document.getElementById('qAnswer-' + questionId);
|
||||
if (!textarea) return;
|
||||
var text = textarea.value.trim();
|
||||
if (!text) {
|
||||
showToast('Escribe una respuesta antes de enviar', 'error', { title: 'Respuesta vacía' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var res = await fetch(API + '/questions/' + questionId + '/answer', {
|
||||
method: 'POST',
|
||||
headers: headers(),
|
||||
body: JSON.stringify({ text: text })
|
||||
});
|
||||
var data = await res.json();
|
||||
if (res.ok) {
|
||||
showToast('Respuesta enviada correctamente', 'ok', { title: 'Pregunta respondida' });
|
||||
loadQuestions();
|
||||
} else {
|
||||
showToast(data.error || 'Error al enviar respuesta', 'error', { title: 'Error' });
|
||||
}
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error', { title: 'Error de red' });
|
||||
}
|
||||
};
|
||||
|
||||
// Register Cmd+K items
|
||||
if (typeof registerCmdKItem === 'function') {
|
||||
registerCmdKItem({ group: 'MercadoLibre', label: 'Configuración ML', href: '/pos/marketplace-external', icon: '⚙️' });
|
||||
registerCmdKItem({ group: 'MercadoLibre', label: 'Publicaciones ML', href: '/pos/marketplace-external#listings', icon: '📦' });
|
||||
registerCmdKItem({ group: 'MercadoLibre', label: 'Órdenes ML', href: '/pos/marketplace-external#orders', icon: '🛒' });
|
||||
registerCmdKItem({ group: 'MercadoLibre', label: 'Preguntas ML', href: '/pos/marketplace-external#questions', icon: '❓' });
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
299
pos/static/js/supplier_catalog.js
Normal file
299
pos/static/js/supplier_catalog.js
Normal file
@@ -0,0 +1,299 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const API = '/pos/api/supplier-catalog';
|
||||
const VEHICLE_API = '/pos/api/inventory/vehicles';
|
||||
const token = localStorage.getItem('pos_token') || '';
|
||||
|
||||
let state = {
|
||||
q: '',
|
||||
category: '',
|
||||
make: '',
|
||||
model: '',
|
||||
year: '',
|
||||
engine: '',
|
||||
myeId: null,
|
||||
page: 1,
|
||||
perPage: 30,
|
||||
totalPages: 1,
|
||||
categories: [],
|
||||
items: []
|
||||
};
|
||||
|
||||
function headers() {
|
||||
return { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' };
|
||||
}
|
||||
|
||||
let scAbort = null;
|
||||
let scSeq = 0;
|
||||
|
||||
async function apiFetch(url) {
|
||||
if (scAbort) {
|
||||
scAbort.abort();
|
||||
scAbort = null;
|
||||
}
|
||||
const ctrl = new AbortController();
|
||||
scAbort = ctrl;
|
||||
try {
|
||||
const resp = await fetch(url, { headers: headers(), signal: ctrl.signal });
|
||||
if (resp.status === 401) { window.location.href = '/pos/login'; return null; }
|
||||
if (!resp.ok) { console.error('API error', url, resp.status); return null; }
|
||||
return resp.json();
|
||||
} catch (e) {
|
||||
if (e.name === 'AbortError') return null;
|
||||
console.error('API error', url, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function apiFetchSeq(url) {
|
||||
const mySeq = ++scSeq;
|
||||
const data = await apiFetch(url);
|
||||
if (!data || scSeq !== mySeq) return null;
|
||||
return data;
|
||||
}
|
||||
|
||||
// ─── Categories ─────────────────────────────────────────────
|
||||
async function loadCategories() {
|
||||
const data = await apiFetch(API + '/categories');
|
||||
if (!data) return;
|
||||
state.categories = data.categories || [];
|
||||
renderCategories();
|
||||
}
|
||||
|
||||
function renderCategories() {
|
||||
const el = document.getElementById('categoriesGrid');
|
||||
if (!el) return;
|
||||
let html = '<div class="sc-cat-card' + (state.category === '' ? ' active' : '') + '" onclick="selectCategory(\'\')">' +
|
||||
'<div>Todas</div><div class="count">' + state.categories.reduce((a,c)=>a+c.count,0) + ' items</div></div>';
|
||||
state.categories.forEach(function(c) {
|
||||
html += '<div class="sc-cat-card' + (state.category === c.name ? ' active' : '') + '" onclick="selectCategory(\'' + escapeHtml(c.name) + '\')">' +
|
||||
'<div>' + escapeHtml(c.name) + '</div><div class="count">' + c.count + ' items</div></div>';
|
||||
});
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
window.selectCategory = function(name) {
|
||||
state.category = name;
|
||||
state.page = 1;
|
||||
renderCategories();
|
||||
doSearch();
|
||||
};
|
||||
|
||||
// ─── Vehicle filters ────────────────────────────────────────
|
||||
async function loadMakes() {
|
||||
const data = await apiFetch(VEHICLE_API + '/makes');
|
||||
if (!data) return;
|
||||
const sel = document.getElementById('filterMake');
|
||||
sel.innerHTML = '<option value="">Marca vehiculo</option>';
|
||||
(data.data || []).forEach(function(m) {
|
||||
sel.innerHTML += '<option value="' + escapeHtml(m.name_brand) + '">' + escapeHtml(m.name_brand) + '</option>';
|
||||
});
|
||||
}
|
||||
|
||||
window.onMakeChange = async function() {
|
||||
const sel = document.getElementById('filterMake');
|
||||
state.make = sel.value;
|
||||
state.model = ''; state.year = ''; state.engine = ''; state.myeId = null;
|
||||
document.getElementById('filterModel').disabled = true;
|
||||
document.getElementById('filterYear').disabled = true;
|
||||
document.getElementById('filterEngine').disabled = true;
|
||||
if (!state.make) { doSearch(); return; }
|
||||
|
||||
const makes = await apiFetchSeq(VEHICLE_API + '/makes');
|
||||
if (!makes) return;
|
||||
const brand = (makes.data || []).find(function(m) { return m.name_brand === state.make; });
|
||||
if (!brand) { doSearch(); return; }
|
||||
|
||||
const models = await apiFetchSeq(VEHICLE_API + '/models?brand_id=' + brand.id_brand);
|
||||
if (!models) return;
|
||||
const msel = document.getElementById('filterModel');
|
||||
msel.innerHTML = '<option value="">Modelo</option>';
|
||||
(models.data || []).forEach(function(m) {
|
||||
msel.innerHTML += '<option value="' + m.id_model + '">' + escapeHtml(m.name_model) + '</option>';
|
||||
});
|
||||
msel.disabled = false;
|
||||
doSearch();
|
||||
};
|
||||
|
||||
window.onModelChange = async function() {
|
||||
const sel = document.getElementById('filterModel');
|
||||
const modelId = sel.value;
|
||||
state.model = modelId ? sel.options[sel.selectedIndex].text : '';
|
||||
state.year = ''; state.engine = ''; state.myeId = null;
|
||||
document.getElementById('filterYear').disabled = true;
|
||||
document.getElementById('filterEngine').disabled = true;
|
||||
if (!modelId) { doSearch(); return; }
|
||||
|
||||
const years = await apiFetchSeq(VEHICLE_API + '/years?model_id=' + modelId);
|
||||
if (!years) return;
|
||||
const ysel = document.getElementById('filterYear');
|
||||
ysel.innerHTML = '<option value="">Año</option>';
|
||||
(years.data || []).forEach(function(y) {
|
||||
ysel.innerHTML += '<option value="' + y.id_year + '">' + y.year_car + '</option>';
|
||||
});
|
||||
ysel.disabled = false;
|
||||
doSearch();
|
||||
};
|
||||
|
||||
window.onYearChange = async function() {
|
||||
const sel = document.getElementById('filterYear');
|
||||
const yearId = sel.value;
|
||||
const modelId = document.getElementById('filterModel').value;
|
||||
state.year = yearId ? sel.options[sel.selectedIndex].text : '';
|
||||
state.engine = ''; state.myeId = null;
|
||||
document.getElementById('filterEngine').disabled = true;
|
||||
if (!yearId || !modelId) { doSearch(); return; }
|
||||
|
||||
const engines = await apiFetchSeq(VEHICLE_API + '/engines?model_id=' + modelId + '&year_id=' + yearId);
|
||||
if (!engines) return;
|
||||
const esel = document.getElementById('filterEngine');
|
||||
esel.innerHTML = '<option value="">Motorizacion</option>';
|
||||
(engines.data || []).forEach(function(e) {
|
||||
const label = escapeHtml(e.name_engine) + (e.trim_level ? ' (' + escapeHtml(e.trim_level) + ')' : '');
|
||||
esel.innerHTML += '<option value="' + e.id_mye + '">' + label + '</option>';
|
||||
});
|
||||
esel.disabled = false;
|
||||
doSearch();
|
||||
};
|
||||
|
||||
// ─── Search ─────────────────────────────────────────────────
|
||||
window.doSearch = async function() {
|
||||
state.q = document.getElementById('searchInput').value.trim();
|
||||
const engineSel = document.getElementById('filterEngine');
|
||||
state.myeId = engineSel.value || null;
|
||||
|
||||
let url = API + '/search?page=' + state.page + '&per_page=' + state.perPage;
|
||||
if (state.q) url += '&q=' + encodeURIComponent(state.q);
|
||||
if (state.category) url += '&category=' + encodeURIComponent(state.category);
|
||||
if (state.myeId) {
|
||||
url += '&mye_id=' + state.myeId;
|
||||
} else {
|
||||
if (state.make) url += '&make=' + encodeURIComponent(state.make);
|
||||
if (state.model) url += '&model=' + encodeURIComponent(state.model);
|
||||
if (state.year) url += '&year=' + encodeURIComponent(state.year);
|
||||
}
|
||||
|
||||
const data = await apiFetch(url);
|
||||
if (!data) return;
|
||||
state.items = data.data || [];
|
||||
state.totalPages = (data.pagination || {}).total_pages || 1;
|
||||
renderItems();
|
||||
renderPagination();
|
||||
};
|
||||
|
||||
window.clearFilters = function() {
|
||||
document.getElementById('searchInput').value = '';
|
||||
document.getElementById('filterMake').value = '';
|
||||
document.getElementById('filterModel').innerHTML = '<option value="">Modelo</option>'; document.getElementById('filterModel').disabled = true;
|
||||
document.getElementById('filterYear').innerHTML = '<option value="">Año</option>'; document.getElementById('filterYear').disabled = true;
|
||||
document.getElementById('filterEngine').innerHTML = '<option value="">Motorizacion</option>'; document.getElementById('filterEngine').disabled = true;
|
||||
state.q = ''; state.category = ''; state.make = ''; state.model = ''; state.year = ''; state.engine = ''; state.myeId = null; state.page = 1;
|
||||
renderCategories();
|
||||
doSearch();
|
||||
};
|
||||
|
||||
// ─── Render results ─────────────────────────────────────────
|
||||
function renderItems() {
|
||||
const el = document.getElementById('partsGrid');
|
||||
if (!el) return;
|
||||
if (!state.items.length) {
|
||||
el.innerHTML = '<div class="sc-empty" style="grid-column:1/-1;"><div style="font-size:48px;margin-bottom:var(--space-4);">🔍</div><h3>Sin resultados</h3><p>Intenta con otros filtros o terminos de busqueda.</p></div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = state.items.map(function(it) {
|
||||
return '<div class="sc-card" onclick="openDetail(' + it.id + ')">' +
|
||||
'<div class="sc-card__sku">' + escapeHtml(it.sku) + '</div>' +
|
||||
'<div class="sc-card__name">' + escapeHtml(it.name) + '</div>' +
|
||||
'<div class="sc-card__meta">' +
|
||||
'<span class="sc-card__badge">' + escapeHtml(it.category || 'SIN CATEGORIA') + '</span>' +
|
||||
' <span>' + escapeHtml(it.supplier_name) + '</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderPagination() {
|
||||
const el = document.getElementById('pagination');
|
||||
if (!el) return;
|
||||
if (state.totalPages <= 1) { el.innerHTML = ''; return; }
|
||||
let html = '<button ' + (state.page <= 1 ? 'disabled' : '') + ' onclick="goPage(' + (state.page - 1) + ')">Anterior</button>';
|
||||
html += '<span>Pagina ' + state.page + ' de ' + state.totalPages + '</span>';
|
||||
html += '<button ' + (state.page >= state.totalPages ? 'disabled' : '') + ' onclick="goPage(' + (state.page + 1) + ')">Siguiente</button>';
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
window.goPage = function(p) {
|
||||
state.page = p;
|
||||
doSearch();
|
||||
};
|
||||
|
||||
// ─── Detail modal ───────────────────────────────────────────
|
||||
window.openDetail = async function(id) {
|
||||
const data = await apiFetch(API + '/items/' + id);
|
||||
if (!data) return;
|
||||
document.getElementById('modalTitle').textContent = escapeHtml(data.sku);
|
||||
let html = '';
|
||||
html += '<div><strong style="font-size:var(--text-h6);">' + escapeHtml(data.name) + '</strong></div>';
|
||||
html += '<div class="sc-modal__section"><h4>Informacion</h4>' +
|
||||
'<p>Proveedor: ' + escapeHtml(data.supplier_name) + '<br>Categoria: ' + escapeHtml(data.category || 'N/A') + '</p></div>';
|
||||
|
||||
if (data.interchanges && data.interchanges.length) {
|
||||
html += '<div class="sc-modal__section"><h4>Intercambios</h4><div class="sc-interchange-list">' +
|
||||
data.interchanges.map(function(ix) {
|
||||
return '<span class="sc-interchange-chip">' + escapeHtml(ix.brand) + ' — ' + escapeHtml(ix.part_number) + '</span>';
|
||||
}).join('') + '</div></div>';
|
||||
}
|
||||
|
||||
if (data.compatibilities && data.compatibilities.length) {
|
||||
var seenCompat = {};
|
||||
var uniqCompat = data.compatibilities.filter(function(c) {
|
||||
var key = (c.make || '') + '|' + (c.model || '') + '|' + (c.year || '') + '|' + (c.engine || '');
|
||||
if (seenCompat[key]) return false;
|
||||
seenCompat[key] = true;
|
||||
return true;
|
||||
});
|
||||
html += '<div class="sc-modal__section"><h4>Vehiculos compatibles (' + uniqCompat.length + ')</h4>' +
|
||||
'<div class="sc-compat-grid">' +
|
||||
uniqCompat.slice(0, 50).map(function(c) {
|
||||
return '<div class="sc-compat-item">' +
|
||||
'<strong>' + escapeHtml(c.make || '') + ' ' + escapeHtml(c.model || '') + '</strong><br>' +
|
||||
(c.year || '') + ' ' + escapeHtml(c.engine || '') +
|
||||
'</div>';
|
||||
}).join('') +
|
||||
(uniqCompat.length > 50 ? '<div style="grid-column:1/-1;text-align:center;color:var(--color-text-muted);">... y ' + (uniqCompat.length - 50) + ' mas</div>' : '') +
|
||||
'</div></div>';
|
||||
}
|
||||
|
||||
document.getElementById('modalBody').innerHTML = html;
|
||||
document.getElementById('detailModal').classList.add('open');
|
||||
};
|
||||
|
||||
window.closeModal = function() {
|
||||
document.getElementById('detailModal').classList.remove('open');
|
||||
};
|
||||
|
||||
// ─── Utils ──────────────────────────────────────────────────
|
||||
function escapeHtml(s) {
|
||||
if (s == null) return '';
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ─── Init ───────────────────────────────────────────────────
|
||||
function init() {
|
||||
if (!token) { window.location.href = '/pos/login'; return; }
|
||||
loadCategories();
|
||||
loadMakes();
|
||||
doSearch().then(function() {
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var id = params.get('id');
|
||||
if (id) { openDetail(parseInt(id)); }
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
@@ -1,27 +1,27 @@
|
||||
// /home/Autopartes/pos/static/pwa/sw.js
|
||||
// Nexus POS — Service Worker v9
|
||||
// Nexus POS — Service Worker v17
|
||||
// Self-contained vanilla JS. No external imports.
|
||||
//
|
||||
// Bump CACHE_NAME whenever static assets change significantly.
|
||||
// The fetch handler normalizes static asset URLs (strips ?v= query strings)
|
||||
// so templates can use cache-busting query params freely.
|
||||
|
||||
const CACHE_NAME = 'nexus-pos-v11';
|
||||
const CACHE_NAME = 'nexus-pos-v17';
|
||||
|
||||
const APP_SHELL = [
|
||||
'/pos/static/css/tokens.css',
|
||||
'/pos/static/css/common.css',
|
||||
'/pos/static/css/pos-ui.css',
|
||||
'/pos/static/js/app-init.js',
|
||||
'/pos/static/js/sidebar.js',
|
||||
'/pos/static/js/login.js',
|
||||
'/pos/static/js/pos.js',
|
||||
'/pos/static/js/catalog.js',
|
||||
'/pos/static/js/inventory.js',
|
||||
'/pos/static/js/customers.js',
|
||||
'/pos/static/js/invoicing.js',
|
||||
'/pos/static/js/accounting.js',
|
||||
'/pos/static/js/dashboard.js',
|
||||
'/pos/static/js/config.js',
|
||||
'/pos/static/js/reports.js',
|
||||
'/pos/static/js/offline-banner.js',
|
||||
'/pos/static/js/sync-engine.js',
|
||||
'/pos/static/js/brand-catalog.js',
|
||||
'/pos/static/js/i18n.js',
|
||||
'/pos/static/js/kiosk.js',
|
||||
'/pos/static/js/splash-loader.js',
|
||||
'/pos/static/js/pos-utils.js',
|
||||
'/pos/static/js/pwa-install.js',
|
||||
'/pos/static/js/chat.js',
|
||||
'/pos/static/pwa/manifest.json',
|
||||
'/pos/static/pwa/icon-192.png',
|
||||
'/pos/static/pwa/icon-512.png'
|
||||
@@ -103,6 +103,12 @@ self.addEventListener('activate', function (event) {
|
||||
);
|
||||
}).then(function () {
|
||||
return self.clients.claim();
|
||||
}).then(function () {
|
||||
return self.clients.matchAll({ type: 'window' }).then(function (clients) {
|
||||
clients.forEach(function (client) {
|
||||
client.postMessage({ type: 'SW_UPDATED', cacheName: CACHE_NAME });
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -117,6 +123,13 @@ self.addEventListener('fetch', function (event) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalize cache key for static assets (strip query strings) so
|
||||
// catalog.js?v=5 and catalog.js?v=6 share the same cache entry.
|
||||
var cacheKey = req;
|
||||
if (/\.(js|css|png|jpg|jpeg|webp|svg|gif|ico|woff|woff2|ttf|eot|json)$/.test(url.pathname)) {
|
||||
cacheKey = new Request(url.pathname);
|
||||
}
|
||||
|
||||
// Never cache auth endpoints
|
||||
if (url.pathname.indexOf('/pos/api/auth/') !== -1) {
|
||||
return;
|
||||
@@ -172,16 +185,17 @@ self.addEventListener('fetch', function (event) {
|
||||
}
|
||||
|
||||
// Everything else (JS, CSS, images) -> cache-first
|
||||
event.respondWith(cacheFirst(req));
|
||||
event.respondWith(cacheFirst(req, cacheKey));
|
||||
});
|
||||
|
||||
function cacheFirst(request) {
|
||||
return caches.match(request).then(function (cached) {
|
||||
function cacheFirst(request, cacheKey) {
|
||||
cacheKey = cacheKey || request;
|
||||
return caches.match(cacheKey).then(function (cached) {
|
||||
if (cached) {
|
||||
fetch(request).then(function (response) {
|
||||
if (response && response.status === 200 && request.method === 'GET') {
|
||||
caches.open(CACHE_NAME).then(function (cache) {
|
||||
cache.put(request, response);
|
||||
cache.put(cacheKey, response);
|
||||
});
|
||||
}
|
||||
}).catch(function () {});
|
||||
@@ -191,7 +205,7 @@ function cacheFirst(request) {
|
||||
if (response && response.status === 200 && request.method === 'GET') {
|
||||
var clone = response.clone();
|
||||
caches.open(CACHE_NAME).then(function (cache) {
|
||||
cache.put(request, clone);
|
||||
cache.put(cacheKey, clone);
|
||||
});
|
||||
}
|
||||
return response;
|
||||
|
||||
Reference in New Issue
Block a user